Skip to content

Commit b5f6d38

Browse files
author
DavidQ
committed
Add 3D scene graph inspector panel
1 parent 30108c1 commit b5f6d38

10 files changed

Lines changed: 249 additions & 9 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
3-
COMMAND: Implement BUILD_PR_LEVEL_17_19_COLLISION_OVERLAYS with minimal provider + panel + wiring + tests. Package to <project folder>/tmp/BUILD_PR_LEVEL_17_19_COLLISION_OVERLAYS.zip
3+
COMMAND: Implement BUILD_PR_LEVEL_17_20_SCENE_GRAPH_INSPECTOR as minimal provider + panel + wiring + tests. Package to <project folder>/tmp/BUILD_PR_LEVEL_17_20_SCENE_GRAPH_INSPECTOR.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add 3D collision overlay debug panel
1+
Add 3D scene graph inspector panel
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
- [ ] overlay renders
1+
- [ ] hierarchy renders
22
- [ ] no crashes
3-
- [ ] camera panel unaffected
3+
- [ ] prior panels unaffected
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# BUILD_PR_LEVEL_17_20_SCENE_GRAPH_INSPECTOR
2+
3+
Implement:
4+
- provider: scene graph nodes
5+
- panel: hierarchy list/tree
6+
- minimal wiring
7+
- tests
8+
9+
Constraints:
10+
- read-only
11+
- no engine changes beyond exposure
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# PLAN_PR_LEVEL_17_20_SCENE_GRAPH_INSPECTOR
2+
3+
Purpose:
4+
Add minimal 3D scene graph inspector panel.
5+
6+
Scope:
7+
- provider (read-only)
8+
- panel (hierarchy view)
9+
- minimal wiring
10+
- testable
11+
12+
Out of scope:
13+
- editing/mutation
14+
- performance optimization
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
panel3dSceneGraphInspector.js
6+
*/
7+
8+
import {
9+
createPanelDescriptor,
10+
toLinePair
11+
} from "../shared/threeDDebugUtils.js";
12+
13+
export const PANEL_3D_SCENE_GRAPH_INSPECTOR = "3d.sceneGraph";
14+
15+
function toNodeLine(node, index) {
16+
const indent = node.depth > 0 ? `${".".repeat(node.depth)} ` : "";
17+
const label = `${indent}${node.nodeId}`;
18+
return toLinePair(
19+
`node.${index + 1}`,
20+
`${label}|parent=${node.parentId}|children=${node.childCount}|active=${node.active === true}`
21+
);
22+
}
23+
24+
export function create3dSceneGraphInspectorPanel(provider, options = {}) {
25+
return createPanelDescriptor({
26+
id: PANEL_3D_SCENE_GRAPH_INSPECTOR,
27+
title: "3D Scene Graph Inspector",
28+
provider,
29+
priority: options.priority ?? 1140,
30+
enabled: options.enabled === true,
31+
linesBuilder(snapshot = {}) {
32+
const nodeRows = Array.isArray(snapshot.nodeRows) ? snapshot.nodeRows : [];
33+
const baseLines = [
34+
toLinePair("nodeCount", snapshot.nodeCount),
35+
toLinePair("rootCount", snapshot.rootCount),
36+
toLinePair("maxDepth", snapshot.maxDepth)
37+
];
38+
39+
if (nodeRows.length === 0) {
40+
return [
41+
...baseLines,
42+
toLinePair("nodes", "none")
43+
];
44+
}
45+
46+
return [
47+
...baseLines,
48+
...nodeRows.map((node, index) => toNodeLine(node, index))
49+
];
50+
}
51+
});
52+
}

src/engine/debug/standard/threeD/panels/registerStandard3dPanels.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ registerStandard3dPanels.js
88
import { PROVIDER_3D_CAMERA_SUMMARY } from "../providers/cameraSummaryProvider.js";
99
import { PROVIDER_3D_COLLISION_OVERLAYS } from "../providers/collisionOverlaysProvider.js";
1010
import { PROVIDER_3D_RENDER_PIPELINE_STAGES } from "../providers/renderPipelineStagesProvider.js";
11-
import { PROVIDER_3D_SCENE_GRAPH_SUMMARY } from "../providers/sceneGraphSummaryProvider.js";
11+
import { PROVIDER_3D_SCENE_GRAPH_INSPECTOR } from "../providers/sceneGraphInspectorProvider.js";
1212
import { PROVIDER_3D_TRANSFORM_SUMMARY } from "../providers/transformSummaryProvider.js";
1313
import { create3dCameraPanel } from "./panel3dCamera.js";
1414
import { create3dCollisionOverlaysPanel } from "./panel3dCollisionOverlays.js";
1515
import { create3dRenderPipelineStagesPanel } from "./panel3dRenderPipelineStages.js";
16-
import { create3dSceneGraphPanel } from "./panel3dSceneGraph.js";
16+
import { create3dSceneGraphInspectorPanel } from "./panel3dSceneGraphInspector.js";
1717
import { create3dTransformPanel } from "./panel3dTransform.js";
1818

