Skip to content

Commit 05c36bd

Browse files
committed
feat(segmentation_user_layer): add individual segment color picker tool
1 parent 3efc904 commit 05c36bd

File tree

3 files changed

+109
-21
lines changed

3 files changed

+109
-21
lines changed

src/layer/segmentation/style.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,19 @@
136136
+ .neuroglancer-tool-button {
137137
margin-left: 1em;
138138
}
139+
140+
.neuroglancer-segment-list-entry .neuroglancer-color-widget {
141+
border: none;
142+
border-color: transparent;
143+
appearance: none;
144+
background-color: transparent;
145+
padding: 0;
146+
margin: 0;
147+
margin-left: 3px;
148+
height: 19px;
149+
width: 20px;
150+
}
151+
152+
.neuroglancer-segment-list-entry .neuroglancer-color-widget.overridden {
153+
background-color: white;
154+
}

src/segmentation_display_state/frontend.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,19 @@ import { observeWatchable, registerNestedSync } from "#src/trackable_value.js";
4747
import { isWithinSelectionPanel } from "#src/ui/selection_details.js";
4848
import type { Uint64Map } from "#src/uint64_map.js";
4949
import { setClipboard } from "#src/util/clipboard.js";
50-
import { useWhiteBackground } from "#src/util/color.js";
50+
import {
51+
packColor,
52+
serializeColor,
53+
TrackableRGB,
54+
useWhiteBackground,
55+
} from "#src/util/color.js";
5156
import { RefCounted } from "#src/util/disposable.js";
5257
import { measureElementClone } from "#src/util/dom.js";
53-
import type { vec3 } from "#src/util/geom.js";
54-
import { kOneVec, vec4 } from "#src/util/geom.js";
58+
import { kOneVec, vec3, vec4 } from "#src/util/geom.js";
5559
import { NullarySignal } from "#src/util/signal.js";
5660
import { Uint64 } from "#src/util/uint64.js";
5761
import { withSharedVisibility } from "#src/visibility_priority/frontend.js";
62+
import { ColorWidget } from "#src/widget/color.js";
5863
import { makeCopyButton } from "#src/widget/copy_button.js";
5964
import { makeEyeButton } from "#src/widget/eye_button.js";
6065
import { makeFilterButton } from "#src/widget/filter_button.js";
@@ -347,6 +352,8 @@ const segmentWidgetTemplate = (() => {
347352
filterElement.classList.add("neuroglancer-segment-list-entry-filter");
348353
const filterIndex = template.childElementCount;
349354
template.appendChild(filterElement);
355+
const colorWidgetIndex = template.childElementCount;
356+
template.appendChild(ColorWidget.template());
350357
return {
351358
template,
352359
copyContainerIndex,
@@ -357,6 +364,7 @@ const segmentWidgetTemplate = (() => {
357364
labelIndex,
358365
filterIndex,
359366
starIndex,
367+
colorWidgetIndex,
360368
unmappedIdIndex: -1,
361369
unmappedCopyIndex: -1,
362370
};
@@ -426,7 +434,7 @@ function makeRegisterSegmentWidgetEventHandlers(
426434
const onMouseEnter = (event: Event) => {
427435
const entryElement = event.currentTarget as HTMLElement;
428436
const idString = entryElement.dataset.id!;
429-
const id = tempStatedColor;
437+
const id = tempObjectId;
430438
id.tryParseString(idString);
431439
displayState.segmentSelectionState.set(id);
432440
if (!isWithinSelectionPanel(entryElement)) {
@@ -437,7 +445,7 @@ function makeRegisterSegmentWidgetEventHandlers(
437445
const selectHandler = (event: Event) => {
438446
const entryElement = event.currentTarget as HTMLElement;
439447
const idString = entryElement.dataset.id!;
440-
const id = tempStatedColor;
448+
const id = tempObjectId;
441449
id.tryParseString(idString);
442450
displayState.selectSegment(
443451
id,
@@ -470,7 +478,7 @@ function makeRegisterSegmentWidgetEventHandlers(
470478
const visibleCheckboxHandler = (event: Event) => {
471479
const entryElement = getEntryElement(event);
472480
const idString = entryElement.dataset.id!;
473-
const id = tempStatedColor;
481+
const id = tempObjectId;
474482
id.tryParseString(idString);
475483
const { selectedSegments, visibleSegments } =
476484
displayState.segmentationGroupState.value;
@@ -486,7 +494,7 @@ function makeRegisterSegmentWidgetEventHandlers(
486494
const filterHandler = (event: Event) => {
487495
const entryElement = getEntryElement(event);
488496
const idString = entryElement.dataset.id!;
489-
const id = tempStatedColor;
497+
const id = tempObjectId;
490498
id.tryParseString(idString);
491499
displayState.filterBySegmentLabel(id);
492500
event.stopPropagation();
@@ -504,7 +512,7 @@ function makeRegisterSegmentWidgetEventHandlers(
504512
}
505513
const entryElement = event.currentTarget as HTMLElement;
506514
const idString = entryElement.dataset.id!;
507-
const id = tempStatedColor;
515+
const id = tempObjectId;
508516
id.tryParseString(idString);
509517
displayState.moveToSegment(id);
510518
};
@@ -539,11 +547,35 @@ function makeRegisterSegmentWidgetEventHandlers(
539547
starButton.addEventListener("click", (event: MouseEvent) => {
540548
const entryElement = getEntryElement(event);
541549
const idString = entryElement.dataset.id!;
542-
const id = tempStatedColor;
550+
const id = tempObjectId;
543551
id.tryParseString(idString);
544552
const { selectedSegments } = displayState.segmentationGroupState.value;
545553
selectedSegments.set(id, !selectedSegments.has(id));
546554
});
555+
556+
const trackableRGB = new TrackableRGB(vec3.fromValues(0, 0, 0));
557+
trackableRGB.changed.add(() => {
558+
const testU = new Uint64(packColor(trackableRGB.value));
559+
const idString = element.dataset.id!;
560+
const id = tempObjectId;
561+
id.tryParseString(idString);
562+
displayState.segmentStatedColors.value.delete(id);
563+
displayState.segmentStatedColors.value.set(id, testU);
564+
});
565+
566+
// TODO, need to register disposer?
567+
new ColorWidget(
568+
trackableRGB,
569+
undefined,
570+
children[template.colorWidgetIndex] as HTMLInputElement,
571+
() => {
572+
const idString = element.dataset.id!;
573+
const id = tempObjectId;
574+
id.tryParseString(idString);
575+
displayState.segmentStatedColors.value.delete(id);
576+
},
577+
false,
578+
);
547579
};
548580
}
549581

@@ -641,7 +673,7 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
641673
}
642674

643675
update(container: HTMLElement) {
644-
const id = tempStatedColor;
676+
const id = tempObjectId;
645677
const idString = container.dataset.id;
646678
if (idString === undefined) return;
647679
id.parseString(idString);
@@ -670,19 +702,26 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
670702
const idContainer = stickyChildren[
671703
template.idContainerIndex
672704
] as HTMLElement;
705+
let color = getBaseObjectColor(this.displayState, mapped) as vec3;
673706
setSegmentIdElementStyle(
674707
idContainer.children[template.idIndex] as HTMLElement,
675-
getBaseObjectColor(this.displayState, mapped) as vec3,
708+
color,
709+
);
710+
const isOverridden =
711+
!!this.displayState?.segmentStatedColors.value.has(mapped);
712+
setColorWidgetColor(
713+
children[template.colorWidgetIndex] as HTMLInputElement,
714+
color,
715+
isOverridden,
676716
);
677717
const { unmappedIdIndex } = template;
678718
if (unmappedIdIndex !== -1) {
679719
let unmappedIdString: string | undefined;
680-
let color: vec3;
681720
if (
682721
displayState!.baseSegmentColoring.value &&
683722
(unmappedIdString = container.dataset.unmappedId) !== undefined
684723
) {
685-
const unmappedId = tempStatedColor;
724+
const unmappedId = tempObjectId;
686725
unmappedId.parseString(unmappedIdString);
687726
color = getBaseObjectColor(this.displayState, unmappedId) as vec3;
688727
} else {
@@ -692,6 +731,13 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
692731
idContainer.children[unmappedIdIndex] as HTMLElement,
693732
color,
694733
);
734+
const isOverridden =
735+
!!this.displayState?.segmentStatedColors.value.has(mapped);
736+
setColorWidgetColor(
737+
children[template.colorWidgetIndex] as HTMLInputElement,
738+
color,
739+
isOverridden,
740+
);
695741
}
696742
}
697743
}
@@ -701,6 +747,15 @@ function setSegmentIdElementStyle(element: HTMLElement, color: vec3) {
701747
element.style.color = useWhiteBackground(color) ? "white" : "black";
702748
}
703749

750+
function setColorWidgetColor(
751+
element: HTMLInputElement,
752+
color: vec3,
753+
isOverridden: boolean,
754+
) {
755+
element.value = serializeColor(color.subarray(0, 3) as vec3);
756+
element.classList.toggle("overridden", isOverridden);
757+
}
758+
704759
export class SegmentWidgetWithExtraColumnsFactory extends SegmentWidgetFactory<SegmentWidgetWithExtraColumnsTemplate> {
705760
segmentPropertyMap: PreprocessedSegmentPropertyMap | undefined;
706761
numericalProperties: InlineSegmentNumericalProperty[];
@@ -885,6 +940,9 @@ export function registerCallbackWhenSegmentationDisplayStateChanged(
885940
displayState.baseSegmentColoring.changed.add(callback),
886941
);
887942
context.registerDisposer(displayState.hoverHighlight.changed.add(callback));
943+
context.registerDisposer(
944+
displayState.segmentStatedColors.changed.add(callback),
945+
);
888946
}
889947

890948
export function registerRedrawWhenSegmentationDisplayStateChanged(
@@ -941,6 +999,7 @@ export function registerRedrawWhenSegmentationDisplayState3DChanged(
941999
* Temporary values used by getObjectColor.
9421000
*/
9431001
const tempColor = vec4.create();
1002+
const tempObjectId = new Uint64();
9441003
const tempStatedColor = new Uint64();
9451004

9461005
export function getBaseObjectColor(

src/widget/color.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,35 @@ import { vec3 } from "#src/util/geom.js";
2323
export class ColorWidget<
2424
Color extends vec3 | undefined = vec3,
2525
> extends RefCounted {
26-
element = document.createElement("input");
26+
static template() {
27+
const element = document.createElement("input");
28+
element.classList.add("neuroglancer-color-widget");
29+
element.type = "color";
30+
return element;
31+
}
2732

2833
constructor(
2934
public model: WatchableValueInterface<Color>,
3035
public getDefaultColor: () => vec3 = () => vec3.fromValues(1, 0, 0),
36+
public element = ColorWidget.template(),
37+
public unsetHandler = () => {},
38+
enableWheel = true,
3139
) {
3240
super();
33-
const { element } = this;
34-
element.classList.add("neuroglancer-color-widget");
35-
element.type = "color";
3641
element.addEventListener("change", () => this.updateModel());
3742
element.addEventListener("input", () => this.updateModel());
38-
element.addEventListener("wheel", (event) => {
39-
event.stopPropagation();
40-
event.preventDefault();
41-
this.adjustHueViaWheel(event);
43+
if (enableWheel) {
44+
element.addEventListener("wheel", (event) => {
45+
event.stopPropagation();
46+
event.preventDefault();
47+
this.adjustHueViaWheel(event);
48+
});
49+
}
50+
element.addEventListener("mousedown", (evt) => {
51+
if (evt.button === 2) {
52+
evt.stopPropagation();
53+
unsetHandler();
54+
}
4255
});
4356
this.registerDisposer(model.changed.add(() => this.updateView()));
4457
this.updateView();

0 commit comments

Comments
 (0)