Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion src/ui/skeleton_tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function makeNode(
};
}

async function getBuildSpatialSkeletonVirtualListItems() {
function stubWebGLContext() {
const webglContextStub = new Proxy(
{},
{
Expand All @@ -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([
Expand Down Expand Up @@ -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.");
});
});
63 changes: 45 additions & 18 deletions src/ui/skeleton_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -369,6 +402,7 @@ export class SpatialSkeletonEditTab extends Tab {

let allNodes: SpatiallyIndexedSkeletonNode[] = [];
let activeSegmentId: number | undefined;
let selectedSegmentNotVisible = false;
let nodesBySegment = new Map<number, SpatiallyIndexedSkeletonNode[]>();
let inspectionAllowed = false;
let navigationAllowed = false;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1610,6 +1634,9 @@ export class SpatialSkeletonEditTab extends Tab {
cachedSelectedSegmentNodes === undefined
? undefined
: selectedSegmentId;
selectedSegmentNotVisible =
selectedSegmentId !== undefined &&
cachedSelectedSegmentNodes === undefined;
loadedNodeSummarySuffix = "";
if (
skeletonLayer === undefined ||
Expand Down