Skip to content

Commit 7104065

Browse files
author
DavidQ
committed
Introduce minimal 3D camera foundation<BUILD_PR_LEVEL_17_3_CAMERA_FOUNDATION>
1 parent 2cf2cd7 commit 7104065

11 files changed

Lines changed: 134 additions & 9 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ MODEL: GPT-5.3-codex
22
REASONING: high
33

44
COMMAND:
5-
Implement isolated 3D scene boot:
6-
- add 3D scene initializer
7-
- hook render pipeline safely
8-
- do not modify existing 2D paths
9-
- validate all 2D samples still run
5+
Implement minimal 3D camera:
6+
- create camera state (position, rotation)
7+
- integrate into scene safely
8+
- ensure no impact to 2D camera
9+
- validate all samples still pass

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD_PR_LEVEL_17_2_RENDER_SCENE_BOOT_VALIDATION - safe 3D scene boot + render hook
1+
Introduce minimal 3D camera foundation<BUILD_PR_LEVEL_17_3_CAMERA_FOUNDATION>

docs/dev/reports/launch_smoke_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Launch Smoke Report
22

3-
Generated: 2026-04-15T19:00:55.207Z
3+
Generated: 2026-04-15T19:11:35.746Z
44

55
Filters: games=false, samples=true, tools=false, sampleRange=all
66

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Post-run: confirm 2D stability + 3D boot success
1+
Post-run: verify camera init + no regression
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# BUILD PR: 17.3 Camera Foundation (3D Safe Introduction)
2+
3+
## Purpose
4+
Introduce a minimal 3D camera system without impacting 2D camera.
5+
6+
## Scope
7+
- Add 3D camera model (position, rotation)
8+
- Non-invasive integration point
9+
- No modification to 2D camera
10+
11+
## Testability
12+
- 2D camera unaffected
13+
- 3D camera can initialize and update safely
14+
15+
## Acceptance
16+
- [ ] Engine boot passes
17+
- [ ] 2D camera unchanged
18+
- [ ] 3D camera initializes and updates

src/engine/camera/Camera3D.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
Camera3D.js
6+
*/
7+
function toVector3(value, fallback) {
8+
if (!value || typeof value !== 'object') {
9+
return { ...fallback };
10+
}
11+
12+
return {
13+
x: Number.isFinite(value.x) ? value.x : fallback.x,
14+
y: Number.isFinite(value.y) ? value.y : fallback.y,
15+
z: Number.isFinite(value.z) ? value.z : fallback.z,
16+
};
17+
}
18+
19+
export default class Camera3D {
20+
constructor({ position = null, rotation = null } = {}) {
21+
this.position = toVector3(position, { x: 0, y: 0, z: 0 });
22+
this.rotation = toVector3(rotation, { x: 0, y: 0, z: 0 });
23+
}
24+
25+
setPosition(position = {}) {
26+
this.position = toVector3(position, this.position);
27+
return this.getState();
28+
}
29+
30+
setRotation(rotation = {}) {
31+
this.rotation = toVector3(rotation, this.rotation);
32+
return this.getState();
33+
}
34+
35+
translate({ x = 0, y = 0, z = 0 } = {}) {
36+
this.position.x += Number.isFinite(x) ? x : 0;
37+
this.position.y += Number.isFinite(y) ? y : 0;
38+
this.position.z += Number.isFinite(z) ? z : 0;
39+
return this.getState();
40+
}
41+
42+
rotate({ x = 0, y = 0, z = 0 } = {}) {
43+
this.rotation.x += Number.isFinite(x) ? x : 0;
44+
this.rotation.y += Number.isFinite(y) ? y : 0;
45+
this.rotation.z += Number.isFinite(z) ? z : 0;
46+
return this.getState();
47+
}
48+
49+
getState() {
50+
return {
51+
position: { ...this.position },
52+
rotation: { ...this.rotation },
53+
};
54+
}
55+
}

src/engine/camera/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ David Quesenberry
55
index.js
66
*/
77
export { default as Camera2D } from './Camera2D.js';
8+
export { default as Camera3D } from './Camera3D.js';
89
export { followCameraTarget, worldRectToScreen } from './CameraSystem.js';
910
export { updateZoneCamera } from './ZoneCameraSystem.js';

