Skip to content
Merged
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
6 changes: 4 additions & 2 deletions src/layer/segmentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ export class SegmentationUserLayer extends Base {
readonly selectedSpatialSkeletonNodeInfo = new WatchableValue<
SelectedSpatialSkeletonNodeInfo | undefined
>(undefined);
readonly hoveredSpatialSkeletonNodeId = this.registerDisposer(
readonly hoveredSpatialSkeletonNodeInfo = this.registerDisposer(
new SpatialSkeletonHoverState(),
);
readonly spatialSkeletonVisibleChunksNeeded = new WatchableValue(0);
Expand Down Expand Up @@ -1118,7 +1118,7 @@ export class SegmentationUserLayer extends Base {
),
);
syncSelectedSpatialSkeletonNodeIdFromGlobalSelection();
this.hoveredSpatialSkeletonNodeId.bindTo(
this.hoveredSpatialSkeletonNodeInfo.bindTo(
this.manager.layerSelectedValues,
this,
);
Expand Down Expand Up @@ -1603,6 +1603,7 @@ export class SegmentationUserLayer extends Base {
{
sources2d: slicePanelSources,
selectedNodeInfo: this.selectedSpatialSkeletonNodeInfo,
hoveredNodeInfo: this.hoveredSpatialSkeletonNodeInfo,
pendingNodePositionVersion:
this.spatialSkeletonState.pendingNodePositionVersion,
getPendingNodePosition: (nodeId) =>
Expand Down Expand Up @@ -1636,6 +1637,7 @@ export class SegmentationUserLayer extends Base {
displayState,
{
selectedNodeInfo: this.selectedSpatialSkeletonNodeInfo,
hoveredNodeInfo: this.hoveredSpatialSkeletonNodeInfo,
pendingNodePositionVersion:
this.spatialSkeletonState.pendingNodePositionVersion,
getPendingNodePosition: (nodeId) =>
Expand Down
12 changes: 10 additions & 2 deletions src/layer/segmentation/selection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe("layer/segmentation/selection", () => {
let mouseState: {
active: boolean;
pickedRenderLayer: unknown;
pickedSpatialSkeleton?: { nodeId?: unknown };
pickedSpatialSkeleton?: { nodeId?: unknown; segmentId?: unknown };
} = {
active: false,
pickedRenderLayer: null,
Expand Down Expand Up @@ -175,7 +175,15 @@ describe("layer/segmentation/selection", () => {
pickedSpatialSkeleton: { nodeId: 31 },
};
trigger();
expect(hoverState.value).toBe(31);
expect(hoverState.value).toEqual({ nodeId: 31 });

mouseState = {
active: true,
pickedRenderLayer: renderLayerA,
pickedSpatialSkeleton: { nodeId: 31, segmentId: 7 },
};
trigger();
expect(hoverState.value).toEqual({ nodeId: 31, segmentId: 7 });

mouseState = {
active: true,
Expand Down
53 changes: 31 additions & 22 deletions src/layer/segmentation/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/

import type { LayerSelectedValues } from "#src/layer/index.js";
import type {
LayerSelectedValues,
PickedSpatialSkeletonState,
} from "#src/layer/index.js";
import type { SegmentationUserLayer } from "#src/layer/segmentation/index.js";
import { RefCounted } from "#src/util/disposable.js";
import { parseUint64 } from "#src/util/json.js";
Expand All @@ -23,11 +26,12 @@ import { NullarySignal } from "#src/util/signal.js";
interface SpatialSkeletonViewerHoverMouseStateLike<TRenderLayer> {
active: boolean;
pickedRenderLayer: TRenderLayer | null | undefined;
pickedSpatialSkeleton?:
| {
nodeId?: unknown;
}
| undefined;
pickedSpatialSkeleton?: PickedSpatialSkeletonState;
}

export interface SpatialSkeletonHoverInfo {
readonly nodeId: number;
readonly segmentId?: number;
}

interface SpatialSkeletonViewerHoverLayerLike<TRenderLayer> {
Expand Down Expand Up @@ -88,12 +92,6 @@ function getSelectionValueIdString(value: unknown) {
}
}

function normalizeSpatialSkeletonViewerHoverNodeId(value: unknown) {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
? value
: undefined;
}

export function getNodeIdFromLayerSelectionState(
state: { nodeId?: unknown; value?: unknown } | undefined,
) {
Expand Down Expand Up @@ -175,10 +173,10 @@ export function getNodeIdFromViewerSelection<TLayer>(
);
}

function getSpatialSkeletonNodeIdFromViewerHover<TRenderLayer>(
function getSpatialSkeletonHoverInfoFromViewerHover<TRenderLayer>(
mouseState: SpatialSkeletonViewerHoverMouseStateLike<TRenderLayer>,
layer: SpatialSkeletonViewerHoverLayerLike<TRenderLayer>,
) {
): SpatialSkeletonHoverInfo | undefined {
if (!mouseState.active) return undefined;
const pickedRenderLayer = mouseState.pickedRenderLayer;
if (pickedRenderLayer !== null) {
Expand All @@ -189,18 +187,29 @@ function getSpatialSkeletonNodeIdFromViewerHover<TRenderLayer>(
return undefined;
}
}
// TODO (SKM): I think we can inline this function
return normalizeSpatialSkeletonViewerHoverNodeId(
mouseState.pickedSpatialSkeleton?.nodeId,
);
const pickedSpatialSkeleton = mouseState.pickedSpatialSkeleton;
const nodeId = pickedSpatialSkeleton?.nodeId;
if (nodeId === undefined) return undefined;
const segmentId = pickedSpatialSkeleton?.segmentId;
if (segmentId === undefined) return undefined;
return segmentId === undefined ? { nodeId } : { nodeId, segmentId };
}

function spatialSkeletonHoverInfoEqual(
a: SpatialSkeletonHoverInfo | undefined,
b: SpatialSkeletonHoverInfo | undefined,
) {
if (a === b) return true;
if (a === undefined || b === undefined) return false;
return a.nodeId === b.nodeId && a.segmentId === b.segmentId;
}

export class SpatialSkeletonHoverState extends RefCounted {
value: number | undefined = undefined;
value: SpatialSkeletonHoverInfo | undefined = undefined;
readonly changed = new NullarySignal();

setValue(value: number | undefined) {
if (this.value !== value) {
setValue(value: SpatialSkeletonHoverInfo | undefined) {
if (!spatialSkeletonHoverInfoEqual(this.value, value)) {
this.value = value;
this.changed.dispatch();
}
Expand All @@ -213,7 +222,7 @@ export class SpatialSkeletonHoverState extends RefCounted {
this.registerDisposer(
layerSelectedValues.changed.add(() => {
this.setValue(
getSpatialSkeletonNodeIdFromViewerHover(
getSpatialSkeletonHoverInfoFromViewerHover(
layerSelectedValues.mouseState,
layer,
),
Expand Down
124 changes: 106 additions & 18 deletions src/skeleton/frontend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,22 +134,23 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => {
const layer = Object.assign(
Object.create(SpatiallyIndexedSkeletonLayer.prototype),
{
selectedNodeId: { value: 101 },
selectedNodeInfo: { value: { nodeId: 101 } },
selectedNodeOutlineColor: vec3.create(),
selectedNodeOutlineColorGeneration: 0,
cachedSelectedNodeOutlineColorGeneration: -1,
highlightedNodeOutlineColor: vec3.create(),
nodeOutlineColorGeneration: 0,
cachedNodeOutlineColorGeneration: -1,
displayState,
},
);

const outlineColor = (layer as any).getSelectedNodeOutlineColor();
const cachedOutlineColor = (layer as any).getSelectedNodeOutlineColor();
(layer as any).updateNodeOutlineColorPair();
const outlineColor = (layer as any).selectedNodeOutlineColor;
(layer as any).updateNodeOutlineColorPair();
const cachedOutlineColor = (layer as any).selectedNodeOutlineColor;

expect(isSelected).not.toHaveBeenCalled();
expect(cachedOutlineColor).toBe(outlineColor);
expect(outlineColor[0]).toBeCloseTo(1);
expect(outlineColor[1]).toBeCloseTo(0.95);
expect(outlineColor[2]).toBeCloseTo(0.35);
// The outline color is chosen for high contrast against the segment color.
expect(getContrastRatio(outlineColor, sourceColor)).toBeGreaterThanOrEqual(
3,
);
Expand Down Expand Up @@ -183,16 +184,17 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => {
{
selectedNodeInfo: { value: { nodeId: 101 } },
selectedNodeOutlineColor: vec3.create(),
selectedNodeOutlineColorGeneration: 0,
cachedSelectedNodeOutlineColorGeneration: -1,
highlightedNodeOutlineColor: vec3.create(),
nodeOutlineColorGeneration: 0,
cachedNodeOutlineColorGeneration: -1,
displayState,
},
);

(layer as any).getSelectedNodeOutlineColor();
(layer as any).updateNodeOutlineColorPair();
selectedNodeId.value = 202;
++(layer as any).selectedNodeOutlineColorGeneration;
(layer as any).getSelectedNodeOutlineColor();
++(layer as any).nodeOutlineColorGeneration;
(layer as any).updateNodeOutlineColorPair();

expect(computeSegmentColor).toHaveBeenCalledTimes(2);
});
Expand Down Expand Up @@ -224,18 +226,104 @@ describe("SpatiallyIndexedSkeletonLayer selected node outline color", () => {
{
selectedNodeInfo: { value: { nodeId: 101 } },
selectedNodeOutlineColor: vec3.create(),
selectedNodeOutlineColorGeneration: 0,
cachedSelectedNodeOutlineColorGeneration: -1,
highlightedNodeOutlineColor: vec3.create(),
nodeOutlineColorGeneration: 0,
cachedNodeOutlineColorGeneration: -1,
displayState,
},
);

(layer as any).getSelectedNodeOutlineColor();
++(layer as any).selectedNodeOutlineColorGeneration;
(layer as any).getSelectedNodeOutlineColor();
(layer as any).updateNodeOutlineColorPair();
++(layer as any).nodeOutlineColorGeneration;
(layer as any).updateNodeOutlineColorPair();

expect(computeSegmentColor).toHaveBeenCalledTimes(2);
});

it("derives the hovered-node outline color from the hovered segment when nothing is selected", () => {
const sourceColor = vec3.fromValues(1, 1, 1);
const displayState = {
segmentationColorGroupState: {
value: {
segmentStatedColors: new Map(),
segmentDefaultColor: { value: sourceColor },
segmentColorHash: { compute: vi.fn() },
},
},
saturation: { value: 0 },
hoverHighlight: { value: true },
segmentSelectionState: { isSelected: vi.fn(() => false), baseValue: 0n },
};
const layer = Object.assign(
Object.create(SpatiallyIndexedSkeletonLayer.prototype),
{
selectedNodeInfo: { value: undefined },
hoveredNodeInfo: { value: { nodeId: 303, segmentId: 202 } },
selectedNodeOutlineColor: vec3.create(),
highlightedNodeOutlineColor: vec3.create(),
nodeOutlineColorGeneration: 0,
cachedNodeOutlineColorGeneration: -1,
displayState,
},
);

(layer as any).updateNodeOutlineColorPair();
const highlightedColor = (layer as any).highlightedNodeOutlineColor;

// The hovered outline is chosen for high contrast against its own (white)
// segment color.
expect(
getContrastRatio(highlightedColor, sourceColor),
).toBeGreaterThanOrEqual(3);
});

it("derives each outline from its own segment when selected and hovered nodes belong to different segments", () => {
// Selected node on a dark segment, hovered node on a bright segment, as
// happens when hovering a merge target on a differently colored skeleton.
const selectedSegmentColor = vec3.fromValues(0, 0, 0);
const hoveredSegmentColor = vec3.fromValues(1, 1, 1);
const displayState = {
segmentationColorGroupState: {
value: {
segmentStatedColors: new Map([
[101n, 0x000000n],
[202n, 0xffffffn],
]),
segmentDefaultColor: { value: undefined },
segmentColorHash: { compute: vi.fn() },
},
},
saturation: { value: 0 },
hoverHighlight: { value: true },
segmentSelectionState: { isSelected: vi.fn(() => false), baseValue: 0n },
};
const layer = Object.assign(
Object.create(SpatiallyIndexedSkeletonLayer.prototype),
{
selectedNodeInfo: { value: { nodeId: 101, segmentId: 101 } },
hoveredNodeInfo: { value: { nodeId: 303, segmentId: 202 } },
selectedNodeOutlineColor: vec3.create(),
highlightedNodeOutlineColor: vec3.create(),
nodeOutlineColorGeneration: 0,
cachedNodeOutlineColorGeneration: -1,
displayState,
},
);

(layer as any).updateNodeOutlineColorPair();
const selectedColor = (layer as any).selectedNodeOutlineColor;
const highlightedColor = (layer as any).highlightedNodeOutlineColor;

// Each outline contrasts against its own segment color...
expect(
getContrastRatio(selectedColor, selectedSegmentColor),
).toBeGreaterThanOrEqual(3);
expect(
getContrastRatio(highlightedColor, hoveredSegmentColor),
).toBeGreaterThanOrEqual(3);
// ...and the two outlines are different colors.
expect([...selectedColor]).not.toEqual([...highlightedColor]);
});
});

describe("SpatiallyIndexedSkeletonLayer targeted source invalidation", () => {
Expand Down
Loading