Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(segmentation_user_layer): add individual segment color picker tool #636

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/layer/segmentation/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,19 @@
+ .neuroglancer-tool-button {
margin-left: 1em;
}

.neuroglancer-segment-list-entry .neuroglancer-color-widget {
border: none;
border-color: transparent;
appearance: none;
background-color: transparent;
padding: 0;
margin: 0;
margin-left: 3px;
height: 19px;
width: 20px;
}

.neuroglancer-segment-list-entry .neuroglancer-color-widget.overridden {
background-color: white;
}
96 changes: 83 additions & 13 deletions src/segmentation_display_state/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,28 @@ import { observeWatchable, registerNestedSync } from "#src/trackable_value.js";
import { isWithinSelectionPanel } from "#src/ui/selection_details.js";
import type { Uint64Map } from "#src/uint64_map.js";
import { setClipboard } from "#src/util/clipboard.js";
import { useWhiteBackground } from "#src/util/color.js";
import {
packColor,
serializeColor,
TrackableRGB,
useWhiteBackground,
} from "#src/util/color.js";
import { RefCounted } from "#src/util/disposable.js";
import { measureElementClone } from "#src/util/dom.js";
import type { vec3 } from "#src/util/geom.js";
import { kOneVec, vec4 } from "#src/util/geom.js";
import { kOneVec, vec3, vec4 } from "#src/util/geom.js";
import { NullarySignal } from "#src/util/signal.js";
import { Uint64 } from "#src/util/uint64.js";
import { withSharedVisibility } from "#src/visibility_priority/frontend.js";
import { ColorWidget } from "#src/widget/color.js";
import { makeCopyButton } from "#src/widget/copy_button.js";
import { makeEyeButton } from "#src/widget/eye_button.js";
import { makeFilterButton } from "#src/widget/filter_button.js";
import { makeStarButton } from "#src/widget/star_button.js";