1919
function pickProvider(providerMap, providerId) {
@@ -40,7 +40,7 @@ export function createStandard3dPanels(options = {}) {
4040
const collision = create3dCollisionOverlaysPanel(pickProvider(providerMap, PROVIDER_3D_COLLISION_OVERLAYS), {
4141
enabled: options.enabled === true
4242
});
43-
const sceneGraph = create3dSceneGraphPanel(pickProvider(providerMap, PROVIDER_3D_SCENE_GRAPH_SUMMARY), {
43+
const sceneGraph = create3dSceneGraphInspectorPanel(pickProvider(providerMap, PROVIDER_3D_SCENE_GRAPH_INSPECTOR), {
4444
enabled: options.enabled === true
4545
});
4646

src/engine/debug/standard/threeD/providers/registerStandard3dProviders.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ registerStandard3dProviders.js
88
import { createCameraSummaryProvider } from "./cameraSummaryProvider.js";
99
import { createCollisionOverlaysProvider } from "./collisionOverlaysProvider.js";
1010
import { createRenderPipelineStagesProvider } from "./renderPipelineStagesProvider.js";
11-
import { createSceneGraphSummaryProvider } from "./sceneGraphSummaryProvider.js";
11+
import { createSceneGraphInspectorProvider } from "./sceneGraphInspectorProvider.js";
1212
import { createTransformSummaryProvider } from "./transformSummaryProvider.js";
1313

1414
export function createStandard3dProviders(options = {}) {
@@ -18,7 +18,7 @@ export function createStandard3dProviders(options = {}) {
1818
createCameraSummaryProvider(adapters),
1919
createRenderPipelineStagesProvider(adapters),
2020
createCollisionOverlaysProvider(adapters),
21-
createSceneGraphSummaryProvider(adapters)
21+
createSceneGraphInspectorProvider(adapters)
2222
];
2323

2424
return {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
sceneGraphInspectorProvider.js
6+
*/
7+
8+
import {
9+
asArray,
10+
asNonNegativeInteger,
11+
asObject,
12+
createProvider,
13+
getAdapter,
14+
sanitizeText
15+
} from "../shared/threeDDebugUtils.js";
16+
17+
export const PROVIDER_3D_SCENE_GRAPH_INSPECTOR = "provider.3d.sceneGraphInspector.snapshot";
18+
19+
function toBoolean(value, fallback = false) {
20+
if (typeof value === "boolean") {
21+
return value;
22+
}
23+
return fallback;
24+
}
25+
26+
function toNodeRow(rawNode, index) {
27+
if (typeof rawNode === "string") {
28+
const nodeId = sanitizeText(rawNode) || `node-${index + 1}`;
29+
return {
30+
nodeId,
31+
parentId: "none",
32+
depth: 0,
33+
childCount: 0,
34+
active: true,
35+
order: index
36+
};
37+
}
38+
39+
const source = asObject(rawNode);
40+
const nodeId = sanitizeText(source.nodeId) || sanitizeText(source.id) || `node-${index + 1}`;
41+
const parentId = sanitizeText(source.parentId) || sanitizeText(source.parent) || "none";
42+
const depth = asNonNegativeInteger(source.depth, 0);
43+
const children = asArray(source.children);
44+
const childCount = asNonNegativeInteger(source.childCount, children.length);
45+
const active = toBoolean(source.active, true);
46+
const order = asNonNegativeInteger(source.order, index);
47+
48+
return {
49+
nodeId,
50+
parentId,
51+
depth,
52+
childCount,
53+
active,
54+
order
55+
};
56+
}
57+
58+
function byDeterministicOrder(left, right) {
59+
if (left.order !== right.order) {
60+
return left.order - right.order;
61+
}
62+
if (left.depth !== right.depth) {
63+
return left.depth - right.depth;
64+
}
65+
return left.nodeId.localeCompare(right.nodeId);
66+
}
67+
68+
function readSceneGraphInspector(raw) {
69+
const source = asObject(raw);
70+
const rawRows = Array.isArray(source.nodeRows)
71+
? source.nodeRows
72+
: Array.isArray(source.nodes)
73+
? source.nodes
74+
: asArray(source.rootNodeIds);
75+
76+
const nodeRows = rawRows
77+
.map((node, index) => toNodeRow(node, index))
78+
.sort(byDeterministicOrder);
79+
80+
return {
81+
nodeRows,
82+
nodeCount: nodeRows.length,
83+
rootCount: nodeRows.filter((node) => node.depth === 0 || node.parentId === "none").length,
84+
maxDepth: nodeRows.reduce((maxDepth, node) => Math.max(maxDepth, node.depth), 0)
85+
};
86+
}
87+
88+
export function createSceneGraphInspectorProvider(adapters = {}) {
89+
const adapter = getAdapter(adapters, "sceneGraphInspector");
90+
return createProvider(
91+
PROVIDER_3D_SCENE_GRAPH_INSPECTOR,
92+
"3D Scene Graph Inspector",
93+
(context = {}) => {
94+
const source = adapter ? adapter(context) : asObject(context?.threeD?.sceneGraphInspector);
95+
return readSceneGraphInspector(source);
96+
}
97+
);
98+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
SceneGraphInspectorDebugPanel.test.mjs
6+
*/
7+
import assert from "node:assert/strict";
8+
import { createStandard3dPanels } from "../../src/engine/debug/standard/threeD/panels/registerStandard3dPanels.js";
9+
import { PANEL_3D_SCENE_GRAPH_INSPECTOR, create3dSceneGraphInspectorPanel } from "../../src/engine/debug/standard/threeD/panels/panel3dSceneGraphInspector.js";
10+
import { createStandard3dProviders } from "../../src/engine/debug/standard/threeD/providers/registerStandard3dProviders.js";
11+
import { PROVIDER_3D_SCENE_GRAPH_INSPECTOR, createSceneGraphInspectorProvider } from "../../src/engine/debug/standard/threeD/providers/sceneGraphInspectorProvider.js";
12+
13+
export async function run() {
14+
const provider = createSceneGraphInspectorProvider({
15+
sceneGraphInspector: () => ({
16+
nodes: [
17+
{ nodeId: "player", parentId: "worldRoot", depth: 1, childCount: 2, active: true, order: 2 },
18+
{ nodeId: "worldRoot", depth: 0, childCount: 3, active: true, order: 0 },
19+
{ nodeId: "uiRoot", depth: 0, childCount: 1, active: true, order: 1 },
20+
{ nodeId: "pauseMenu", parentId: "uiRoot", depth: 1, childCount: 0, active: false, order: 3 }
21+
]
22+
})
23+
});
24+
25+
const snapshot = provider.getSnapshot({});
26+
assert.equal(snapshot.nodeCount, 4);
27+
assert.equal(snapshot.rootCount, 2);
28+
assert.equal(snapshot.maxDepth, 1);
29+
assert.deepEqual(
30+
snapshot.nodeRows.map((row) => row.nodeId),
31+
["worldRoot", "uiRoot", "player", "pauseMenu"]
32+
);
33+
34+
const panel = create3dSceneGraphInspectorPanel(provider, { enabled: true });
35+
const render = panel.render({}, {});
36+
assert.equal(render.id, PANEL_3D_SCENE_GRAPH_INSPECTOR);
37+
assert.equal(render.title, "3D Scene Graph Inspector");
38+
assert.deepEqual(render.lines, [
39+
"nodeCount=4",
40+
"rootCount=2",
41+
"maxDepth=1",
42+
"node.1=worldRoot|parent=none|children=3|active=true",
43+
"node.2=uiRoot|parent=none|children=1|active=true",
44+
"node.3=. player|parent=worldRoot|children=2|active=true",
45+
"node.4=. pauseMenu|parent=uiRoot|children=0|active=false"
46+
]);
47+
48+
const fallbackProvider = createSceneGraphInspectorProvider({
49+
sceneGraphInspector: () => ({})
50+
});
51+
const fallbackPanel = create3dSceneGraphInspectorPanel(fallbackProvider, { enabled: true });
52+
const fallbackRender = fallbackPanel.render({}, {});
53+
assert.deepEqual(fallbackRender.lines, [
54+
"nodeCount=0",
55+
"rootCount=0",
56+
"maxDepth=0",
57+
"nodes=none"
58+
]);
59+
60+
const { providerMap } = createStandard3dProviders();
61+
assert.equal(providerMap.has(PROVIDER_3D_SCENE_GRAPH_INSPECTOR), true);
62+
const registeredPanels = createStandard3dPanels({ providerMap, enabled: false });
63+
const panelIds = registeredPanels.map((entry) => entry.id);
64+
assert.equal(panelIds.includes(PANEL_3D_SCENE_GRAPH_INSPECTOR), true);
65+
}

0 commit comments

Comments
 (0)