src/engine/core/Engine.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import RuntimeMetrics from './RuntimeMetrics.js';
99
import FrameClock from './FrameClock.js';
1010
import FixedTicker from './FixedTicker.js';
1111
import EventBus from '../events/EventBus.js';
12+
import { Camera3D } from '../camera/index.js';
1213
import { backgroundImage, fullscreenBezel, FullscreenService, resolvePreferredFullscreenTarget } from '../runtime/index.js';
1314
import { AudioService } from '../audio/index.js';
1415
import { Logger } from '../logging/index.js';
@@ -30,6 +31,7 @@ export default class Engine {
3031
fullscreenBezelLayer = null,
3132
audio = null,
3233
logger = null,
34+
camera3D = null,
3335
} = {}) {
3436
if (!canvas) {
3537
throw new Error('Engine requires a canvas.');
@@ -68,6 +70,7 @@ export default class Engine {
6870
});
6971
this.audio = audio || new AudioService();
7072
this.logger = logger || new Logger({ channel: 'engine' });
73+
this.camera3D = camera3D || new Camera3D();
7174
this.settings = new SettingsSystem({
7275
namespace: 'toolboxaid:engine-settings',
7376
defaults: {
@@ -97,12 +100,42 @@ export default class Engine {
97100
}
98101

99102
this.scene = scene;
103+
this.attachScene3DCamera(this.scene);
100104

101105
if (this.scene && typeof this.scene.enter === 'function') {
102106
this.scene.enter(this);
103107
}
104108
}
105109

110+
attachScene3DCamera(scene) {
111+
if (!scene || !this.camera3D) {
112+
return;
113+
}
114+
115+
if (typeof scene.setCamera3D === 'function') {
116+
try {
117+
scene.setCamera3D(this.camera3D, this);
118+
} catch (error) {
119+
this.logger?.warn?.('Engine scene setCamera3D hook failed.', {
120+
error: error?.message || String(error),
121+
});
122+
}
123+
return;
124+
}
125+
126+
if (scene.camera3D !== undefined && scene.camera3D !== null) {
127+
return;
128+
}
129+
130+
try {
131+
scene.camera3D = this.camera3D;
132+
} catch (error) {
133+
this.logger?.warn?.('Engine scene camera3D assignment failed.', {
134+
error: error?.message || String(error),
135+
});
136+
}
137+
}
138+
106139
start() {
107140
if (this.input && typeof this.input.attach === 'function') {
108141
this.input.attach();

src/engine/core/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export { default as RuntimeMetrics } from './RuntimeMetrics.js';
1111

1212
// Baseline core service cluster exports (timing/frame, event routing, camera integration).
1313
export { EventBus } from '../events/index.js';
14-
export { Camera2D, followCameraTarget, worldRectToScreen, updateZoneCamera } from '../camera/index.js';
14+
export { Camera2D, Camera3D, followCameraTarget, worldRectToScreen, updateZoneCamera } from '../camera/index.js';

tests/core/EngineCoreBoundaryBaseline.test.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ export function run() {
6868
assert.equal(typeof rect.y, 'number');
6969
assert.equal(rect.width, 10);
7070
assert.equal(rect.height, 10);
71+
72+
const camera3D = new core.Camera3D({
73+
position: { x: 1, y: 2, z: 3 },
74+
rotation: { x: 0.1, y: 0.2, z: 0.3 },
75+
});
76+
camera3D.translate({ x: 2, y: -1, z: 5 });
77+
camera3D.rotate({ y: 0.5 });
78+
const camera3DState = camera3D.getState();
79+
assert.equal(camera3DState.position.x, 3);
80+
assert.equal(camera3DState.position.y, 1);
81+
assert.equal(camera3DState.position.z, 8);
82+
assert.equal(camera3DState.rotation.x, 0.1);
83+
assert.equal(camera3DState.rotation.y, 0.7);
84+
assert.equal(camera3DState.rotation.z, 0.3);
7185
}
7286

7387
if (import.meta.url === `file://${process.argv[1]}`) {

0 commit comments

Comments
 (0)