Skip to content

Commit 30108c1

Browse files
author
DavidQ
committed
Add 3D collision overlay debug panel
1 parent 5c054f6 commit 30108c1

10 files changed

Lines changed: 240 additions & 27 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: Create BUILD_PR_LEVEL_17_18_RENDER_PIPELINE_STAGES as a docs-first, repo-structured delta. Use docs/pr/PLAN_PR_LEVEL_17_18_RENDER_PIPELINE_STAGES.md and docs/pr/BUILD_PR_LEVEL_17_18_RENDER_PIPELINE_STAGES.md as the source of truth. Implement the smallest valid read-only 3D render pipeline stages panel under src/engine/debug/standard/threeD by adding one provider, one panel, minimal registration wiring, and targeted test coverage only. Do not expand into collision overlays, scene graph inspection, transform inspector expansion, roadmap edits, start_of_day changes, or unrelated cleanup. Preserve existing camera debug panel behavior. Final step: package only this PR's created and modified files into <project folder>/tmp/BUILD_PR_LEVEL_17_18_RENDER_PIPELINE_STAGES.zip with exact repo-relative structure.
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

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
Add scoped 3D render-pipeline stages debug panel BUILD bundle
2-
3-
- define the next Track H 3D debug-support step after camera debug panel
4-
- constrain implementation to a read-only provider + panel + minimal wiring + tests
5-
- preserve current 3D debug behavior while enabling the next validation-backed APPLY step
1+
Add 3D collision overlay debug panel
Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
BUILD_PR_LEVEL_17_18_RENDER_PIPELINE_STAGES VALIDATION CHECKLIST
2-
3-
Automated
4-
- [ ] targeted test file passes
5-
- [ ] provider normalization test covers empty and populated input
6-
- [ ] panel render test covers fallback state
7-
- [ ] panel registration visibility is verified if applicable
8-
9-
Manual
10-
- [ ] app launches without new console errors
11-
- [ ] existing 3D debug surface still opens
12-
- [ ] render pipeline stages panel is visible
13-
- [ ] stage rows display in deterministic order
14-
- [ ] empty/no-data state renders safely
15-
- [ ] camera debug panel still renders correctly
16-
- [ ] no collision overlay or scene graph UI appears from this PR
1+
- [ ] overlay renders
2+
- [ ] no crashes
3+
- [ ] camera panel unaffected
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# BUILD_PR_LEVEL_17_19_COLLISION_OVERLAYS
2+
3+
Implement:
4+
- provider: collision overlay data
5+
- panel: render overlay
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_19_COLLISION_OVERLAYS
2+
3+
Purpose:
4+
Add minimal 3D collision overlay debug visualization.
5+
6+
Scope:
7+
- one provider
8+
- one panel
9+
- minimal wiring
10+
- testable overlay rendering
11+
12+
Out of scope:
13+
- physics rewrite
14+
- scene graph inspector
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
panel3dCollisionOverlays.js
6+
*/
7+
8+
import {
9+
createPanelDescriptor,
10+
toLinePair
11+
} from "../shared/threeDDebugUtils.js";
12+
13+
export const PANEL_3D_COLLISION_OVERLAYS = "3d.collision";
14+
15+
function toOverlayLine(overlay, index) {
16+
return toLinePair(
17+
`overlay.${index + 1}`,
18+
`${overlay.overlayId}|${overlay.kind}|${overlay.state}|enabled=${overlay.enabled === true}`
19+
);
20+
}
21+
22+
export function create3dCollisionOverlaysPanel(provider, options = {}) {
23+
return createPanelDescriptor({
24+
id: PANEL_3D_COLLISION_OVERLAYS,
25+
title: "3D Collision Overlays",
26+
provider,
27+
priority: options.priority ?? 1130,
28+
enabled: options.enabled === true,
29+
linesBuilder(snapshot = {}) {
30+
const overlayRows = Array.isArray(snapshot.overlayRows) ? snapshot.overlayRows : [];
31+
const baseLines = [
32+
toLinePair("overlayCount", snapshot.overlayCount),
33+
toLinePair("activeCount", snapshot.activeCount)
34+
];
35+
36+
if (overlayRows.length === 0) {
37+
return [
38+
...baseLines,
39+
toLinePair("overlays", "none")
40+
];
41+
}
42+
43+
return [
44+
...baseLines,
45+
...overlayRows.map((overlay, index) => toOverlayLine(overlay, index))
46+
];
47+
}
48+
});
49+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ registerStandard3dPanels.js
66
*/
77

88
import { PROVIDER_3D_CAMERA_SUMMARY } from "../providers/cameraSummaryProvider.js";
9-
import { PROVIDER_3D_COLLISION_SUMMARY } from "../providers/collisionSummaryProvider.js";
9+
import { PROVIDER_3D_COLLISION_OVERLAYS } from "../providers/collisionOverlaysProvider.js";
1010
import { PROVIDER_3D_RENDER_PIPELINE_STAGES } from "../providers/renderPipelineStagesProvider.js";
1111
import { PROVIDER_3D_SCENE_GRAPH_SUMMARY } from "../providers/sceneGraphSummaryProvider.js";
1212
import { PROVIDER_3D_TRANSFORM_SUMMARY } from "../providers/transformSummaryProvider.js";
1313
import { create3dCameraPanel } from "./panel3dCamera.js";
14-
import { create3dCollisionPanel } from "./panel3dCollision.js";
14+
import { create3dCollisionOverlaysPanel } from "./panel3dCollisionOverlays.js";
1515
import { create3dRenderPipelineStagesPanel } from "./panel3dRenderPipelineStages.js";
1616
import { create3dSceneGraphPanel } from "./panel3dSceneGraph.js";
1717
import { create3dTransformPanel } from "./panel3dTransform.js";
@@ -37,7 +37,7 @@ export function createStandard3dPanels(options = {}) {
3737
const renderStages = create3dRenderPipelineStagesPanel(pickProvider(providerMap, PROVIDER_3D_RENDER_PIPELINE_STAGES), {
3838
enabled: options.enabled === true
3939
});
40-
const collision = create3dCollisionPanel(pickProvider(providerMap, PROVIDER_3D_COLLISION_SUMMARY), {
40+
const collision = create3dCollisionOverlaysPanel(pickProvider(providerMap, PROVIDER_3D_COLLISION_OVERLAYS), {
4141
enabled: options.enabled === true
4242
});
4343
const sceneGraph = create3dSceneGraphPanel(pickProvider(providerMap, PROVIDER_3D_SCENE_GRAPH_SUMMARY), {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
collisionOverlaysProvider.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_COLLISION_OVERLAYS = "provider.3d.collisionOverlays.snapshot";
18+
19+
function toBoolean(value, fallback = false) {
20+
if (typeof value === "boolean") {
21+
return value;
22+
}
23+
return fallback;
24+
}
25+
26+
function toOverlayRow(rawOverlay, index) {
27+
if (typeof rawOverlay === "string") {
28+
const overlayId = sanitizeText(rawOverlay) || `overlay-${index + 1}`;
29+
return {
30+
overlayId,
31+
kind: "bounds",
32+
state: "active",
33+
enabled: true,
34+
order: index
35+
};
36+
}
37+
38+
const source = asObject(rawOverlay);
39+
const overlayId = sanitizeText(source.overlayId) || sanitizeText(source.id) || `overlay-${index + 1}`;
40+
const kind = sanitizeText(source.kind) || sanitizeText(source.type) || "bounds";
41+
const state = sanitizeText(source.state) || sanitizeText(source.status) || "unknown";
42+
const enabled = toBoolean(source.enabled, state === "active");
43+
const order = asNonNegativeInteger(source.order, index);
44+
45+
return {
46+
overlayId,
47+
kind,
48+
state,
49+
enabled,
50+
order
51+
};
52+
}
53+
54+
function byDeterministicOrder(left, right) {
55+
if (left.order !== right.order) {
56+
return left.order - right.order;
57+
}
58+
return left.overlayId.localeCompare(right.overlayId);
59+
}
60+
61+
function readCollisionOverlays(raw) {
62+
const source = asObject(raw);
63+
const rawRows = Array.isArray(source.overlayRows)
64+
? source.overlayRows
65+
: Array.isArray(source.overlays)
66+
? source.overlays
67+
: asArray(source.activeOverlays);
68+
69+
const overlayRows = rawRows
70+
.map((overlay, index) => toOverlayRow(overlay, index))
71+
.sort(byDeterministicOrder);
72+
73+
return {
74+
overlayRows,
75+
overlayCount: overlayRows.length,
76+
activeCount: overlayRows.filter((overlay) => overlay.enabled === true).length
77+
};
78+
}
79+
80+
export function createCollisionOverlaysProvider(adapters = {}) {
81+
const adapter = getAdapter(adapters, "collisionOverlays");
82+
return createProvider(
83+
PROVIDER_3D_COLLISION_OVERLAYS,
84+
"3D Collision Overlays",
85+
(context = {}) => {
86+
const source = adapter ? adapter(context) : asObject(context?.threeD?.collisionOverlays);
87+
return readCollisionOverlays(source);
88+
}
89+
);
90+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ registerStandard3dProviders.js
66
*/
77

88
import { createCameraSummaryProvider } from "./cameraSummaryProvider.js";
9-
import { createCollisionSummaryProvider } from "./collisionSummaryProvider.js";
9+
import { createCollisionOverlaysProvider } from "./collisionOverlaysProvider.js";
1010
import { createRenderPipelineStagesProvider } from "./renderPipelineStagesProvider.js";
1111
import { createSceneGraphSummaryProvider } from "./sceneGraphSummaryProvider.js";
1212
import { createTransformSummaryProvider } from "./transformSummaryProvider.js";
@@ -17,7 +17,7 @@ export function createStandard3dProviders(options = {}) {
1717
createTransformSummaryProvider(adapters),
1818
createCameraSummaryProvider(adapters),
1919
createRenderPipelineStagesProvider(adapters),
20-
createCollisionSummaryProvider(adapters),
20+
createCollisionOverlaysProvider(adapters),
2121
createSceneGraphSummaryProvider(adapters)
2222
];
2323

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
CollisionOverlaysDebugPanel.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_COLLISION_OVERLAYS, create3dCollisionOverlaysPanel } from "../../src/engine/debug/standard/threeD/panels/panel3dCollisionOverlays.js";
10+
import { createStandard3dProviders } from "../../src/engine/debug/standard/threeD/providers/registerStandard3dProviders.js";
11+
import { PROVIDER_3D_COLLISION_OVERLAYS, createCollisionOverlaysProvider } from "../../src/engine/debug/standard/threeD/providers/collisionOverlaysProvider.js";
12+
13+
export async function run() {
14+
const provider = createCollisionOverlaysProvider({
15+
collisionOverlays: () => ({
16+
overlays: [
17+
{ overlayId: "contact-pairs", kind: "contacts", state: "active", enabled: true, order: 2 },
18+
{ overlayId: "aabb", kind: "bounds", state: "active", enabled: true, order: 1 },
19+
"triggers",
20+
{ overlayId: "sleeping", kind: "bounds", state: "inactive", enabled: false, order: 4 }
21+
]
22+
})
23+
});
24+
25+
const snapshot = provider.getSnapshot({});
26+
assert.equal(snapshot.overlayCount, 4);
27+
assert.equal(snapshot.activeCount, 3);
28+
assert.deepEqual(
29+
snapshot.overlayRows.map((row) => row.overlayId),
30+
["aabb", "contact-pairs", "triggers", "sleeping"]
31+
);
32+
assert.deepEqual(
33+
snapshot.overlayRows.map((row) => row.state),
34+
["active", "active", "active", "inactive"]
35+
);
36+
37+
const panel = create3dCollisionOverlaysPanel(provider, { enabled: true });
38+
const render = panel.render({}, {});
39+
assert.equal(render.id, PANEL_3D_COLLISION_OVERLAYS);
40+
assert.equal(render.title, "3D Collision Overlays");
41+
assert.deepEqual(render.lines, [
42+
"overlayCount=4",
43+
"activeCount=3",
44+
"overlay.1=aabb|bounds|active|enabled=true",
45+
"overlay.2=contact-pairs|contacts|active|enabled=true",
46+
"overlay.3=triggers|bounds|active|enabled=true",
47+
"overlay.4=sleeping|bounds|inactive|enabled=false"
48+
]);
49+
50+
const fallbackProvider = createCollisionOverlaysProvider({
51+
collisionOverlays: () => ({})
52+
});
53+
const fallbackPanel = create3dCollisionOverlaysPanel(fallbackProvider, { enabled: true });
54+
const fallbackRender = fallbackPanel.render({}, {});
55+
assert.deepEqual(fallbackRender.lines, [
56+
"overlayCount=0",
57+
"activeCount=0",
58+
"overlays=none"
59+
]);
60+
61+
const { providerMap } = createStandard3dProviders();
62+
assert.equal(providerMap.has(PROVIDER_3D_COLLISION_OVERLAYS), true);
63+
const registeredPanels = createStandard3dPanels({ providerMap, enabled: false });
64+
const panelIds = registeredPanels.map((entry) => entry.id);
65+
assert.equal(panelIds.includes(PANEL_3D_COLLISION_OVERLAYS), true);
66+
}

0 commit comments

Comments
 (0)