diff --git a/src/datasource/catmaid/spatial_skeleton_commands.ts b/src/datasource/catmaid/spatial_skeleton_commands.ts index 37f367c41..780bda6ce 100644 --- a/src/datasource/catmaid/spatial_skeleton_commands.ts +++ b/src/datasource/catmaid/spatial_skeleton_commands.ts @@ -921,7 +921,9 @@ async function commitAndApplyDeleteNode( resolvedNode.node.segmentId, ) ?? []; if (remainingNodes.length > 0) { - resolvedNode.skeletonLayer.retainOverlaySegment(resolvedNode.node.segmentId); + resolvedNode.skeletonLayer.retainOverlaySegment( + resolvedNode.node.segmentId, + ); } else { resolvedNode.skeletonLayer.markSegmentEdited(resolvedNode.node.segmentId); } @@ -2132,7 +2134,6 @@ class MergeCommand implements SpatialSkeletonCommand { if (deletedSkeletonId !== resultSkeletonId) { firstNode.skeletonLayer.markSegmentEdited(deletedSkeletonId); } - this.layer.clearSpatialSkeletonMergeAnchor(); await refreshTopologySegments( this.layer, [resultSkeletonId, deletedSkeletonId], diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 63ed4c8c2..f135e34d0 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -51,15 +51,6 @@ import { getSegmentIdFromLayerSelectionValue, SpatialSkeletonHoverState, } from "#src/layer/segmentation/selection.js"; -import { - executeSpatialSkeletonDeleteNode, - executeSpatialSkeletonNodeConfidenceUpdate, - executeSpatialSkeletonNodeDescriptionUpdate, - executeSpatialSkeletonNodeRadiusUpdate, - executeSpatialSkeletonReroot, - executeSpatialSkeletonNodeTrueEndUpdate, - showSpatialSkeletonActionError, -} from "#src/layer/segmentation/spatial_skeleton_commands.js"; import { MeshLayer, MeshSource, @@ -138,6 +129,15 @@ import { SpatialSkeletonDisplayNodeType, SpatialSkeletonNodeFilterType, } from "#src/skeleton/node_types.js"; +import { + executeSpatialSkeletonDeleteNode, + executeSpatialSkeletonNodeConfidenceUpdate, + executeSpatialSkeletonNodeDescriptionUpdate, + executeSpatialSkeletonNodeRadiusUpdate, + executeSpatialSkeletonReroot, + executeSpatialSkeletonNodeTrueEndUpdate, + showSpatialSkeletonActionError, +} from "#src/skeleton/spatial_skeleton_commands.js"; import { editableSpatiallyIndexedSkeletonSourceSupportsAction, getEditableSpatiallyIndexedSkeletonSource, @@ -2449,7 +2449,8 @@ export class SegmentationUserLayer extends Base { let committedTrueEnd = nodeHasTrueEnd; let leafTypeSavePending = false; const leafTypeEditor = document.createElement("div"); - leafTypeEditor.className = "neuroglancer-selection-details-skeleton-leaf-type"; + leafTypeEditor.className = + "neuroglancer-selection-details-skeleton-leaf-type"; const leafTypeRadioName = `neuroglancer-selection-details-skeleton-leaf-type-${segmentId}-${fullNodeInfo.nodeId}`; const leafTypeOptionElements: HTMLLabelElement[] = []; const makeLeafTypeOption = (options: { @@ -2458,7 +2459,8 @@ export class SegmentationUserLayer extends Base { trueEnd: boolean; }) => { const option = document.createElement("label"); - option.className = "neuroglancer-selection-details-skeleton-leaf-type-option"; + option.className = + "neuroglancer-selection-details-skeleton-leaf-type-option"; const input = document.createElement("input"); input.type = "radio"; input.name = leafTypeRadioName; @@ -2466,7 +2468,8 @@ export class SegmentationUserLayer extends Base { input.className = "neuroglancer-selection-details-skeleton-leaf-type-option-input"; const icon = document.createElement("span"); - icon.className = "neuroglancer-selection-details-skeleton-leaf-type-option-icon"; + icon.className = + "neuroglancer-selection-details-skeleton-leaf-type-option-icon"; icon.appendChild( makeIcon({ svg: options.svg, @@ -2475,7 +2478,8 @@ export class SegmentationUserLayer extends Base { }), ); const text = document.createElement("span"); - text.className = "neuroglancer-selection-details-skeleton-leaf-type-option-text"; + text.className = + "neuroglancer-selection-details-skeleton-leaf-type-option-text"; text.textContent = options.label; option.appendChild(input); option.appendChild(icon); @@ -2639,7 +2643,8 @@ export class SegmentationUserLayer extends Base { } else { let committedRadius = fullNodeInfo.radius ?? 0; const radiusInput = document.createElement("input"); - radiusInput.className = "neuroglancer-selection-details-skeleton-properties-input"; + radiusInput.className = + "neuroglancer-selection-details-skeleton-properties-input"; radiusInput.type = "number"; radiusInput.step = "any"; radiusInput.value = formatSpatialSkeletonEditableNumber( diff --git a/src/layer/segmentation/style.css b/src/layer/segmentation/style.css index 2bec3029a..9b1be3148 100644 --- a/src/layer/segmentation/style.css +++ b/src/layer/segmentation/style.css @@ -312,7 +312,8 @@ justify-content: center; } -.neuroglancer-selection-details-skeleton-leaf-type-option-icon .neuroglancer-icon { +.neuroglancer-selection-details-skeleton-leaf-type-option-icon + .neuroglancer-icon { min-width: 14px; min-height: 14px; } diff --git a/src/skeleton/frontend.ts b/src/skeleton/frontend.ts index 136934500..85fea611f 100644 --- a/src/skeleton/frontend.ts +++ b/src/skeleton/frontend.ts @@ -1824,7 +1824,9 @@ interface SelectedSkeletonNodeInfo { interface SpatiallyIndexedSkeletonLayerOptions { sources2d?: SpatiallyIndexedSkeletonSourceEntry[]; - selectedNodeInfo?: WatchableValueInterface; + selectedNodeInfo?: WatchableValueInterface< + SelectedSkeletonNodeInfo | undefined + >; pendingNodePositionVersion?: WatchableValueInterface; getPendingNodePosition?: (nodeId: number) => ArrayLike | undefined; getCachedNode?: (nodeId: number) => SpatiallyIndexedSkeletonNode | undefined; diff --git a/src/layer/segmentation/spatial_skeleton_commands.spec.ts b/src/skeleton/spatial_skeleton_commands.spec.ts similarity index 99% rename from src/layer/segmentation/spatial_skeleton_commands.spec.ts rename to src/skeleton/spatial_skeleton_commands.spec.ts index 0c9f663e0..969911fe8 100644 --- a/src/layer/segmentation/spatial_skeleton_commands.spec.ts +++ b/src/skeleton/spatial_skeleton_commands.spec.ts @@ -19,6 +19,14 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { makeCatmaidNodeSourceState } from "#src/datasource/catmaid/api.js"; import { buildCatmaidNeighborhoodEditContext } from "#src/datasource/catmaid/edit_state.js"; import { CatmaidSpatialSkeletonEditCommands } from "#src/datasource/catmaid/spatial_skeleton_commands.js"; +import { SpatialSkeletonActions } from "#src/skeleton/actions.js"; +import type { SpatiallyIndexedSkeletonNode } from "#src/skeleton/api.js"; +import { SpatialSkeletonCommandHistory } from "#src/skeleton/command_history.js"; +import { + findSpatiallyIndexedSkeletonNode, + getSpatiallyIndexedSkeletonDirectChildren, + getSpatiallyIndexedSkeletonNodeParent, +} from "#src/skeleton/node_traversal.js"; import { executeSpatialSkeletonAddNode, executeSpatialSkeletonDeleteNode, @@ -33,15 +41,7 @@ import { executeSpatialSkeletonSplit, redoSpatialSkeletonCommand, undoSpatialSkeletonCommand, -} from "#src/layer/segmentation/spatial_skeleton_commands.js"; -import { SpatialSkeletonActions } from "#src/skeleton/actions.js"; -import type { SpatiallyIndexedSkeletonNode } from "#src/skeleton/api.js"; -import { SpatialSkeletonCommandHistory } from "#src/skeleton/command_history.js"; -import { - findSpatiallyIndexedSkeletonNode, - getSpatiallyIndexedSkeletonDirectChildren, - getSpatiallyIndexedSkeletonNodeParent, -} from "#src/skeleton/node_traversal.js"; +} from "#src/skeleton/spatial_skeleton_commands.js"; import { SpatialSkeletonState } from "#src/skeleton/spatial_skeleton_manager.js"; import { StatusMessage } from "#src/status.js"; diff --git a/src/layer/segmentation/spatial_skeleton_commands.ts b/src/skeleton/spatial_skeleton_commands.ts similarity index 89% rename from src/layer/segmentation/spatial_skeleton_commands.ts rename to src/skeleton/spatial_skeleton_commands.ts index e172d8e11..83731a5f0 100644 --- a/src/layer/segmentation/spatial_skeleton_commands.ts +++ b/src/skeleton/spatial_skeleton_commands.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import type { SegmentationUserLayer } from "#src/layer/segmentation/index.js"; import { SpatialSkeletonActions, type SpatialSkeletonAction, @@ -32,6 +31,7 @@ import { getSpatialSkeletonActionErrorMessage } from "#src/skeleton/edit_errors. import { getEditableSpatiallyIndexedSkeletonSource, getSpatialSkeletonEditCommandFactoryForAction, + type SpatialSkeletonLayerContext, } from "#src/skeleton/spatial_skeleton_manager.js"; import { StatusMessage } from "#src/status.js"; @@ -40,7 +40,7 @@ interface SpatialSkeletonSourceAccess { } function getEditSource( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, ): EditableSpatiallyIndexedSkeletonSource { const source = getEditableSpatiallyIndexedSkeletonSource( layer.getSpatiallyIndexedSkeletonLayer(), @@ -63,7 +63,7 @@ export function getSpatialSkeletonEditCommandFactory( } function executeCommand( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, command: SpatialSkeletonCommand, ) { return layer.spatialSkeletonState.commandHistory.execute(command); @@ -88,7 +88,7 @@ export function showSpatialSkeletonActionError(action: string, error: unknown) { } function createSpatialSkeletonCommand( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, action: SpatialSkeletonAction, payload: SpatialSkeletonCommandPayload, unsupportedMessage: string, @@ -101,7 +101,10 @@ function createSpatialSkeletonCommand( if (commandFactory === undefined) { throw new Error(unsupportedMessage); } - return commandFactory.createCommand(layer, payload); + // The concrete createCommand implementations expect the full layer type; the + // cast is safe because in practice layer always satisfies those requirements. + + return commandFactory.createCommand(layer as any, payload); } interface SpatialSkeletonExecutionMetadata { @@ -198,7 +201,7 @@ const spatialSkeletonExecutionMetadata = new Map< ]); function executeSpatialSkeletonAction( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, action: SpatialSkeletonAction, payload: SpatialSkeletonCommandPayload, ) { @@ -219,7 +222,7 @@ function executeSpatialSkeletonAction( } export function executeSpatialSkeletonAddNode( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -230,7 +233,7 @@ export function executeSpatialSkeletonAddNode( } export function executeSpatialSkeletonInsertNode( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -241,7 +244,7 @@ export function executeSpatialSkeletonInsertNode( } export function executeSpatialSkeletonMoveNode( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -252,7 +255,7 @@ export function executeSpatialSkeletonMoveNode( } export function executeSpatialSkeletonDeleteNode( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, node: SpatiallyIndexedSkeletonNode, ) { return executeSpatialSkeletonAction( @@ -263,7 +266,7 @@ export function executeSpatialSkeletonDeleteNode( } export function executeSpatialSkeletonNodeDescriptionUpdate( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -274,7 +277,7 @@ export function executeSpatialSkeletonNodeDescriptionUpdate( } export function executeSpatialSkeletonNodeTrueEndUpdate( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -285,7 +288,7 @@ export function executeSpatialSkeletonNodeTrueEndUpdate( } export function executeSpatialSkeletonNodeRadiusUpdate( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -296,7 +299,7 @@ export function executeSpatialSkeletonNodeRadiusUpdate( } export function executeSpatialSkeletonNodeConfidenceUpdate( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, options: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -307,7 +310,7 @@ export function executeSpatialSkeletonNodeConfidenceUpdate( } export function executeSpatialSkeletonReroot( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, node: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -318,7 +321,7 @@ export function executeSpatialSkeletonReroot( } export function executeSpatialSkeletonSplit( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, node: SpatialSkeletonCommandPayload, ) { return executeSpatialSkeletonAction( @@ -329,7 +332,7 @@ export function executeSpatialSkeletonSplit( } export function executeSpatialSkeletonMerge( - layer: SegmentationUserLayer, + layer: SpatialSkeletonLayerContext, firstNode: SpatialSkeletonCommandPayload, secondNode: SpatialSkeletonCommandPayload, ) { @@ -340,7 +343,9 @@ export function executeSpatialSkeletonMerge( ); } -export async function undoSpatialSkeletonCommand(layer: SegmentationUserLayer) { +export async function undoSpatialSkeletonCommand( + layer: SpatialSkeletonLayerContext, +) { const changed = await layer.spatialSkeletonState.commandHistory.undo(); if (!changed) { return false; @@ -348,7 +353,9 @@ export async function undoSpatialSkeletonCommand(layer: SegmentationUserLayer) { return true; } -export async function redoSpatialSkeletonCommand(layer: SegmentationUserLayer) { +export async function redoSpatialSkeletonCommand( + layer: SpatialSkeletonLayerContext, +) { const changed = await layer.spatialSkeletonState.commandHistory.redo(); if (!changed) { return false; diff --git a/src/skeleton/spatial_skeleton_manager.ts b/src/skeleton/spatial_skeleton_manager.ts index 98cfdc38e..f0d4565e4 100644 --- a/src/skeleton/spatial_skeleton_manager.ts +++ b/src/skeleton/spatial_skeleton_manager.ts @@ -911,3 +911,8 @@ export class SpatialSkeletonState extends RefCounted { this.cachedNodesById.clear(); } } + +export interface SpatialSkeletonLayerContext { + getSpatiallyIndexedSkeletonLayer(): SpatiallyIndexedSkeletonLayer | undefined; + readonly spatialSkeletonState: SpatialSkeletonState; +} diff --git a/src/ui/skeleton_edit_tools.spec.ts b/src/ui/skeleton_edit_tools.spec.ts index d84ce9941..12aaa9770 100644 --- a/src/ui/skeleton_edit_tools.spec.ts +++ b/src/ui/skeleton_edit_tools.spec.ts @@ -18,16 +18,16 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { makeCatmaidNodeSourceState } from "#src/datasource/catmaid/api.js"; import { CatmaidSpatialSkeletonEditCommands } from "#src/datasource/catmaid/spatial_skeleton_commands.js"; -import { - executeSpatialSkeletonAddNode, - executeSpatialSkeletonMerge, -} from "#src/layer/segmentation/spatial_skeleton_commands.js"; import { SpatialSkeletonActions, type SpatialSkeletonAction, } from "#src/skeleton/actions.js"; import type { SpatiallyIndexedSkeletonNode } from "#src/skeleton/api.js"; import { SpatialSkeletonCommandHistory } from "#src/skeleton/command_history.js"; +import { + executeSpatialSkeletonAddNode, + executeSpatialSkeletonMerge, +} from "#src/skeleton/spatial_skeleton_commands.js"; import { StatusMessage } from "#src/status.js"; if (!("WebGL2RenderingContext" in globalThis)) { @@ -737,7 +737,10 @@ describe("spatial_skeleton_edit_tool", () => { }, }, spatialSkeletonMergeMode: makeModeWatchable(), - selectedSpatialSkeletonNodeInfo: { value: selectedNode, changed: makeChangedSignal() }, + selectedSpatialSkeletonNodeInfo: { + value: selectedNode, + changed: makeChangedSignal(), + }, spatialSkeletonState: { mergeAnchorNodeId, getCachedNode: vi.fn(), diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 51a30415f..ae1635035 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -21,21 +21,9 @@ import { getSegmentIdFromLayerSelectionValue, hasSpatialSkeletonNodeSelection, } from "#src/layer/segmentation/selection.js"; -import { - executeSpatialSkeletonAddNode, - executeSpatialSkeletonDeleteNode, - executeSpatialSkeletonMerge, - executeSpatialSkeletonMoveNode, - executeSpatialSkeletonSplit, - showSpatialSkeletonActionError, -} from "#src/layer/segmentation/spatial_skeleton_commands.js"; import { getChunkPositionFromCombinedGlobalLocalPositions } from "#src/render_coordinate_transform.js"; import { RenderedDataPanel } from "#src/rendered_data_panel.js"; -import { - addSegmentToVisibleSets, - getVisibleSegments, - removeSegmentFromVisibleSets, -} from "#src/segmentation_display_state/base.js"; +import { getVisibleSegments } from "#src/segmentation_display_state/base.js"; import { SpatialSkeletonActions } from "#src/skeleton/actions.js"; import type { SpatialSkeletonSourceState, @@ -49,12 +37,21 @@ import { PerspectiveViewSpatiallyIndexedSkeletonLayer, SliceViewPanelSpatiallyIndexedSkeletonLayer, } from "#src/skeleton/frontend.js"; +import { + executeSpatialSkeletonAddNode, + executeSpatialSkeletonDeleteNode, + executeSpatialSkeletonMerge, + executeSpatialSkeletonMoveNode, + executeSpatialSkeletonSplit, + showSpatialSkeletonActionError, +} from "#src/skeleton/spatial_skeleton_commands.js"; import { StatusMessage } from "#src/status.js"; import type { SpatialSkeletonToolPointInfo } from "#src/ui/skeleton_edit_tool_messages.js"; import { SPATIAL_SKELETON_SPLIT_BANNER_MESSAGE, getSpatialSkeletonEditBannerMessage, - getSpatialSkeletonMergeBannerMessage, + SPATIAL_SKELETON_MERGE_BANNER_MESSAGE, + SPATIAL_SKELETON_MERGE_SELECTED_BANNER_MESSAGE, getSpatialSkeletonToolPointStatusFields, SPATIAL_SKELETON_MOVING_NODE_MESSAGE, } from "#src/ui/skeleton_edit_tool_messages.js"; @@ -90,12 +87,7 @@ const SKELETON_EDIT_STATUS_INPUT_EVENT_MAP = EventActionMap.fromObject({ }, }); -const SPATIAL_SKELETON_EDIT_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ - "at:dblclick0": { - action: "spatial-skeleton-toggle-visible", - stopPropagation: true, - preventDefault: true, - }, +const SPATIAL_SKELETON_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ "at:shift+control+mousedown2": { action: "spatial-skeleton-clear-node-selection", stopPropagation: true, @@ -111,19 +103,6 @@ const SPATIAL_SKELETON_PICK_INPUT_EVENT_MAP = EventActionMap.fromObject({ }, }); -const SPATIAL_SKELETON_PICK_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ - "at:dblclick0": { - action: "spatial-skeleton-toggle-visible", - stopPropagation: true, - preventDefault: true, - }, - "at:shift+control+mousedown2": { - action: "spatial-skeleton-clear-node-selection", - stopPropagation: true, - preventDefault: true, - }, -}); - const DRAG_START_DISTANCE_PX = 4; function waitForNextAnimationFrame() { @@ -156,16 +135,13 @@ function renderSpatialSkeletonToolStatus( point.className = "neuroglancer-skeleton-tool-status-point"; for (const field of getSpatialSkeletonToolPointStatusFields(options.point)) { const fieldElement = document.createElement("span"); - fieldElement.className = - "neuroglancer-skeleton-tool-status-point-field"; + fieldElement.className = "neuroglancer-skeleton-tool-status-point-field"; const label = document.createElement("span"); - label.className = - "neuroglancer-skeleton-tool-status-point-field-label"; + label.className = "neuroglancer-skeleton-tool-status-point-field-label"; label.textContent = field.label; fieldElement.appendChild(label); const value = document.createElement("span"); - value.className = - "neuroglancer-skeleton-tool-status-point-field-value"; + value.className = "neuroglancer-skeleton-tool-status-point-field-value"; value.textContent = field.value; fieldElement.appendChild(value); point.appendChild(fieldElement); @@ -251,126 +227,12 @@ abstract class SpatialSkeletonToolBase extends LayerTool this.layer.selectSegment(BigInt(Math.round(value)), true); } - protected ensureSegmentVisibleByNumber(value: number) { - if (!Number.isFinite(value)) return; - addSegmentToVisibleSets( - this.layer.displayState.segmentationGroupState.value, - BigInt(Math.round(value)), - ); - } - - protected removeVisibleSegmentByNumber( - value: number, - options: { - deselect?: boolean; - } = {}, - ) { - if (!Number.isFinite(value)) return; - removeSegmentFromVisibleSets( - this.layer.displayState.segmentationGroupState.value, - BigInt(Math.round(value)), - options, - ); - } - protected isSpatialSkeletonSegmentVisible(segmentId: number) { return getVisibleSegments( this.layer.displayState.segmentationGroupState.value, ).has(BigInt(Math.round(segmentId))); } - protected describeVisibleSegmentRequirement(segmentId: number) { - return `Only visible skeletons are editable. Make skeleton ${segmentId} visible in Seg tab or by double-clicking it in the viewer.`; - } - - protected togglePickedSpatialSkeletonVisibility() { - const pickedSegmentId = this.getPickedSpatialSkeletonSegment(); - if (pickedSegmentId === undefined) { - return false; - } - const skeletonLayer = this.layer.getSpatiallyIndexedSkeletonLayer(); - const isVisible = this.isSpatialSkeletonSegmentVisible(pickedSegmentId); - if (isVisible) { - this.removeVisibleSegmentByNumber(pickedSegmentId, { deselect: true }); - const selectedNodeId = this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId; - const selectedNode = - selectedNodeId === undefined - ? undefined - : skeletonLayer?.getNode(selectedNodeId); - if (selectedNode?.segmentId === pickedSegmentId) { - this.layer.clearSpatialSkeletonNodeSelection(false); - } - const mergeAnchorNodeId = - this.layer.spatialSkeletonState.mergeAnchorNodeId.value; - const anchorSegmentId = - mergeAnchorNodeId === undefined - ? undefined - : (skeletonLayer?.getNode(mergeAnchorNodeId)?.segmentId ?? - this.layer.spatialSkeletonState.getCachedNode(mergeAnchorNodeId) - ?.segmentId); - if (anchorSegmentId === pickedSegmentId) { - this.layer.clearSpatialSkeletonMergeAnchor(); - } - const cachedSegmentIds = new Set( - [ - ...getVisibleSegments( - this.layer.displayState.segmentationGroupState.value, - ).keys(), - ] - .map((segmentId) => Number(segmentId)) - .filter( - (segmentId) => Number.isSafeInteger(segmentId) && segmentId > 0, - ), - ); - for (const retainedSegmentId of skeletonLayer?.getRetainedOverlaySegmentIds() ?? - []) { - cachedSegmentIds.add(retainedSegmentId); - } - this.layer.spatialSkeletonState.evictInactiveSegmentNodes( - cachedSegmentIds, - ); - StatusMessage.showTemporaryMessage( - `Removed skeleton ${pickedSegmentId} from visible/editable skeletons.`, - ); - return true; - } - this.ensureSegmentVisibleByNumber(pickedSegmentId); - this.selectSegmentByNumber(pickedSegmentId); - StatusMessage.showTemporaryMessage( - `Made skeleton ${pickedSegmentId} visible/editable.`, - ); - return true; - } - - protected bindVisibilityToggleAction(activation: ToolActivation) { - activation.bindAction( - "spatial-skeleton-toggle-visible", - (event: ActionEvent) => { - if (event.detail.button !== 0) return; - event.stopPropagation(); - event.detail.preventDefault(); - this.togglePickedSpatialSkeletonVisibility(); - }, - ); - } - - protected resolvePickedNodeForAction( - skeletonLayer: SpatiallyIndexedSkeletonLayer, - ) { - const pickedNode = this.resolvePickedNodeSelection(skeletonLayer); - if (pickedNode === undefined) { - return undefined; - } - if (pickedNode.segmentId !== undefined) { - this.selectSegmentByNumber(pickedNode.segmentId); - } - this.layer.selectSpatialSkeletonNode(pickedNode.nodeId, false, pickedNode); - return { - nodeId: pickedNode.nodeId, - segmentId: pickedNode.segmentId, - }; - } - protected resolvePickedNodeSelection( skeletonLayer: SpatiallyIndexedSkeletonLayer, ) { @@ -395,7 +257,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool segmentId?: number; position?: SpatialSkeletonVector; sourceState?: SpatialSkeletonSourceState; - visible: boolean; } | undefined { const nodeHit = this.getPickedSpatialSkeletonNode(); @@ -405,15 +266,11 @@ abstract class SpatialSkeletonToolBase extends LayerTool const resolvedNodeInfo = skeletonLayer.getNode(nodeHit.nodeId) ?? this.layer.spatialSkeletonState.getCachedNode(nodeHit.nodeId); - const segmentId = nodeHit.segmentId ?? resolvedNodeInfo?.segmentId; return { nodeId: nodeHit.nodeId, - segmentId, + segmentId: nodeHit.segmentId ?? resolvedNodeInfo?.segmentId, position: nodeHit.position ?? resolvedNodeInfo?.position, sourceState: nodeHit.sourceState ?? resolvedNodeInfo?.sourceState, - visible: - segmentId !== undefined && - this.isSpatialSkeletonSegmentVisible(segmentId), }; } @@ -528,18 +385,12 @@ abstract class SpatialSkeletonToolBase extends LayerTool activation.bindAction( "spatial-skeleton-clear-node-selection", (event: ActionEvent) => { - if ( - event.detail.button !== 2 || - !event.detail.ctrlKey || - !event.detail.shiftKey - ) { - return; - } event.stopPropagation(); event.detail.preventDefault(); const pinnedSelection = this.layer.manager.root.selectionState.value; const hasSpatialSkeletonSelection = - this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId !== undefined || + this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId !== + undefined || (pinnedSelection?.layers.some( ({ layer, state }) => layer === this.layer && hasSpatialSkeletonNodeSelection(state), @@ -552,13 +403,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool if (hasMergeAnchor) { this.layer.clearSpatialSkeletonMergeAnchor(); } - StatusMessage.showTemporaryMessage( - hasMergeAnchor - ? hasSpatialSkeletonSelection - ? "Spatial skeleton selection and merge anchor cleared." - : "Spatial skeleton merge anchor cleared." - : "Spatial skeleton node selection cleared.", - ); return; } this.layer.manager.root.selectionState.unpin(); @@ -602,6 +446,30 @@ abstract class SpatialSkeletonToolBase extends LayerTool this.layer.layersChanged.add(handleStateChanged), ); } + + protected cancelActivationIfPreconditionsFail( + activation: ToolActivation, + requiredAction: Parameters< + SegmentationUserLayer["getSpatialSkeletonActionsDisabledReason"] + >[0], + ): boolean { + const reason = this.layer.getSpatialSkeletonActionsDisabledReason( + requiredAction, + ); + if (reason !== undefined) { + StatusMessage.showTemporaryMessage(reason); + queueMicrotask(() => activation.cancel()); + return false; + } + if (this.getActiveSpatiallyIndexedSkeletonLayer() === undefined) { + StatusMessage.showTemporaryMessage( + "No spatially indexed skeleton source is currently loaded.", + ); + queueMicrotask(() => activation.cancel()); + return false; + } + return true; + } } export class SpatialSkeletonEditModeTool extends SpatialSkeletonToolBase { @@ -800,14 +668,13 @@ export class SpatialSkeletonEditModeTool extends SpatialSkeletonToolBase { this.activateModeWatchable(activation, layer.spatialSkeletonEditMode); activation.bindInputEventMap(SKELETON_EDIT_STATUS_INPUT_EVENT_MAP); rawInputEventMapBinder( - SPATIAL_SKELETON_EDIT_AUX_INPUT_EVENT_MAP, + SPATIAL_SKELETON_AUX_INPUT_EVENT_MAP, activation, ); this.bindPinnedSelectionAction(activation, { showNodeSelectionMessage: false, }); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); updateInteractionStatus(); activation.registerDisposer(() => { layer.spatialSkeletonState.clearPendingNodePositions(); @@ -868,7 +735,8 @@ export class SpatialSkeletonEditModeTool extends SpatialSkeletonToolBase { ); return; } - const selectedParentNodeId = layer.selectedSpatialSkeletonNodeInfo.value?.nodeId; + const selectedParentNodeId = + layer.selectedSpatialSkeletonNodeInfo.value?.nodeId; const addNodeBlockedReason = this.getAddNodeBlockedReason( skeletonLayer, selectedParentNodeId, @@ -1140,22 +1008,14 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { } activate(activation: ToolActivation) { - const rawInputEventMapBinder = activation.inputEventMapBinder; - const reason = this.layer.getSpatialSkeletonActionsDisabledReason( - SpatialSkeletonActions.mergeSkeletons, - ); - if (reason !== undefined) { - StatusMessage.showTemporaryMessage(reason); - queueMicrotask(() => activation.cancel()); - return; - } - if (this.getActiveSpatiallyIndexedSkeletonLayer() === undefined) { - StatusMessage.showTemporaryMessage( - "No spatially indexed skeleton source is currently loaded.", - ); - queueMicrotask(() => activation.cancel()); + if ( + !this.cancelActivationIfPreconditionsFail( + activation, + SpatialSkeletonActions.mergeSkeletons, + ) + ) return; - } + const rawInputEventMapBinder = activation.inputEventMapBinder; this.activateModeWatchable(activation, this.layer.spatialSkeletonMergeMode); const { body, header } = @@ -1173,10 +1033,7 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { const skeletonLayer = this.getActiveSpatiallyIndexedSkeletonLayer(); const selectedNode = this.getSelectedSpatialSkeletonNodeForTool(skeletonLayer); - if ( - selectedNode?.segmentId !== undefined && - this.isSpatialSkeletonSegmentVisible(selectedNode.segmentId) - ) { + if (selectedNode !== undefined) { anchorSelection = selectedNode; this.layer.selectSpatialSkeletonNode( selectedNode.nodeId, @@ -1196,12 +1053,16 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { anchorSelection = undefined; return undefined; } - if (anchorSelection?.nodeId === nodeId) { - return anchorSelection; - } const cachedNode = this.getActiveSpatiallyIndexedSkeletonLayer()?.getNode(nodeId) ?? this.layer.spatialSkeletonState.getCachedNode(nodeId); + if ( + anchorSelection?.nodeId === nodeId && + (cachedNode === undefined || + anchorSelection.segmentId === cachedNode.segmentId) + ) { + return anchorSelection; + } const anchorNode = { nodeId, segmentId: cachedNode?.segmentId, @@ -1213,9 +1074,19 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { }; const renderStatus = () => { const anchorNode = getAnchorNode(); + let mergeStatus: string; + if (anchorNode === undefined) { + mergeStatus = SPATIAL_SKELETON_MERGE_BANNER_MESSAGE; + } else if ( + anchorNode.segmentId !== undefined && + !this.isSpatialSkeletonSegmentVisible(anchorNode.segmentId) + ) { + mergeStatus = `Make this segment visible, then select a 2nd node to merge with`; + } else { + mergeStatus = SPATIAL_SKELETON_MERGE_SELECTED_BANNER_MESSAGE; + } renderSpatialSkeletonToolStatus(body, { - message: - statusOverride ?? getSpatialSkeletonMergeBannerMessage(anchorNode), + message: statusOverride ?? mergeStatus, point: anchorNode, }); }; @@ -1229,11 +1100,10 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { setReadyStatus(); activation.bindInputEventMap(SPATIAL_SKELETON_PICK_INPUT_EVENT_MAP); rawInputEventMapBinder( - SPATIAL_SKELETON_PICK_AUX_INPUT_EVENT_MAP, + SPATIAL_SKELETON_AUX_INPUT_EVENT_MAP, activation, ); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); this.registerAutoCancelOnDisabled( activation, SpatialSkeletonActions.mergeSkeletons, @@ -1244,10 +1114,16 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { renderStatus, ), ); + activation.registerDisposer( + this.layer.displayState.segmentationGroupState.value.visibleSegments.changed.add( + renderStatus, + ), + ); activation.registerDisposer( this.layer.selectedSpatialSkeletonNodeInfo.changed.add(() => { if ( - this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId === undefined && + this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId === + undefined && this.layer.spatialSkeletonState.mergeAnchorNodeId.value !== undefined ) { anchorSelection = undefined; @@ -1259,16 +1135,7 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { ); activation.bindAction( "spatial-skeleton-pick-node", - (event: ActionEvent) => { - if ( - event.detail.button !== 2 || - !event.detail.ctrlKey || - event.detail.shiftKey || - event.detail.altKey || - event.detail.metaKey - ) { - return; - } + (_event: ActionEvent) => { if (pending) return; const disabledReason = this.layer.getSpatialSkeletonActionsDisabledReason( @@ -1299,42 +1166,14 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { } return; } - if (pickedNode === undefined || pickedNode.segmentId === undefined) { + if (pickedNode.segmentId === undefined) { return; } if ( anchorNode === undefined || - anchorNode.nodeId === pickedNode.nodeId + anchorNode.nodeId === pickedNode.nodeId || + anchorNode.segmentId === pickedNode.segmentId ) { - if (!pickedNode.visible) { - StatusMessage.showTemporaryMessage( - "Pick the first merge anchor from a visible segment.", - ); - return; - } - this.pinSegmentByNumber(pickedNode.segmentId); - anchorSelection = { - nodeId: pickedNode.nodeId, - segmentId: pickedNode.segmentId, - position: pickedNode.position, - sourceState: pickedNode.sourceState, - }; - this.layer.setSpatialSkeletonMergeAnchor(pickedNode.nodeId); - this.layer.selectSpatialSkeletonNode( - pickedNode.nodeId, - true, - pickedNode, - ); - renderStatus(); - return; - } - if (anchorNode.segmentId === pickedNode.segmentId) { - if (!pickedNode.visible) { - StatusMessage.showTemporaryMessage( - "Pick the first merge anchor from a visible segment.", - ); - return; - } this.pinSegmentByNumber(pickedNode.segmentId); anchorSelection = { nodeId: pickedNode.nodeId, @@ -1367,6 +1206,13 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { ); return; } + if (!this.isSpatialSkeletonSegmentVisible(firstNode.segmentId)) { + StatusMessage.showTemporaryMessage( + `The first node selected for a merge operation must be from a visible skeleton. Make skeleton ${firstNode.segmentId} visible in the Seg tab or by double-clicking it in the viewer.`, + 3000, + ); + return; + } this.pinSegmentByNumber(pickedNode.segmentId); this.layer.selectSpatialSkeletonNode( pickedNode.nodeId, @@ -1397,6 +1243,7 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { showSpatialSkeletonActionError("merge skeletons", error); } finally { pending = false; + this.layer.setSpatialSkeletonMergeAnchor(secondNode.nodeId); setReadyStatus(); } })(); @@ -1415,22 +1262,14 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { } activate(activation: ToolActivation) { - const rawInputEventMapBinder = activation.inputEventMapBinder; - const reason = this.layer.getSpatialSkeletonActionsDisabledReason( - SpatialSkeletonActions.splitSkeletons, - ); - if (reason !== undefined) { - StatusMessage.showTemporaryMessage(reason); - queueMicrotask(() => activation.cancel()); - return; - } - if (this.getActiveSpatiallyIndexedSkeletonLayer() === undefined) { - StatusMessage.showTemporaryMessage( - "No spatially indexed skeleton source is currently loaded.", - ); - queueMicrotask(() => activation.cancel()); + if ( + !this.cancelActivationIfPreconditionsFail( + activation, + SpatialSkeletonActions.splitSkeletons, + ) + ) return; - } + const rawInputEventMapBinder = activation.inputEventMapBinder; this.activateModeWatchable(activation, this.layer.spatialSkeletonSplitMode); const { body, header } = @@ -1503,11 +1342,10 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { setReadyStatus(); activation.bindInputEventMap(SPATIAL_SKELETON_PICK_INPUT_EVENT_MAP); rawInputEventMapBinder( - SPATIAL_SKELETON_PICK_AUX_INPUT_EVENT_MAP, + SPATIAL_SKELETON_AUX_INPUT_EVENT_MAP, activation, ); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); this.registerAutoCancelOnDisabled( activation, SpatialSkeletonActions.splitSkeletons, @@ -1524,16 +1362,7 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { } activation.bindAction( "spatial-skeleton-pick-node", - (event: ActionEvent) => { - if ( - event.detail.button !== 2 || - !event.detail.ctrlKey || - event.detail.shiftKey || - event.detail.altKey || - event.detail.metaKey - ) { - return; - } + (_event: ActionEvent) => { if (pending) return; const disabledReason = this.layer.getSpatialSkeletonActionsDisabledReason( @@ -1560,7 +1389,7 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { } return; } - if (pickedNode === undefined || pickedNode.segmentId === undefined) { + if (pickedNode.segmentId === undefined) { return; } splitNode(pickedNode); diff --git a/src/ui/skeleton_tab.ts b/src/ui/skeleton_tab.ts index c8ad36372..005b0291f 100644 --- a/src/ui/skeleton_tab.ts +++ b/src/ui/skeleton_tab.ts @@ -31,13 +31,6 @@ import svg_share_android from "ikonate/icons/share-android.svg?raw"; import svg_undo from "ikonate/icons/undo.svg?raw"; import type { SegmentationUserLayer } from "#src/layer/segmentation/index.js"; import { getSegmentIdFromLayerSelectionValue } from "#src/layer/segmentation/selection.js"; -import { - executeSpatialSkeletonDeleteNode, - executeSpatialSkeletonNodeTrueEndUpdate, - redoSpatialSkeletonCommand, - showSpatialSkeletonActionError, - undoSpatialSkeletonCommand, -} from "#src/layer/segmentation/spatial_skeleton_commands.js"; import { getSegmentEquivalences, getVisibleSegments, @@ -67,6 +60,13 @@ import { SpatialSkeletonDisplayNodeType, SpatialSkeletonNodeFilterType, } from "#src/skeleton/node_types.js"; +import { + executeSpatialSkeletonDeleteNode, + executeSpatialSkeletonNodeTrueEndUpdate, + redoSpatialSkeletonCommand, + showSpatialSkeletonActionError, + undoSpatialSkeletonCommand, +} from "#src/skeleton/spatial_skeleton_commands.js"; import { StatusMessage } from "#src/status.js"; import { observeWatchable, registerNested } from "#src/trackable_value.js"; import { @@ -1429,7 +1429,8 @@ export class SpatialSkeletonEditTab extends Tab { segmentState === undefined || segmentState.totalNodeCount === 0 || (getFilterText().length === 0 && - (nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.DEFAULT || + (nodeFilterTypeModel.value === + SpatialSkeletonNodeFilterType.DEFAULT || nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.NONE)) ) { return "No loaded nodes.";