From 50a51037ee4b2f48e2a07875893baa0db4946401 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 10 Jun 2026 22:59:55 +0200 Subject: [PATCH] feat: add NONE filter and make old filter default --- python/neuroglancer/viewer_state.py | 1 + src/layer/segmentation/index.ts | 2 +- src/skeleton/node_types.spec.ts | 14 ++++++++++++++ src/skeleton/node_types.ts | 4 ++++ src/ui/skeleton_tab.spec.ts | 24 ++++++++++++++++++++++-- src/ui/skeleton_tab.ts | 3 ++- src/ui/skeleton_tab_render.ts | 21 ++++++++++----------- 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/python/neuroglancer/viewer_state.py b/python/neuroglancer/viewer_state.py index 246e4ea931..63ff7c6306 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 4cf4fb3a00..63ed4c8c24 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 8f672b6c85..f76adfc54d 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 752a5b1862..15ff5e0dc6 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 3d272a4beb..8bf0a3298a 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 02bd2e974c..674aecd022 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 2bc7af8b4a..8032f0eeb2 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;