Skip to content

Commit 959c56b

Browse files
committed
feat(segment_list): add individual segment color picker tool
show individual color picker with alt+click on segment id, alt+shift+click to unset visible widget enabled with SEGMENT_LIST_COLOR_WIDGET option
1 parent 249f866 commit 959c56b

File tree

3 files changed

+165
-22
lines changed

3 files changed

+165
-22
lines changed

src/layer/segmentation/style.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@
7979
width: var(--neuroglancer-segment-list-width);
8080
}
8181

82+
.neuroglancer-segment-list-entry-id.stated-color {
83+
border: 1px solid white;
84+
}
85+
8286
.neuroglancer-segment-list .neuroglancer-segment-list-entry-sticky {
8387
position: sticky;
8488
left: 0;
@@ -136,3 +140,30 @@
136140
+ .neuroglancer-tool-button {
137141
margin-left: 1em;
138142
}
143+
144+
.neuroglancer-segment-list-entry .neuroglancer-segment-list-entry-color-input {
145+
border: none;
146+
appearance: none;
147+
background-color: transparent;
148+
padding: 2px;
149+
margin: 0;
150+
margin-left: 3px;
151+
height: 18px;
152+
width: 20px;
153+
}
154+
155+
.neuroglancer-segment-list-entry
156+
.neuroglancer-segment-list-entry-color-input.hidden {
157+
position: absolute;
158+
visibility: hidden;
159+
}
160+
161+
.neuroglancer-segment-list-entry
162+
.neuroglancer-segment-list-entry-color-input.stated-color {
163+
background-color: white;
164+
}
165+
166+
.neuroglancer-segment-list-entry
167+
.neuroglancer-segment-list-entry-color-input::-webkit-color-swatch-wrapper {
168+
padding: 0;
169+
}

src/segmentation_display_state/frontend.ts

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ import { isWithinSelectionPanel } from "#src/ui/selection_details.js";
4848
import type { Uint64Map } from "#src/uint64_map.js";
4949
import { wrapSigned32BitIntegerToUint64 } from "#src/util/bigint.js";
5050
import { setClipboard } from "#src/util/clipboard.js";
51-
import { useWhiteBackground } from "#src/util/color.js";
51+
import {
52+
packColor,
53+
parseRGBColorSpecification,
54+
serializeColor,
55+
useWhiteBackground,
56+
} from "#src/util/color.js";
5257
import { RefCounted } from "#src/util/disposable.js";
5358
import { measureElementClone } from "#src/util/dom.js";
5459
import type { vec3 } from "#src/util/geom.js";
@@ -61,6 +66,11 @@ import { makeEyeButton } from "#src/widget/eye_button.js";
6166
import { makeFilterButton } from "#src/widget/filter_button.js";
6267
import { makeStarButton } from "#src/widget/star_button.js";
6368

69+
declare const NEUROGLANCER_SEGMENT_LIST_COLOR_WIDGET: boolean | undefined;
70+
const SEGMENT_LIST_COLOR_WIDGET_ENABLED =
71+
typeof NEUROGLANCER_SEGMENT_LIST_COLOR_WIDGET !== "undefined" &&
72+
NEUROGLANCER_SEGMENT_LIST_COLOR_WIDGET === true;
73+
6474
export class Uint64MapEntry {
6575
constructor(
6676
public key: bigint,
@@ -293,6 +303,8 @@ export function bindSegmentListWidth(
293303
const segmentWidgetTemplate = (() => {
294304
const template = document.createElement("div");
295305
template.classList.add("neuroglancer-segment-list-entry");
306+
template.title =
307+
"Right click to move to segment, alt+click to set color, alt+shift+click to unset color";
296308
const stickyContainer = document.createElement("div");
297309
stickyContainer.classList.add("neuroglancer-segment-list-entry-sticky");
298310
template.appendChild(stickyContainer);
@@ -337,6 +349,18 @@ const segmentWidgetTemplate = (() => {
337349
filterElement.classList.add("neuroglancer-segment-list-entry-filter");
338350
const filterIndex = template.childElementCount;
339351
template.appendChild(filterElement);
352+
const colorInputIndex = template.childElementCount;
353+
const colorInputElement = document.createElement("input");
354+
colorInputElement.type = "color";
355+
colorInputElement.classList.add(
356+
"neuroglancer-segment-list-entry-color-input",
357+
);
358+
colorInputElement.classList.toggle(
359+
"hidden",
360+
!SEGMENT_LIST_COLOR_WIDGET_ENABLED,
361+
);
362+
colorInputElement.title = "Set segment color (right click to unset)";
363+
template.appendChild(colorInputElement);
340364
return {
341365
template,
342366
copyContainerIndex,
@@ -347,6 +371,7 @@ const segmentWidgetTemplate = (() => {
347371
labelIndex,
348372
filterIndex,
349373
starIndex,
374+
colorInputIndex,
350375
unmappedIdIndex: -1,
351376
unmappedCopyIndex: -1,
352377
};
@@ -478,26 +503,65 @@ function makeRegisterSegmentWidgetEventHandlers(
478503
event.stopPropagation();
479504
};
480505

481-
const onMousedown = (event: MouseEvent) => {
506+
const onMousedown = (event: MouseEvent, template: SegmentWidgetTemplate) => {
507+
const entryElement = event.currentTarget as HTMLElement;
508+
const idString = entryElement.dataset.id!;
509+
const id = BigInt(idString);
510+
if (event.button === 0 && event.altKey) {
511+
if (event.shiftKey) {
512+
displayState.segmentStatedColors.value.delete(id);
513+
} else {
514+
const colorInputElement = entryElement.children[
515+
template.colorInputIndex
516+
] as HTMLInputElement;
517+
const color = getBaseObjectColor(displayState, id) as vec3;
518+
setColorInputColor(
519+
colorInputElement,
520+
color,
521+
displayState.segmentStatedColors.value.has(id),
522+
);
523+
colorInputElement.showPicker();
524+
}
525+
}
482526
if (
483-
event.button !== 2 ||
484-
event.ctrlKey ||
485-
event.altKey ||
486-
event.metaKey ||
487-
event.shiftKey
527+
event.button === 2 &&
528+
!event.ctrlKey &&
529+
!event.altKey &&
530+
!event.metaKey &&
531+
!event.shiftKey
488532
) {
489-
return;
533+
displayState.moveToSegment(id);
490534
}
491-
const entryElement = event.currentTarget as HTMLElement;
535+
};
536+
537+
const colorHandler = (event: Event) => {
538+
const colorInputElement = event.currentTarget as HTMLInputElement;
539+
const entryElement = getEntryElement(event);
492540
const idString = entryElement.dataset.id!;
493541
const id = BigInt(idString);
494-
displayState.moveToSegment(id);
542+
const color = BigInt(
543+
packColor(parseRGBColorSpecification(colorInputElement.value)),
544+
);
545+
displayState.segmentStatedColors.value.delete(id);
546+
displayState.segmentStatedColors.value.set(id, color);
547+
};
548+
549+
const onColorInputMousedown = (event: MouseEvent) => {
550+
if (event.button === 2) {
551+
event.stopPropagation();
552+
const entryElement = getEntryElement(event);
553+
const idString = entryElement.dataset.id!;
554+
const id = BigInt(idString);
555+
displayState.segmentStatedColors.value.delete(id);
556+
}
495557
};
496558

497559
return (element: HTMLElement, template: SegmentWidgetTemplate) => {
498560
const { children } = element;
499561
const stickyChildren = children[0].children;
500-
element.addEventListener("mousedown", onMousedown);
562+
element.addEventListener("mousedown", (event: MouseEvent) =>
563+
onMousedown(event, template),
564+
);
501565
const copyContainer = stickyChildren[
502566
template.copyContainerIndex
503567
] as HTMLElement;
@@ -528,6 +592,14 @@ function makeRegisterSegmentWidgetEventHandlers(
528592
const { selectedSegments } = displayState.segmentationGroupState.value;
529593
selectedSegments.set(id, !selectedSegments.has(id));
530594
});
595+
const colorInputElement = children[
596+
template.colorInputIndex
597+
] as HTMLInputElement;
598+
colorInputElement.addEventListener("input", colorHandler);
599+
colorInputElement.addEventListener("change", colorHandler);
600+
if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
601+
colorInputElement.addEventListener("mousedown", onColorInputMousedown);
602+
}
531603
};
532604
}
533605

@@ -653,14 +725,24 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
653725
const idContainer = stickyChildren[
654726
template.idContainerIndex
655727
] as HTMLElement;
728+
let color = getBaseObjectColor(this.displayState, mapped) as vec3;
656729
setSegmentIdElementStyle(
657730
idContainer.children[template.idIndex] as HTMLElement,
658-
getBaseObjectColor(this.displayState, mapped) as vec3,
731+
color,
732+
!SEGMENT_LIST_COLOR_WIDGET_ENABLED &&
733+
!!this.displayState?.segmentStatedColors.value.has(mapped),
659734
);
735+
children[template.colorInputIndex] as HTMLInputElement;
736+
if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
737+
setColorInputColor(
738+
children[template.colorInputIndex] as HTMLInputElement,
739+
color,
740+
!!this.displayState?.segmentStatedColors.value.has(mapped),
741+
);
742+
}
660743
const { unmappedIdIndex } = template;
661744
if (unmappedIdIndex !== -1) {
662745
let unmappedIdString: string | undefined;
663-
let color: vec3;
664746
if (
665747
displayState!.baseSegmentColoring.value &&
666748
(unmappedIdString = container.dataset.unmappedId) !== undefined
@@ -678,9 +760,23 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
678760
}
679761
}
680762

681-
function setSegmentIdElementStyle(element: HTMLElement, color: vec3) {
763+
function setSegmentIdElementStyle(
764+
element: HTMLElement,
765+
color: vec3,
766+
stated = false,
767+
) {
682768
element.style.backgroundColor = getCssColor(color);
683769
element.style.color = useWhiteBackground(color) ? "white" : "black";
770+
element.classList.toggle("stated-color", stated);
771+
}
772+
773+
function setColorInputColor(
774+
element: HTMLInputElement,
775+
color: vec3,
776+
stated: boolean,
777+
) {
778+
element.value = serializeColor(color.subarray(0, 3) as vec3);
779+
element.classList.toggle("stated-color", stated);
684780
}
685781

686782
export class SegmentWidgetWithExtraColumnsFactory extends SegmentWidgetFactory<SegmentWidgetWithExtraColumnsTemplate> {
@@ -867,6 +963,9 @@ export function registerCallbackWhenSegmentationDisplayStateChanged(
867963
displayState.baseSegmentColoring.changed.add(callback),
868964
);
869965
context.registerDisposer(displayState.hoverHighlight.changed.add(callback));
966+
context.registerDisposer(
967+
displayState.segmentStatedColors.changed.add(callback),
968+
);
870969
}
871970

872971
export function registerRedrawWhenSegmentationDisplayStateChanged(

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)