From b6e1a8268dfacb282d328cebdb6d833a89a770dc Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 11:32:57 +0200 Subject: [PATCH 1/9] refactor: abstract layer and move spatial skel commands to skeleton dir --- .../catmaid/spatial_skeleton_commands.ts | 5 ++- src/layer/segmentation/index.ts | 33 ++++++++------ src/layer/segmentation/style.css | 3 +- src/skeleton/frontend.ts | 4 +- .../spatial_skeleton_commands.spec.ts | 18 ++++---- .../spatial_skeleton_commands.ts | 45 +++++++++++-------- src/skeleton/spatial_skeleton_manager.ts | 5 +++ src/ui/skeleton_edit_tools.spec.ts | 13 +++--- src/ui/skeleton_edit_tools.ts | 37 +++++++-------- src/ui/skeleton_tab.ts | 17 +++---- 10 files changed, 103 insertions(+), 77 deletions(-) rename src/{layer/segmentation => skeleton}/spatial_skeleton_commands.spec.ts (99%) rename src/{layer/segmentation => skeleton}/spatial_skeleton_commands.ts (89%) 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..84a813de1 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -21,14 +21,6 @@ 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 { @@ -49,6 +41,14 @@ 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 { @@ -156,16 +156,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); @@ -292,7 +289,8 @@ abstract class SpatialSkeletonToolBase extends LayerTool const isVisible = this.isSpatialSkeletonSegmentVisible(pickedSegmentId); if (isVisible) { this.removeVisibleSegmentByNumber(pickedSegmentId, { deselect: true }); - const selectedNodeId = this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId; + const selectedNodeId = + this.layer.selectedSpatialSkeletonNodeInfo.value?.nodeId; const selectedNode = selectedNodeId === undefined ? undefined @@ -539,7 +537,8 @@ abstract class SpatialSkeletonToolBase extends LayerTool 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), @@ -868,7 +867,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, @@ -1247,7 +1247,8 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { 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; 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."; From 313c4aba0b0afe23cb964b45e9ce7f4c12018758 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 12:00:00 +0200 Subject: [PATCH 2/9] fix: correct merge anchor problems with non visible seg previously you had to make the segment visible and then pick the node again also fixes some action key guards which are not needed and an usused message --- src/ui/skeleton_edit_tools.ts | 64 ++++++----------------------------- 1 file changed, 10 insertions(+), 54 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 84a813de1..2547014db 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -276,10 +276,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool ).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) { @@ -526,13 +522,6 @@ 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; @@ -551,13 +540,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(); @@ -1173,10 +1155,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, @@ -1260,16 +1239,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( @@ -1307,12 +1277,6 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { anchorNode === undefined || anchorNode.nodeId === pickedNode.nodeId ) { - if (!pickedNode.visible) { - StatusMessage.showTemporaryMessage( - "Pick the first merge anchor from a visible segment.", - ); - return; - } this.pinSegmentByNumber(pickedNode.segmentId); anchorSelection = { nodeId: pickedNode.nodeId, @@ -1330,12 +1294,6 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { 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, @@ -1368,6 +1326,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, @@ -1525,16 +1490,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( From 48d90296395b4be824bb00edb5bc6634de142113 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 12:06:56 +0200 Subject: [PATCH 3/9] fix: allow chain merges --- src/ui/skeleton_edit_tools.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 2547014db..c9895e2a9 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -1363,6 +1363,7 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { showSpatialSkeletonActionError("merge skeletons", error); } finally { pending = false; + this.layer.setSpatialSkeletonMergeAnchor(secondNode.nodeId); setReadyStatus(); } })(); From 2e41adc486738e4bc08ecad36a1e6662d652bfd9 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 12:17:57 +0200 Subject: [PATCH 4/9] fix: allow merge after undo --- src/ui/skeleton_edit_tools.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index c9895e2a9..2c2523855 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -1181,6 +1181,12 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { const cachedNode = this.getActiveSpatiallyIndexedSkeletonLayer()?.getNode(nodeId) ?? this.layer.spatialSkeletonState.getCachedNode(nodeId); + if ( + anchorSelection?.nodeId === nodeId && + anchorSelection.segmentId === cachedNode?.segmentId + ) { + return anchorSelection; + } const anchorNode = { nodeId, segmentId: cachedNode?.segmentId, From d5c2b3278a5a631b68617136ca73524635accdc8 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 12:53:35 +0200 Subject: [PATCH 5/9] feat: allow merge banner to track visibility --- src/ui/skeleton_edit_tools.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 2c2523855..70b19f2d2 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -54,7 +54,8 @@ import type { SpatialSkeletonToolPointInfo } from "#src/ui/skeleton_edit_tool_me 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"; @@ -1175,15 +1176,13 @@ 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 && - anchorSelection.segmentId === cachedNode?.segmentId + (cachedNode === undefined || + anchorSelection.segmentId === cachedNode.segmentId) ) { return anchorSelection; } @@ -1198,9 +1197,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,6 +1238,11 @@ 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 ( From b2f73fb0b770a9ff2308475b0292cd8a75256d44 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 12:58:09 +0200 Subject: [PATCH 6/9] fix: remove old uneeded complication on selected handling --- src/ui/skeleton_edit_tools.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 70b19f2d2..0e481dd25 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -286,26 +286,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool 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( @@ -324,16 +304,10 @@ abstract class SpatialSkeletonToolBase extends LayerTool 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; } @@ -341,7 +315,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool activation.bindAction( "spatial-skeleton-toggle-visible", (event: ActionEvent) => { - if (event.detail.button !== 0) return; event.stopPropagation(); event.detail.preventDefault(); this.togglePickedSpatialSkeletonVisibility(); From d61e93b3df803b44d521bf3380924b7235e87c29 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 13:33:58 +0200 Subject: [PATCH 7/9] fix: remove double active bind already handled in layer --- src/ui/skeleton_edit_tools.ts | 58 ----------------------------------- 1 file changed, 58 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 0e481dd25..9f89ad9b8 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -92,11 +92,6 @@ 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, - }, "at:shift+control+mousedown2": { action: "spatial-skeleton-clear-node-selection", stopPropagation: true, @@ -113,11 +108,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, @@ -277,51 +267,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool ).has(BigInt(Math.round(segmentId))); } - 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 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, - ); - return true; - } - this.ensureSegmentVisibleByNumber(pickedSegmentId); - this.selectSegmentByNumber(pickedSegmentId); - return true; - } - - protected bindVisibilityToggleAction(activation: ToolActivation) { - activation.bindAction( - "spatial-skeleton-toggle-visible", - (event: ActionEvent) => { - event.stopPropagation(); - event.detail.preventDefault(); - this.togglePickedSpatialSkeletonVisibility(); - }, - ); - } - protected resolvePickedNodeForAction( skeletonLayer: SpatiallyIndexedSkeletonLayer, ) { @@ -762,7 +707,6 @@ export class SpatialSkeletonEditModeTool extends SpatialSkeletonToolBase { showNodeSelectionMessage: false, }); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); updateInteractionStatus(); activation.registerDisposer(() => { layer.spatialSkeletonState.clearPendingNodePositions(); @@ -1200,7 +1144,6 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { activation, ); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); this.registerAutoCancelOnDisabled( activation, SpatialSkeletonActions.mergeSkeletons, @@ -1467,7 +1410,6 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { activation, ); this.bindClearSelectionAction(activation); - this.bindVisibilityToggleAction(activation); this.registerAutoCancelOnDisabled( activation, SpatialSkeletonActions.splitSkeletons, From 96bb0d8d9ad2955e1fcc9fc496fcccd3d02153b7 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 13:41:12 +0200 Subject: [PATCH 8/9] refactor: remove dead code --- src/ui/skeleton_edit_tools.ts | 45 +---------------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index 9f89ad9b8..c2802ec76 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -23,11 +23,7 @@ import { } from "#src/layer/segmentation/selection.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, @@ -239,51 +235,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 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, ) { From f85a16d54fbd6180ec1af03a05df213de910029f Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Fri, 12 Jun 2026 14:36:46 +0200 Subject: [PATCH 9/9] refactor: clean up some unused code and duplication --- src/ui/skeleton_edit_tools.ts | 115 ++++++++++++++-------------------- 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/src/ui/skeleton_edit_tools.ts b/src/ui/skeleton_edit_tools.ts index c2802ec76..ae1635035 100644 --- a/src/ui/skeleton_edit_tools.ts +++ b/src/ui/skeleton_edit_tools.ts @@ -87,7 +87,7 @@ const SKELETON_EDIT_STATUS_INPUT_EVENT_MAP = EventActionMap.fromObject({ }, }); -const SPATIAL_SKELETON_EDIT_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ +const SPATIAL_SKELETON_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ "at:shift+control+mousedown2": { action: "spatial-skeleton-clear-node-selection", stopPropagation: true, @@ -103,14 +103,6 @@ const SPATIAL_SKELETON_PICK_INPUT_EVENT_MAP = EventActionMap.fromObject({ }, }); -const SPATIAL_SKELETON_PICK_AUX_INPUT_EVENT_MAP = EventActionMap.fromObject({ - "at:shift+control+mousedown2": { - action: "spatial-skeleton-clear-node-selection", - stopPropagation: true, - preventDefault: true, - }, -}); - const DRAG_START_DISTANCE_PX = 4; function waitForNextAnimationFrame() { @@ -265,7 +257,6 @@ abstract class SpatialSkeletonToolBase extends LayerTool segmentId?: number; position?: SpatialSkeletonVector; sourceState?: SpatialSkeletonSourceState; - visible: boolean; } | undefined { const nodeHit = this.getPickedSpatialSkeletonNode(); @@ -275,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), }; } @@ -459,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 { @@ -657,7 +668,7 @@ 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, { @@ -997,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 } = @@ -1097,7 +1100,7 @@ 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); @@ -1163,12 +1166,13 @@ 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 ) { this.pinSegmentByNumber(pickedNode.segmentId); anchorSelection = { @@ -1186,23 +1190,6 @@ export class SpatialSkeletonMergeModeTool extends SpatialSkeletonToolBase { renderStatus(); return; } - if (anchorNode.segmentId === pickedNode.segmentId) { - 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; - } const firstNode = anchorNode; const secondNode = { nodeId: pickedNode.nodeId, @@ -1275,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 } = @@ -1363,7 +1342,7 @@ 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); @@ -1410,7 +1389,7 @@ export class SpatialSkeletonSplitModeTool extends SpatialSkeletonToolBase { } return; } - if (pickedNode === undefined || pickedNode.segmentId === undefined) { + if (pickedNode.segmentId === undefined) { return; } splitNode(pickedNode);