Skip to content
14 changes: 6 additions & 8 deletions src/datasource/catmaid/spatial_skeleton_commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,18 @@ import {
addSegmentToVisibleSets,
removeSegmentFromVisibleSets,
} from "#src/segmentation_display_state/base.js";
import {
SpatialSkeletonActions,
type SpatialSkeletonAction,
} from "#src/skeleton/actions.js";
import type {
SpatiallyIndexedSkeletonNode,
SpatialSkeletonSourceState,
SpatialSkeletonVector,
} from "#src/skeleton/api.js";
import type { SpatialSkeletonEditCommandFactory } from "#src/skeleton/command_factories.js";
import type {
SpatialSkeletonCommand,
SpatialSkeletonCommandContext,
} from "#src/skeleton/command_history.js";
import {
SpatialSkeletonActions,
type SpatialSkeletonAction,
type SpatialSkeletonCommand,
type SpatialSkeletonCommandContext,
} from "#src/skeleton/command_protocol.js";
import type { SpatiallyIndexedSkeletonLayer } from "#src/skeleton/frontend.js";
import {
findSpatiallyIndexedSkeletonNode,
Expand Down
2 changes: 1 addition & 1 deletion src/layer/segmentation/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { describe, expect, it, vi } from "vitest";

import type { RenderLayerTransform } from "#src/render_coordinate_transform.js";
import { SpatialSkeletonActions } from "#src/skeleton/actions.js";
import { SpatialSkeletonActions } from "#src/skeleton/command_protocol.js";
import { WatchableValue } from "#src/trackable_value.js";

if (!("WebGL2RenderingContext" in globalThis)) {
Expand Down
265 changes: 244 additions & 21 deletions src/layer/segmentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,38 @@ import { SegmentationGraphSourceTab } from "#src/segmentation_graph/source.js";
import { SharedDisjointUint64Sets } from "#src/shared_disjoint_sets.js";
import { SharedWatchableValue } from "#src/shared_watchable_value.js";
import {
DEFAULT_SPATIAL_SKELETON_EDIT_ACTIONS,
getSpatialSkeletonActionSupportLabel,
isSpatialSkeletonEditAction,
SpatialSkeletonActions,
type SpatialSkeletonAction,
SKELETON_CYCLE_BRANCHES,
SKELETON_GO_BRANCH_END,
SKELETON_GO_BRANCH_START,
SKELETON_GO_CHILD,
SKELETON_GO_PARENT,
SKELETON_GO_ROOT,
SKELETON_GO_UNFINISHED,
SKELETON_REDO,
SKELETON_UNDO,
} from "#src/skeleton/actions.js";
import type {
SpatiallyIndexedSkeletonNode,
SpatialSkeletonSourceState,
} from "#src/skeleton/api.js";
import {
DEFAULT_SPATIAL_SKELETON_EDIT_ACTIONS,
getSpatialSkeletonActionSupportLabel,
isSpatialSkeletonEditAction,
SpatialSkeletonActions,
type SpatialSkeletonAction,
} from "#src/skeleton/command_protocol.js";
import {
executeSpatialSkeletonDeleteNode,
executeSpatialSkeletonNodeConfidenceUpdate,
executeSpatialSkeletonNodeDescriptionUpdate,
executeSpatialSkeletonNodeRadiusUpdate,
executeSpatialSkeletonReroot,
executeSpatialSkeletonNodeTrueEndUpdate,
redoSpatialSkeletonCommand,
showSpatialSkeletonActionError,
undoSpatialSkeletonCommand,
} from "#src/skeleton/commands.js";
import {
PerspectiveViewSkeletonLayer,
SkeletonLayer,
Expand All @@ -117,6 +139,16 @@ import {
SpatiallyIndexedSkeletonSource,
MultiscaleSpatiallyIndexedSkeletonSource,
} from "#src/skeleton/frontend.js";
import {
buildSpatiallyIndexedSkeletonNavigationGraph,
getBranchEnd as getBranchEndFromGraph,
getBranchStart as getBranchStartFromGraph,
getNextCollapsedLevelNode as getNextCollapsedLevelNodeFromGraph,
getOpenLeaves as getOpenLeavesFromGraph,
getParentNode as getParentNodeFromGraph,
getRandomChildNode as getRandomChildNodeFromGraph,
getSkeletonRootNode as getSkeletonRootNodeFromGraph,
} from "#src/skeleton/navigation_graph.js";
import {
findSpatiallyIndexedSkeletonNode,
getSpatiallyIndexedSkeletonDirectChildren,
Expand All @@ -129,15 +161,6 @@ 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,
Expand Down Expand Up @@ -2017,8 +2040,205 @@ export class SegmentationUserLayer extends Base {
}
break;
}
case SKELETON_GO_ROOT:
case SKELETON_GO_BRANCH_START:
case SKELETON_GO_BRANCH_END:
case SKELETON_CYCLE_BRANCHES:
case SKELETON_GO_PARENT:
case SKELETON_GO_CHILD:
case SKELETON_GO_UNFINISHED:
case SKELETON_UNDO:
case SKELETON_REDO: {
if (!this.shouldHandleGlobalSkeletonAction()) return;
void this.handleSkeletonNavigationAction(action);
break;
}
}
}

private shouldHandleGlobalSkeletonAction(): boolean {
let skeletonLayerCount = 0;
for (const managedLayer of this.manager.root.layerManager.managedLayers) {
if (!managedLayer.visible || managedLayer.layer === null) continue;
const layer = managedLayer.layer;
if (
layer instanceof SegmentationUserLayer &&
layer.getSpatialSkeletonActionsDisabledReason(
SpatialSkeletonActions.inspect,
{ requireVisibleChunks: false },
) === undefined
) {
skeletonLayerCount++;
if (skeletonLayerCount > 1) break;
}
}
return (
skeletonLayerCount <= 1 ||
this.managedLayer === this.manager.root.selectedLayer.layer
);
}

private async handleSkeletonNavigationAction(action: string): Promise<void> {
const inspectDisabledReason = this.getSpatialSkeletonActionsDisabledReason(
SpatialSkeletonActions.inspect,
{ requireVisibleChunks: false },
);
if (inspectDisabledReason !== undefined) {
StatusMessage.showTemporaryMessage(inspectDisabledReason);
return;
}

if (action === SKELETON_UNDO) {
try {
await undoSpatialSkeletonCommand(this);
} catch (error) {
showSpatialSkeletonActionError("undo", error);
}
return;
}
if (action === SKELETON_REDO) {
try {
await redoSpatialSkeletonCommand(this);
} catch (error) {
showSpatialSkeletonActionError("redo", error);
}
return;
}

const nodeInfo = this.selectedSpatialSkeletonNodeInfo.value;
const cachedNode =
nodeInfo?.nodeId !== undefined
? this.spatialSkeletonState.getCachedNode(nodeInfo.nodeId)
: undefined;

const segmentId =
cachedNode?.segmentId ??
nodeInfo?.segmentId ??
getSegmentIdFromLayerSelectionValue(
this.manager.root.selectionState.value?.layers.find(
(entry) => entry.layer === this,
)?.state,
);

if (segmentId === undefined) {
StatusMessage.showTemporaryMessage("No segment/skeleton is selected.");
return;
}

const segmentNodes =
this.spatialSkeletonState.getCachedSegmentNodes(segmentId);
if (segmentNodes === undefined || segmentNodes.length === 0) {
StatusMessage.showTemporaryMessage(
"A non-visible segment is selected. Make it visible to use skeleton navigation features.",
);
return;
}

const graph = buildSpatiallyIndexedSkeletonNavigationGraph(segmentNodes);

try {
if (action === SKELETON_GO_ROOT) {
const target = getSkeletonRootNodeFromGraph(graph);
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
return;
}

const nodeId = cachedNode?.nodeId ?? nodeInfo?.nodeId;
if (nodeId === undefined) {
StatusMessage.showTemporaryMessage(
"No skeleton node is selected, only go to root is supported on skeleton edges.",
);
return;
}

switch (action) {
case SKELETON_GO_BRANCH_START: {
const target = getBranchStartFromGraph(graph, nodeId);
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
break;
}
case SKELETON_GO_BRANCH_END: {
const target = getBranchEndFromGraph(graph, nodeId);
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
break;
}
case SKELETON_CYCLE_BRANCHES: {
const target = getNextCollapsedLevelNodeFromGraph(graph, nodeId);
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
break;
}
case SKELETON_GO_PARENT: {
const target = getParentNodeFromGraph(graph, nodeId);
if (target === undefined) {
StatusMessage.showTemporaryMessage("Selected node has no parent.");
return;
}
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
break;
}
case SKELETON_GO_CHILD: {
const target = getRandomChildNodeFromGraph(graph, nodeId);
if (target === undefined) {
StatusMessage.showTemporaryMessage("Selected node has no child.");
return;
}
this.selectAndMoveToSpatialSkeletonNode({
nodeId: target.nodeId,
segmentId,
position: target.position,
});
break;
}
case SKELETON_GO_UNFINISHED: {
const openLeaves = getOpenLeavesFromGraph(graph, nodeId);
if (openLeaves.length === 0) {
StatusMessage.showTemporaryMessage(
"No unfinished branch was found in the current skeleton.",
);
return;
}
openLeaves.sort((a, b) =>
a.distance === b.distance
? a.nodeId - b.nodeId
: a.distance - b.distance,
);
const leaf = openLeaves[0];
this.selectAndMoveToSpatialSkeletonNode({
nodeId: leaf.nodeId,
segmentId,
position: leaf.position,
});
break;
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
StatusMessage.showTemporaryMessage(
`Skeleton navigation failed: ${message}`,
);
}
}

selectionStateFromJson(state: this["selectionState"], json: any) {
super.selectionStateFromJson(state, json);
let parsedValue = state.value;
Expand Down Expand Up @@ -2295,12 +2515,14 @@ export class SegmentationUserLayer extends Base {
? "Load the active skeleton in the Skeleton tab before rerooting from Selection."
: fullNodeInfo.parentNodeId === undefined
? "Selected node is already root."
: this.getSpatialSkeletonActionsDisabledReason(
SpatialSkeletonActions.reroot,
{
requireVisibleChunks: false,
},
);
: (fullNodeInfo.isTrueEnd ?? false)
? "True end nodes cannot be set as root. Clear the true end state first."
: this.getSpatialSkeletonActionsDisabledReason(
SpatialSkeletonActions.reroot,
{
requireVisibleChunks: false,
},
);
const rerootButton = document.createElement("button");
rerootButton.type = "button";
rerootButton.className = "neuroglancer-selection-details-skeleton-action";
Expand All @@ -2319,7 +2541,8 @@ export class SegmentationUserLayer extends Base {
rerootButton.disabled ||
rerootPending ||
completeNodeInfo === undefined ||
completeNodeInfo.parentNodeId === undefined
completeNodeInfo.parentNodeId === undefined ||
(completeNodeInfo.isTrueEnd ?? false)
) {
return;
}
Expand Down
Loading