diff --git a/python/neuroglancer/viewer_state.py b/python/neuroglancer/viewer_state.py index 246e4ea93..63ff7c630 100644 --- a/python/neuroglancer/viewer_state.py +++ b/python/neuroglancer/viewer_state.py @@ -649,6 +649,7 @@ def _linked_segmentation_color_group_value(x): @export class SkeletonNodeFilterType(enum.StrEnum): + DEFAULT = "default" NONE = "none" LEAF = "leaf" VIRTUAL_END = "virtual_end" diff --git a/src/layer/segmentation/index.ts b/src/layer/segmentation/index.ts index 4cf4fb3a0..63ed4c8c2 100644 --- a/src/layer/segmentation/index.ts +++ b/src/layer/segmentation/index.ts @@ -718,7 +718,7 @@ class SegmentationUserLayerDisplayState implements SegmentationDisplayState { spatialSkeletonNodeQuery = new TrackableValue("", verifyString); spatialSkeletonNodeFilter = new TrackableEnum( SpatialSkeletonNodeFilterType, - SpatialSkeletonNodeFilterType.NONE, + SpatialSkeletonNodeFilterType.DEFAULT, ); ignoreNullVisibleSet = new TrackableBoolean(true, true); skeletonRenderingOptions = new SkeletonRenderingOptions(); diff --git a/src/skeleton/node_types.spec.ts b/src/skeleton/node_types.spec.ts index 8f672b6c8..f76adfc54 100644 --- a/src/skeleton/node_types.spec.ts +++ b/src/skeleton/node_types.spec.ts @@ -90,6 +90,20 @@ describe("skeleton node types", () => { nodeType: SpatialSkeletonDisplayNodeType.REGULAR, }; + expect( + matchesSpatialSkeletonNodeFilter( + SpatialSkeletonNodeFilterType.DEFAULT, + rootLeaf, + ), + ).toBe(true); + expect( + matchesSpatialSkeletonNodeFilter(SpatialSkeletonNodeFilterType.DEFAULT, { + isLeaf: false, + nodeHasDescription: false, + nodeIsTrueEnd: false, + nodeType: SpatialSkeletonDisplayNodeType.REGULAR, + }), + ).toBe(true); expect( matchesSpatialSkeletonNodeFilter( SpatialSkeletonNodeFilterType.LEAF, diff --git a/src/skeleton/node_types.ts b/src/skeleton/node_types.ts index 752a5b186..15ff5e0dc 100644 --- a/src/skeleton/node_types.ts +++ b/src/skeleton/node_types.ts @@ -24,6 +24,7 @@ export enum SpatialSkeletonDisplayNodeType { } export enum SpatialSkeletonNodeFilterType { + DEFAULT, NONE, LEAF, VIRTUAL_END, @@ -55,6 +56,8 @@ export function getSpatialSkeletonNodeFilterLabel( filterType: SpatialSkeletonNodeFilterType, ) { switch (filterType) { + case SpatialSkeletonNodeFilterType.DEFAULT: + return "Default"; case SpatialSkeletonNodeFilterType.NONE: return "None"; case SpatialSkeletonNodeFilterType.LEAF: @@ -78,6 +81,7 @@ export function matchesSpatialSkeletonNodeFilter( }, ) { switch (filterType) { + case SpatialSkeletonNodeFilterType.DEFAULT: case SpatialSkeletonNodeFilterType.NONE: return true; case SpatialSkeletonNodeFilterType.LEAF: diff --git a/src/ui/skeleton_tab.spec.ts b/src/ui/skeleton_tab.spec.ts index 3d272a4be..8bf0a3298 100644 --- a/src/ui/skeleton_tab.spec.ts +++ b/src/ui/skeleton_tab.spec.ts @@ -121,7 +121,7 @@ describe("spatial skeleton edit tab render state", () => { const state = buildSpatialSkeletonSegmentRenderState(20380, graph, { filterText: "", - nodeFilterType: SpatialSkeletonNodeFilterType.NONE, + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, getNodeDescription() { return undefined; }, @@ -133,6 +133,26 @@ describe("spatial skeleton edit tab render state", () => { expect(state.rows.map((row) => row.node.nodeId)).toEqual([10, 12]); }); + it("shows all nodes including regular chain nodes when filter is None", () => { + const graph = buildSpatiallyIndexedSkeletonNavigationGraph([ + makeNode(10, undefined), + makeNode(11, 10), + makeNode(12, 11), + ]); + + const state = buildSpatialSkeletonSegmentRenderState(20380, graph, { + filterText: "", + nodeFilterType: SpatialSkeletonNodeFilterType.NONE, + getNodeDescription() { + return undefined; + }, + }); + + expect(state.matchedNodeCount).toBe(3); + expect(state.displayedNodeCount).toBe(3); + expect(state.rows.map((row) => row.node.nodeId)).toEqual([10, 11, 12]); + }); + it("treats node-type-only matches as disconnected visible branches", () => { const graph = buildSpatiallyIndexedSkeletonNavigationGraph([ makeNode(20, undefined), @@ -198,7 +218,7 @@ describe("spatial skeleton edit tab virtual list items", () => { const segmentState = { ...buildSpatialSkeletonSegmentRenderState(20380, graph, { filterText: "", - nodeFilterType: SpatialSkeletonNodeFilterType.NONE, + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, getNodeDescription() { return undefined; }, diff --git a/src/ui/skeleton_tab.ts b/src/ui/skeleton_tab.ts index 02bd2e974..674aecd02 100644 --- a/src/ui/skeleton_tab.ts +++ b/src/ui/skeleton_tab.ts @@ -1428,7 +1428,8 @@ export class SpatialSkeletonEditTab extends Tab { segmentState === undefined || segmentState.totalNodeCount === 0 || (getFilterText().length === 0 && - nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.NONE) + (nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.DEFAULT || + nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.NONE)) ) { return "No loaded nodes."; } diff --git a/src/ui/skeleton_tab_render.ts b/src/ui/skeleton_tab_render.ts index 2bc7af8b4..8032f0eeb 100644 --- a/src/ui/skeleton_tab_render.ts +++ b/src/ui/skeleton_tab_render.ts @@ -109,14 +109,12 @@ export function buildSpatialSkeletonSegmentRenderState( if (info === undefined) return false; const { node, type, isLeaf, description } = info; return ( - (options.nodeFilterType === SpatialSkeletonNodeFilterType.NONE || - matchesSpatialSkeletonNodeFilter(options.nodeFilterType, { - isLeaf, - nodeHasDescription: hasNonEmptyNodeDescription(description), - nodeIsTrueEnd: node.isTrueEnd ?? false, - nodeType: type, - })) && - nodeMatchesFilter(node, options.filterText, description) + matchesSpatialSkeletonNodeFilter(options.nodeFilterType, { + isLeaf, + nodeHasDescription: hasNonEmptyNodeDescription(description), + nodeIsTrueEnd: node.isTrueEnd ?? false, + nodeType: type, + }) && nodeMatchesFilter(node, options.filterText, description) ); }; @@ -147,11 +145,12 @@ export function buildSpatialSkeletonSegmentRenderState( } // Stage 2: among matched nodes, plain regular chain nodes are collapsed from the - // display list when no filter is active. An active filter means the user explicitly - // searched for something, so every match should be shown. + // display list under the Default filter with no search text. Any other filter + // or active search text means the user explicitly narrowed the view, so every + // match is shown including regular chain nodes. const hasActiveFilter = options.filterText.length > 0 || - options.nodeFilterType !== SpatialSkeletonNodeFilterType.NONE; + options.nodeFilterType !== SpatialSkeletonNodeFilterType.DEFAULT; const isCollapsedFromDisplay = (nodeId: number): boolean => { if (hasActiveFilter) return false;