declare const SEGMENT_LIST_COLOR_WIDGET: boolean | undefined;
const SEGMENT_LIST_COLOR_WIDGET_ENABLED =
typeof SEGMENT_LIST_COLOR_WIDGET !== "undefined" &&
SEGMENT_LIST_COLOR_WIDGET === true;
export class Uint64MapEntry {
constructor(
public key: Uint64,
Expand Down Expand Up @@ -347,6 +356,11 @@ const segmentWidgetTemplate = (() => {
filterElement.classList.add("neuroglancer-segment-list-entry-filter");
const filterIndex = template.childElementCount;
template.appendChild(filterElement);
let colorWidgetIndex = -1;
if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
colorWidgetIndex = template.childElementCount;
template.appendChild(ColorWidget.template());
}
return {
template,
copyContainerIndex,
Expand All @@ -357,6 +371,7 @@ const segmentWidgetTemplate = (() => {
labelIndex,
filterIndex,
starIndex,
colorWidgetIndex,
unmappedIdIndex: -1,
unmappedCopyIndex: -1,
};
Expand Down Expand Up @@ -426,7 +441,7 @@ function makeRegisterSegmentWidgetEventHandlers(
const onMouseEnter = (event: Event) => {
const entryElement = event.currentTarget as HTMLElement;
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
displayState.segmentSelectionState.set(id);
if (!isWithinSelectionPanel(entryElement)) {
Expand All @@ -437,7 +452,7 @@ function makeRegisterSegmentWidgetEventHandlers(
const selectHandler = (event: Event) => {
const entryElement = event.currentTarget as HTMLElement;
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
displayState.selectSegment(
id,
Expand Down Expand Up @@ -470,7 +485,7 @@ function makeRegisterSegmentWidgetEventHandlers(
const visibleCheckboxHandler = (event: Event) => {
const entryElement = getEntryElement(event);
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
const { selectedSegments, visibleSegments } =
displayState.segmentationGroupState.value;
Expand All @@ -486,7 +501,7 @@ function makeRegisterSegmentWidgetEventHandlers(
const filterHandler = (event: Event) => {
const entryElement = getEntryElement(event);
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
displayState.filterBySegmentLabel(id);
event.stopPropagation();
Expand All @@ -504,7 +519,7 @@ function makeRegisterSegmentWidgetEventHandlers(
}
const entryElement = event.currentTarget as HTMLElement;
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
displayState.moveToSegment(id);
};
Expand Down Expand Up @@ -539,11 +554,35 @@ function makeRegisterSegmentWidgetEventHandlers(
starButton.addEventListener("click", (event: MouseEvent) => {
const entryElement = getEntryElement(event);
const idString = entryElement.dataset.id!;
const id = tempStatedColor;
const id = tempObjectId;
id.tryParseString(idString);
const { selectedSegments } = displayState.segmentationGroupState.value;
selectedSegments.set(id, !selectedSegments.has(id));
});

if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
const trackableRGB = new TrackableRGB(vec3.fromValues(0, 0, 0));
trackableRGB.changed.add(() => {
const testU = new Uint64(packColor(trackableRGB.value));
const idString = element.dataset.id!;
const id = tempObjectId;
id.tryParseString(idString);
displayState.segmentStatedColors.value.delete(id);
displayState.segmentStatedColors.value.set(id, testU);
});
new ColorWidget(
trackableRGB,
undefined,
children[template.colorWidgetIndex] as HTMLInputElement,
() => {
const idString = element.dataset.id!;
const id = tempObjectId;
id.tryParseString(idString);
displayState.segmentStatedColors.value.delete(id);
},
false,
);
}
};
}

Expand Down Expand Up @@ -641,7 +680,7 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
}

update(container: HTMLElement) {
const id = tempStatedColor;
const id = tempObjectId;
const idString = container.dataset.id;
if (idString === undefined) return;
id.parseString(idString);
Expand Down Expand Up @@ -670,19 +709,28 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
const idContainer = stickyChildren[
template.idContainerIndex
] as HTMLElement;
let color = getBaseObjectColor(this.displayState, mapped) as vec3;
setSegmentIdElementStyle(
idContainer.children[template.idIndex] as HTMLElement,
getBaseObjectColor(this.displayState, mapped) as vec3,
color,
);
if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
const isOverridden =
!!this.displayState?.segmentStatedColors.value.has(mapped);
setColorWidgetColor(
children[template.colorWidgetIndex] as HTMLInputElement,
color,
isOverridden,
);
}
const { unmappedIdIndex } = template;
if (unmappedIdIndex !== -1) {
let unmappedIdString: string | undefined;
let color: vec3;
if (
displayState!.baseSegmentColoring.value &&
(unmappedIdString = container.dataset.unmappedId) !== undefined
) {
const unmappedId = tempStatedColor;
const unmappedId = tempObjectId;
unmappedId.parseString(unmappedIdString);
color = getBaseObjectColor(this.displayState, unmappedId) as vec3;
} else {
Expand All @@ -692,6 +740,15 @@ export class SegmentWidgetFactory<Template extends SegmentWidgetTemplate> {
idContainer.children[unmappedIdIndex] as HTMLElement,
color,
);
if (SEGMENT_LIST_COLOR_WIDGET_ENABLED) {
const isOverridden =
!!this.displayState?.segmentStatedColors.value.has(mapped);
setColorWidgetColor(
children[template.colorWidgetIndex] as HTMLInputElement,
color,
isOverridden,
);
}
}
}
}
Expand All @@ -701,6 +758,15 @@ function setSegmentIdElementStyle(element: HTMLElement, color: vec3) {
element.style.color = useWhiteBackground(color) ? "white" : "black";
}

function setColorWidgetColor(
element: HTMLInputElement,
color: vec3,
isOverridden: boolean,
) {
element.value = serializeColor(color.subarray(0, 3) as vec3);
element.classList.toggle("overridden", isOverridden);
}

export class SegmentWidgetWithExtraColumnsFactory extends SegmentWidgetFactory<SegmentWidgetWithExtraColumnsTemplate> {
segmentPropertyMap: PreprocessedSegmentPropertyMap | undefined;
numericalProperties: InlineSegmentNumericalProperty[];
Expand Down Expand Up @@ -885,6 +951,9 @@ export function registerCallbackWhenSegmentationDisplayStateChanged(
displayState.baseSegmentColoring.changed.add(callback),
);
context.registerDisposer(displayState.hoverHighlight.changed.add(callback));
context.registerDisposer(
displayState.segmentStatedColors.changed.add(callback),
);
}

export function registerRedrawWhenSegmentationDisplayStateChanged(
Expand Down Expand Up @@ -941,6 +1010,7 @@ export function registerRedrawWhenSegmentationDisplayState3DChanged(
* Temporary values used by getObjectColor.
*/
const tempColor = vec4.create();
const tempObjectId = new Uint64();
const tempStatedColor = new Uint64();

export function getBaseObjectColor(
Expand Down
29 changes: 21 additions & 8 deletions src/widget/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,35 @@ import { vec3 } from "#src/util/geom.js";
export class ColorWidget<
Color extends vec3 | undefined = vec3,
> extends RefCounted {
element = document.createElement("input");
static template() {
const element = document.createElement("input");
element.classList.add("neuroglancer-color-widget");
element.type = "color";
return element;
}

constructor(
public model: WatchableValueInterface<Color>,
public getDefaultColor: () => vec3 = () => vec3.fromValues(1, 0, 0),
public element = ColorWidget.template(),
public unsetHandler = () => {},
enableWheel = true,
) {
super();
const { element } = this;
element.classList.add("neuroglancer-color-widget");
element.type = "color";
element.addEventListener("change", () => this.updateModel());
element.addEventListener("input", () => this.updateModel());
element.addEventListener("wheel", (event) => {
event.stopPropagation();
event.preventDefault();
this.adjustHueViaWheel(event);
if (enableWheel) {
element.addEventListener("wheel", (event) => {
event.stopPropagation();
event.preventDefault();
this.adjustHueViaWheel(event);
});
}
element.addEventListener("mousedown", (evt) => {
if (evt.button === 2) {
evt.stopPropagation();
unsetHandler();
}
});
this.registerDisposer(model.changed.add(() => this.updateView()));
this.updateView();
Expand Down
Loading