From 9f797efa32a699cded40b6103dba48bc058b51c2 Mon Sep 17 00:00:00 2001 From: Sean Martin Date: Wed, 1 Jul 2026 11:18:45 +0200 Subject: [PATCH] feat: message in skeleton tab if node from non vis --- src/ui/skeleton_tab.spec.ts | 83 ++++++++++++++++++++++++++++++++++++- src/ui/skeleton_tab.ts | 63 ++++++++++++++++++++-------- 2 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/ui/skeleton_tab.spec.ts b/src/ui/skeleton_tab.spec.ts index 8bf0a3298..4c2065aa8 100644 --- a/src/ui/skeleton_tab.spec.ts +++ b/src/ui/skeleton_tab.spec.ts @@ -39,7 +39,7 @@ function makeNode( }; } -async function getBuildSpatialSkeletonVirtualListItems() { +function stubWebGLContext() { const webglContextStub = new Proxy( {}, { @@ -49,10 +49,19 @@ async function getBuildSpatialSkeletonVirtualListItems() { ( globalThis as { WebGL2RenderingContext?: unknown } ).WebGL2RenderingContext ??= webglContextStub; +} + +async function getBuildSpatialSkeletonVirtualListItems() { + stubWebGLContext(); return (await import("#src/ui/skeleton_tab.js")) .buildSpatialSkeletonVirtualListItems; } +async function getSpatialSkeletonEmptyListText() { + stubWebGLContext(); + return (await import("#src/ui/skeleton_tab.js")).getSpatialSkeletonEmptyListText; +} + describe("spatial skeleton edit tab render state", () => { it("shows only directly matching nodes for text filtering", () => { const graph = buildSpatiallyIndexedSkeletonNavigationGraph([ @@ -293,3 +302,75 @@ describe("spatial skeleton edit tab virtual list items", () => { expect(flattened.listIndexByNodeId.get(leafCount + 1)).toBe(leafCount + 1); }); }); + +describe("spatial skeleton edit tab empty list text", () => { + it("tells the user to make the segment visible when a non-visible segment is selected", async () => { + const getEmptyListText = await getSpatialSkeletonEmptyListText(); + + const text = getEmptyListText({ + activeSegmentId: undefined, + selectedSegmentNotVisible: true, + segmentState: undefined, + filterText: "", + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, + }); + + expect(text).not.toBe("Select a skeleton segment to inspect editable nodes."); + expect(text).toMatch(/not visible|make it visible/i); + }); + + it("still asks the user to select a segment when nothing is selected at all", async () => { + const getEmptyListText = await getSpatialSkeletonEmptyListText(); + + const text = getEmptyListText({ + activeSegmentId: undefined, + selectedSegmentNotVisible: false, + segmentState: undefined, + filterText: "", + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, + }); + + expect(text).toBe("Select a skeleton segment to inspect editable nodes."); + }); + + it("reports no loaded nodes for an active segment with no cached nodes, regardless of visibility", async () => { + const getEmptyListText = await getSpatialSkeletonEmptyListText(); + + const text = getEmptyListText({ + activeSegmentId: 20380, + selectedSegmentNotVisible: false, + segmentState: undefined, + filterText: "", + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, + }); + + expect(text).toBe("No loaded nodes."); + }); + + it("reports no matching nodes when a filter excludes all loaded nodes", async () => { + const getEmptyListText = await getSpatialSkeletonEmptyListText(); + const graph = buildSpatiallyIndexedSkeletonNavigationGraph([ + makeNode(1, undefined), + ]); + const segmentState = { + ...buildSpatialSkeletonSegmentRenderState(20380, graph, { + filterText: "no-match", + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, + getNodeDescription() { + return undefined; + }, + }), + segmentLabel: undefined, + }; + + const text = getEmptyListText({ + activeSegmentId: 20380, + selectedSegmentNotVisible: false, + segmentState, + filterText: "no-match", + nodeFilterType: SpatialSkeletonNodeFilterType.DEFAULT, + }); + + expect(text).toBe("No matching nodes."); + }); +}); diff --git a/src/ui/skeleton_tab.ts b/src/ui/skeleton_tab.ts index 68c093e1e..a8cf213ed 100644 --- a/src/ui/skeleton_tab.ts +++ b/src/ui/skeleton_tab.ts @@ -113,6 +113,8 @@ const NO_NODE_SELECTED_MESSAGE = "No skeleton node is selected, only go to root is supported on skeleton edges."; const NAVIGATE_FROM_SPATIAL_INDEX_MESSAGE = "A non-visible segment is selected. Make it visible to use skeleton navigation features."; +const SEGMENT_NOT_VISIBLE_LIST_MESSAGE = + "A non-visible segment is selected. Make it visible to inspect its nodes here."; export type SegmentDisplayState = SpatialSkeletonSegmentRenderState & { segmentLabel: string | undefined; @@ -141,6 +143,37 @@ export function buildSpatialSkeletonVirtualListItems( return { items, listIndexByNodeId }; } +export function getSpatialSkeletonEmptyListText(options: { + activeSegmentId: number | undefined; + selectedSegmentNotVisible: boolean; + segmentState: SegmentDisplayState | undefined; + filterText: string; + nodeFilterType: SpatialSkeletonNodeFilterType; +}): string { + const { + activeSegmentId, + selectedSegmentNotVisible, + segmentState, + filterText, + nodeFilterType, + } = options; + if (activeSegmentId === undefined) { + return selectedSegmentNotVisible + ? SEGMENT_NOT_VISIBLE_LIST_MESSAGE + : "Select a skeleton segment to inspect editable nodes."; + } + if ( + segmentState === undefined || + segmentState.totalNodeCount === 0 || + (filterText.length === 0 && + (nodeFilterType === SpatialSkeletonNodeFilterType.DEFAULT || + nodeFilterType === SpatialSkeletonNodeFilterType.NONE)) + ) { + return "No loaded nodes."; + } + return "No matching nodes."; +} + interface SpatiallyIndexedSkeletonNavigationApi { getSkeletonRootNode( skeletonId: number, @@ -369,6 +402,7 @@ export class SpatialSkeletonEditTab extends Tab { let allNodes: SpatiallyIndexedSkeletonNode[] = []; let activeSegmentId: number | undefined; + let selectedSegmentNotVisible = false; let nodesBySegment = new Map(); let inspectionAllowed = false; let navigationAllowed = false; @@ -1515,24 +1549,14 @@ export class SpatialSkeletonEditTab extends Tab { } }; - const getEmptyListText = ( - segmentState: SegmentDisplayState | undefined, - ) => { - if (activeSegmentId === undefined) { - return "Select a skeleton segment to inspect editable nodes."; - } - if ( - segmentState === undefined || - segmentState.totalNodeCount === 0 || - (getFilterText().length === 0 && - (nodeFilterTypeModel.value === - SpatialSkeletonNodeFilterType.DEFAULT || - nodeFilterTypeModel.value === SpatialSkeletonNodeFilterType.NONE)) - ) { - return "No loaded nodes."; - } - return "No matching nodes."; - }; + const getEmptyListText = (segmentState: SegmentDisplayState | undefined) => + getSpatialSkeletonEmptyListText({ + activeSegmentId, + selectedSegmentNotVisible, + segmentState, + filterText: getFilterText(), + nodeFilterType: nodeFilterTypeModel.value, + }); const updateList = (segmentState: SegmentDisplayState | undefined) => { const flattened = buildSpatialSkeletonVirtualListItems( @@ -1610,6 +1634,9 @@ export class SpatialSkeletonEditTab extends Tab { cachedSelectedSegmentNodes === undefined ? undefined : selectedSegmentId; + selectedSegmentNotVisible = + selectedSegmentId !== undefined && + cachedSelectedSegmentNodes === undefined; loadedNodeSummarySuffix = ""; if ( skeletonLayer === undefined ||