@scenography/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,399 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let three = require("three");
3
+ //#region src/controls/boundaries.ts
4
+ /**
5
+ * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped
6
+ * independently so sliding along a wall still works when one axis is blocked.
7
+ */
8
+ const clampToBounds = (current, step, bound) => {
9
+ const next = new three.Vector3().addVectors(current, step);
10
+ if (next.x < -bound || next.x > bound) next.x = current.x;
11
+ if (next.z < -bound || next.z > bound) next.z = current.z;
12
+ return next;
13
+ };
14
+ //#endregion
15
+ //#region src/controls/headBob.ts
16
+ const createHeadBobState = () => ({
17
+ active: false,
18
+ timer: 0
19
+ });
20
+ /**
21
+ * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength
22
+ * steps so the bob always settles back to neutral when movement stops.
23
+ */
24
+ const updateHeadBob = (state, elapsed) => {
25
+ if (!state.active) return;
26
+ const wavelength = Math.PI;
27
+ const nextStepTime = (1 + Math.floor((state.timer + 1e-6) * 10 / wavelength)) * wavelength / 10;
28
+ state.timer = Math.min(state.timer + elapsed, nextStepTime);
29
+ if (state.timer === nextStepTime) state.active = false;
30
+ };
31
+ /** Vertical offset to add to the camera, in world units. */
32
+ const headBobOffset = (state) => Math.sin(state.timer * 10) * 1.5;
33
+ //#endregion
34
+ //#region src/controls/keyboard.ts
35
+ const createKeyboardState = () => ({});
36
+ const onKeyDown = (state, event) => {
37
+ state[event.code] = true;
38
+ };
39
+ const onKeyUp = (state, event) => {
40
+ state[event.code] = false;
41
+ };
42
+ /**
43
+ * Compute the per-frame translation from WASD input, oriented by the current
44
+ * yaw (`phi`). Holding Shift acts as a speed booster.
45
+ */
46
+ const movementFromKeyboard = (state, phi, delta) => {
47
+ const has = (code) => state[code] ? 1 : 0;
48
+ const forwardVelocity = has("KeyW") - has("KeyS");
49
+ const strafeVelocity = has("KeyA") - has("KeyD");
50
+ const factor = state["ShiftLeft"] || state["ShiftRight"] ? 30 : 10;
51
+ const yaw = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), phi);
52
+ const forward = new three.Vector3(0, 0, -10).applyQuaternion(yaw).multiplyScalar(forwardVelocity * delta * factor);
53
+ const left = new three.Vector3(-10, 0, 0).applyQuaternion(yaw).multiplyScalar(strafeVelocity * delta * factor);
54
+ return forward.add(left);
55
+ };
56
+ //#endregion
57
+ //#region src/controls/mouse.ts
58
+ const PHI_SPEED = 8;
59
+ const THETA_SPEED = 5;
60
+ const createMouseState = () => ({
61
+ dragging: false,
62
+ pos: new three.Vector2(),
63
+ prevPos: new three.Vector2(),
64
+ delta: new three.Vector2()
65
+ });
66
+ const clamp = (x, a, b) => Math.min(Math.max(x, a), b);
67
+ const onMouseDown = (state, event) => {
68
+ state.dragging = event.button === 0;
69
+ };
70
+ const onMouseUp = (state) => {
71
+ state.dragging = false;
72
+ };
73
+ const onMouseMove = (state, event) => {
74
+ state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);
75
+ state.delta.subVectors(state.pos, state.prevPos);
76
+ };
77
+ const commitMouseFrame = (state) => {
78
+ state.prevPos.copy(state.pos);
79
+ state.delta.set(0, 0);
80
+ };
81
+ /**
82
+ * Integrate the pointer delta into a look quaternion. Mutates `look` and
83
+ * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid
84
+ * flipping over.
85
+ */
86
+ const updateLookFromMouse = (mouse, look) => {
87
+ const xh = mouse.delta.x / window.innerWidth;
88
+ const yh = mouse.delta.y / window.innerHeight;
89
+ look.phi += -xh * PHI_SPEED;
90
+ look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);
91
+ const qy = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), look.phi);
92
+ const qx = new three.Quaternion().setFromAxisAngle(new three.Vector3(1, 0, 0), look.theta);
93
+ return new three.Quaternion().multiply(qy).multiply(qx);
94
+ };
95
+ //#endregion
96
+ //#region src/controls/FreeRoamControls.ts
97
+ /**
98
+ * First-person, free-roaming camera controls ported from the original Virtual
99
+ * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while
100
+ * moving, and square boundary clamping. The cardboard / device-orientation path
101
+ * was intentionally dropped.
102
+ *
103
+ * Framework-agnostic: construct it with a camera and the canvas element, call
104
+ * `attach()`, then `update(delta)` once per frame.
105
+ */
106
+ var FreeRoamControls = class {
107
+ bounds;
108
+ camera;
109
+ domElement;
110
+ mouse = createMouseState();
111
+ keyboard = createKeyboardState();
112
+ headBob = createHeadBobState();
113
+ look = {
114
+ phi: 0,
115
+ theta: 0
116
+ };
117
+ rotation = new three.Quaternion();
118
+ translation = new three.Vector3(0, 170, 0);
119
+ handlers = [];
120
+ constructor(camera, domElement, options = {}) {
121
+ this.camera = camera;
122
+ this.domElement = domElement;
123
+ this.bounds = options.bounds ?? 450;
124
+ if (options.position) this.translation.copy(options.position);
125
+ if (options.yaw !== void 0) this.look.phi = options.yaw;
126
+ this.rotation.setFromAxisAngle(new three.Vector3(0, 1, 0), this.look.phi);
127
+ }
128
+ attach() {
129
+ const bind = (target, type, listener) => {
130
+ target.addEventListener(type, listener);
131
+ this.handlers.push([
132
+ target,
133
+ type,
134
+ listener
135
+ ]);
136
+ };
137
+ bind(this.domElement, "mousedown", (e) => onMouseDown(this.mouse, e));
138
+ bind(window, "mousemove", (e) => onMouseMove(this.mouse, e));
139
+ bind(window, "mouseup", () => onMouseUp(this.mouse));
140
+ bind(window, "keydown", (e) => onKeyDown(this.keyboard, e));
141
+ bind(window, "keyup", (e) => onKeyUp(this.keyboard, e));
142
+ }
143
+ detach() {
144
+ for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);
145
+ this.handlers.length = 0;
146
+ }
147
+ /** Advance the camera by `delta` seconds of input. */
148
+ update(delta) {
149
+ if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);
150
+ const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);
151
+ this.translation = clampToBounds(this.translation, step, this.bounds);
152
+ if (step.x !== 0 || step.z !== 0) this.headBob.active = true;
153
+ updateHeadBob(this.headBob, delta);
154
+ this.camera.quaternion.copy(this.rotation);
155
+ this.camera.position.copy(this.translation);
156
+ this.camera.position.y += headBobOffset(this.headBob);
157
+ const target = new three.Vector3(0, 0, -1).applyQuaternion(this.rotation).multiplyScalar(100).add(this.translation);
158
+ this.camera.lookAt(target);
159
+ commitMouseFrame(this.mouse);
160
+ }
161
+ };
162
+ //#endregion
163
+ //#region src/builders/wall.ts
164
+ /** Build the three.js object that materializes a `Wall` definition. */
165
+ function buildWall(wall) {
166
+ const [width, height] = wall.size;
167
+ const mesh = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshStandardMaterial({
168
+ color: wall.color ?? "#ffffff",
169
+ side: three.DoubleSide
170
+ }));
171
+ mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);
172
+ mesh.rotation.y = wall.rotation ?? 0;
173
+ mesh.name = `wall:${wall.id}`;
174
+ mesh.userData.scenographyId = wall.id;
175
+ return mesh;
176
+ }
177
+ //#endregion
178
+ //#region src/builders/artwork.ts
179
+ const loader = new three.TextureLoader();
180
+ const FRAME_MARGIN = 4;
181
+ const SURFACE_OFFSET = 1;
182
+ /**
183
+ * Build the three.js object for an `Artwork`, placing it on its wall. The piece
184
+ * is positioned in the wall's local plane via `offset` and nudged forward along
185
+ * the wall normal so it sits in front of the surface.
186
+ */
187
+ function buildArtwork(artwork, wall) {
188
+ const [width, height] = artwork.size;
189
+ const [offsetX, offsetY] = artwork.offset ?? [0, 0];
190
+ const yaw = wall.rotation ?? 0;
191
+ const group = new three.Group();
192
+ const wallQuat = new three.Quaternion().setFromAxisAngle(new three.Vector3(0, 1, 0), yaw);
193
+ const right = new three.Vector3(1, 0, 0).applyQuaternion(wallQuat);
194
+ const normal = new three.Vector3(0, 0, 1).applyQuaternion(wallQuat);
195
+ const position = new three.Vector3(wall.position[0], wall.position[1], wall.position[2]).add(right.clone().multiplyScalar(offsetX)).add(new three.Vector3(0, offsetY, 0)).add(normal.clone().multiplyScalar(SURFACE_OFFSET));
196
+ group.position.copy(position);
197
+ group.rotation.y = yaw;
198
+ const frame = new three.Mesh(new three.PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN), new three.MeshStandardMaterial({ color: "#1a1a1a" }));
199
+ group.add(frame);
200
+ const texture = loader.load(artwork.src);
201
+ texture.colorSpace = three.SRGBColorSpace;
202
+ const image = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshBasicMaterial({ map: texture }));
203
+ image.position.z = .1;
204
+ group.add(image);
205
+ group.name = `artwork:${artwork.id}`;
206
+ group.userData.scenographyId = artwork.id;
207
+ group.userData.wallId = artwork.wall;
208
+ return group;
209
+ }
210
+ //#endregion
211
+ //#region src/SceneManager.ts
212
+ const disposeObject = (object) => {
213
+ object.traverse((child) => {
214
+ const mesh = child;
215
+ mesh.geometry?.dispose?.();
216
+ const material = mesh.material;
217
+ const dispose = (m) => m?.dispose?.();
218
+ if (Array.isArray(material)) material.forEach(dispose);
219
+ else dispose(material);
220
+ });
221
+ };
222
+ /**
223
+ * Owns the live three.js scene graph and keeps it in sync with a `Scene`
224
+ * definition. `reconcile` diffs the incoming definition against what is already
225
+ * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what
226
+ * makes hot scene edits cheap: add a wing or drop one while moving and only the
227
+ * touched objects are rebuilt.
228
+ */
229
+ var SceneManager = class {
230
+ root;
231
+ walls = /* @__PURE__ */ new Map();
232
+ artworks = /* @__PURE__ */ new Map();
233
+ constructor(root) {
234
+ this.root = root;
235
+ }
236
+ reconcile(scene) {
237
+ const wallById = new Map(scene.walls.map((w) => [w.id, w]));
238
+ this.reconcileWalls(scene.walls);
239
+ this.reconcileArtworks(scene.artworks, wallById);
240
+ }
241
+ reconcileWalls(walls) {
242
+ const next = new Set(walls.map((w) => w.id));
243
+ for (const [id, entry] of this.walls) if (!next.has(id)) {
244
+ this.root.remove(entry.object);
245
+ disposeObject(entry.object);
246
+ this.walls.delete(id);
247
+ }
248
+ for (const wall of walls) {
249
+ const hash = JSON.stringify(wall);
250
+ const existing = this.walls.get(wall.id);
251
+ if (existing?.hash === hash) continue;
252
+ if (existing) {
253
+ this.root.remove(existing.object);
254
+ disposeObject(existing.object);
255
+ }
256
+ const object = buildWall(wall);
257
+ this.root.add(object);
258
+ this.walls.set(wall.id, {
259
+ object,
260
+ hash
261
+ });
262
+ }
263
+ }
264
+ reconcileArtworks(artworks, wallById) {
265
+ const next = new Set(artworks.map((a) => a.id));
266
+ for (const [id, entry] of this.artworks) if (!next.has(id)) {
267
+ this.root.remove(entry.object);
268
+ disposeObject(entry.object);
269
+ this.artworks.delete(id);
270
+ }
271
+ for (const artwork of artworks) {
272
+ const wall = wallById.get(artwork.wall);
273
+ if (!wall) continue;
274
+ const hash = JSON.stringify({
275
+ a: artwork,
276
+ w: wall
277
+ });
278
+ const existing = this.artworks.get(artwork.id);
279
+ if (existing?.hash === hash) continue;
280
+ if (existing) {
281
+ this.root.remove(existing.object);
282
+ disposeObject(existing.object);
283
+ }
284
+ const object = buildArtwork(artwork, wall);
285
+ this.root.add(object);
286
+ this.artworks.set(artwork.id, {
287
+ object,
288
+ hash
289
+ });
290
+ }
291
+ }
292
+ /** Look up the live three.js object for a wall or artwork `id`. */
293
+ get(id) {
294
+ return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;
295
+ }
296
+ dispose() {
297
+ for (const entry of [...this.walls.values(), ...this.artworks.values()]) {
298
+ this.root.remove(entry.object);
299
+ disposeObject(entry.object);
300
+ }
301
+ this.walls.clear();
302
+ this.artworks.clear();
303
+ }
304
+ };
305
+ //#endregion
306
+ //#region src/createGallery.ts
307
+ /**
308
+ * Create and run a gallery from a scene definition. Sets up the three.js
309
+ * renderer, camera, lights and free-roam controls, materializes the scene, and
310
+ * starts the render loop. `setScene` later reconciles changes in place.
311
+ */
312
+ function createGallery(scene, options) {
313
+ const { canvas } = options;
314
+ const width = canvas.clientWidth || window.innerWidth;
315
+ const height = canvas.clientHeight || window.innerHeight;
316
+ const renderer = new three.WebGLRenderer({
317
+ canvas,
318
+ antialias: options.antialias ?? true
319
+ });
320
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
321
+ renderer.setSize(width, height, false);
322
+ const threeScene = new three.Scene();
323
+ const camera = new three.PerspectiveCamera(75, width / height, .1, 5e3);
324
+ const { position, yaw } = scene.camera.start;
325
+ camera.position.set(position[0], position[1], position[2]);
326
+ threeScene.add(new three.AmbientLight(16777215, 1.4));
327
+ threeScene.add(new three.HemisphereLight(16777215, 4473924, 1));
328
+ const manager = new SceneManager(threeScene);
329
+ manager.reconcile(scene);
330
+ const controls = new FreeRoamControls(camera, canvas, {
331
+ bounds: scene.camera.bounds,
332
+ position: camera.position.clone(),
333
+ yaw
334
+ });
335
+ controls.attach();
336
+ const listeners = /* @__PURE__ */ new Map();
337
+ const clock = new three.Clock();
338
+ let rafId = null;
339
+ const renderFrame = () => {
340
+ rafId = requestAnimationFrame(renderFrame);
341
+ controls.update(clock.getDelta());
342
+ renderer.render(threeScene, camera);
343
+ };
344
+ return {
345
+ ctx: {
346
+ entity: (id) => manager.get(id),
347
+ three: {
348
+ scene: threeScene,
349
+ camera,
350
+ renderer
351
+ }
352
+ },
353
+ setScene(next) {
354
+ manager.reconcile(next);
355
+ if (next.camera.bounds !== void 0) controls.bounds = next.camera.bounds;
356
+ },
357
+ start() {
358
+ if (rafId === null) {
359
+ clock.start();
360
+ renderFrame();
361
+ }
362
+ },
363
+ stop() {
364
+ if (rafId !== null) {
365
+ cancelAnimationFrame(rafId);
366
+ rafId = null;
367
+ }
368
+ },
369
+ dispose() {
370
+ this.stop();
371
+ controls.detach();
372
+ manager.dispose();
373
+ renderer.dispose();
374
+ listeners.clear();
375
+ },
376
+ on(event, handler) {
377
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
378
+ set.add(handler);
379
+ listeners.set(event, set);
380
+ return () => set.delete(handler);
381
+ },
382
+ emit(event, payload) {
383
+ listeners.get(event)?.forEach((handler) => handler(payload));
384
+ },
385
+ resize(w, h) {
386
+ camera.aspect = w / h;
387
+ camera.updateProjectionMatrix();
388
+ renderer.setSize(w, h, false);
389
+ }
390
+ };
391
+ }
392
+ //#endregion
393
+ exports.FreeRoamControls = FreeRoamControls;
394
+ exports.SceneManager = SceneManager;
395
+ exports.buildArtwork = buildArtwork;
396
+ exports.buildWall = buildWall;
397
+ exports.createGallery = createGallery;
398
+
399
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["Vector3","Quaternion","Vector3","Vector2","Quaternion","Vector3","Quaternion","Vector3","Mesh","PlaneGeometry","MeshStandardMaterial","DoubleSide","TextureLoader","Group","Quaternion","Vector3","Mesh","PlaneGeometry","MeshStandardMaterial","SRGBColorSpace","MeshBasicMaterial","WebGLRenderer","ThreeScene","PerspectiveCamera","AmbientLight","HemisphereLight","Clock"],"sources":["../src/controls/boundaries.ts","../src/controls/headBob.ts","../src/controls/keyboard.ts","../src/controls/mouse.ts","../src/controls/FreeRoamControls.ts","../src/builders/wall.ts","../src/builders/artwork.ts","../src/SceneManager.ts","../src/createGallery.ts"],"sourcesContent":["import { Vector3 } from \"three\";\n\n/**\n * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped\n * independently so sliding along a wall still works when one axis is blocked.\n */\nexport const clampToBounds = (current: Vector3, step: Vector3, bound: number): Vector3 => {\n const next = new Vector3().addVectors(current, step);\n if (next.x < -bound || next.x > bound) next.x = current.x;\n if (next.z < -bound || next.z > bound) next.z = current.z;\n return next;\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/**\n * Compute the per-frame translation from WASD input, oriented by the current\n * yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (code: string): number => (state[code] ? 1 : 0);\n const forwardVelocity = has(\"KeyW\") - has(\"KeyS\");\n const strafeVelocity = has(\"KeyA\") - has(\"KeyD\");\n const boosted = state[\"ShiftLeft\"] || state[\"ShiftRight\"];\n const factor = boosted ? 30 : 10;\n\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into a look quaternion. Mutates `look` and\n * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid\n * flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): Quaternion => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);\n\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\n\nimport { clampToBounds } from \"./boundaries\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport { createKeyboardState, movementFromKeyboard, onKeyDown, onKeyUp } from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\n\nexport interface FreeRoamOptions {\n /** Square half-extent that clamps XZ movement. */\n bounds?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n}\n\n/**\n * First-person, free-roaming camera controls ported from the original Virtual\n * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while\n * moving, and square boundary clamping. The cardboard / device-orientation path\n * was intentionally dropped.\n *\n * Framework-agnostic: construct it with a camera and the canvas element, call\n * `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n bounds: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(camera: PerspectiveCamera, domElement: HTMLElement, options: FreeRoamOptions = {}) {\n this.camera = camera;\n this.domElement = domElement;\n this.bounds = options.bounds ?? 450;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);\n this.translation = clampToBounds(this.translation, step, this.bounds);\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import { DoubleSide, Mesh, MeshStandardMaterial, PlaneGeometry, type Object3D } from \"three\";\nimport type { Wall } from \"@scenography/core\";\n\n/** Build the three.js object that materializes a `Wall` definition. */\nexport function buildWall(wall: Wall): Object3D {\n const [width, height] = wall.size;\n const geometry = new PlaneGeometry(width, height);\n const material = new MeshStandardMaterial({ color: wall.color ?? \"#ffffff\", side: DoubleSide });\n\n const mesh = new Mesh(geometry, material);\n mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);\n mesh.rotation.y = wall.rotation ?? 0;\n mesh.name = `wall:${wall.id}`;\n mesh.userData.scenographyId = wall.id;\n return mesh;\n}\n","import {\n Group,\n Mesh,\n MeshBasicMaterial,\n MeshStandardMaterial,\n PlaneGeometry,\n Quaternion,\n SRGBColorSpace,\n TextureLoader,\n Vector3,\n type Object3D,\n} from \"three\";\nimport type { Artwork, Wall } from \"@scenography/core\";\n\nconst loader = new TextureLoader();\nconst FRAME_MARGIN = 4;\nconst SURFACE_OFFSET = 1;\n\n/**\n * Build the three.js object for an `Artwork`, placing it on its wall. The piece\n * is positioned in the wall's local plane via `offset` and nudged forward along\n * the wall normal so it sits in front of the surface.\n */\nexport function buildArtwork(artwork: Artwork, wall: Wall): Object3D {\n const [width, height] = artwork.size;\n const [offsetX, offsetY] = artwork.offset ?? [0, 0];\n const yaw = wall.rotation ?? 0;\n\n const group = new Group();\n const wallQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), yaw);\n const right = new Vector3(1, 0, 0).applyQuaternion(wallQuat);\n const normal = new Vector3(0, 0, 1).applyQuaternion(wallQuat);\n\n const position = new Vector3(wall.position[0], wall.position[1], wall.position[2])\n .add(right.clone().multiplyScalar(offsetX))\n .add(new Vector3(0, offsetY, 0))\n .add(normal.clone().multiplyScalar(SURFACE_OFFSET));\n group.position.copy(position);\n group.rotation.y = yaw;\n\n // Frame: a slightly larger dark plane behind the image.\n const frame = new Mesh(\n new PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN),\n new MeshStandardMaterial({ color: \"#1a1a1a\" }),\n );\n group.add(frame);\n\n // Image: textured plane, nudged forward so it reads above the frame.\n const texture = loader.load(artwork.src);\n texture.colorSpace = SRGBColorSpace;\n const image = new Mesh(new PlaneGeometry(width, height), new MeshBasicMaterial({ map: texture }));\n image.position.z = 0.1;\n group.add(image);\n\n group.name = `artwork:${artwork.id}`;\n group.userData.scenographyId = artwork.id;\n group.userData.wallId = artwork.wall;\n return group;\n}\n","import type { Object3D, Scene as ThreeScene } from \"three\";\nimport type { Artwork, Scene, Wall } from \"@scenography/core\";\n\nimport { buildArtwork, buildWall } from \"./builders\";\n\ninterface Entry {\n object: Object3D;\n /** JSON snapshot used to detect changes between reconciles. */\n hash: string;\n}\n\nconst disposeObject = (object: Object3D): void => {\n object.traverse((child) => {\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const material = mesh.material;\n const dispose = (m: unknown): void => (m as { dispose?: () => void })?.dispose?.();\n if (Array.isArray(material)) material.forEach(dispose);\n else dispose(material);\n });\n};\n\n/**\n * Owns the live three.js scene graph and keeps it in sync with a `Scene`\n * definition. `reconcile` diffs the incoming definition against what is already\n * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what\n * makes hot scene edits cheap: add a wing or drop one while moving and only the\n * touched objects are rebuilt.\n */\nexport class SceneManager {\n private readonly root: ThreeScene;\n private readonly walls = new Map<string, Entry>();\n private readonly artworks = new Map<string, Entry>();\n\n constructor(root: ThreeScene) {\n this.root = root;\n }\n\n reconcile(scene: Scene): void {\n const wallById = new Map(scene.walls.map((w) => [w.id, w] as const));\n this.reconcileWalls(scene.walls);\n this.reconcileArtworks(scene.artworks, wallById);\n }\n\n private reconcileWalls(walls: Wall[]): void {\n const next = new Set(walls.map((w) => w.id));\n for (const [id, entry] of this.walls) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.walls.delete(id);\n }\n }\n for (const wall of walls) {\n const hash = JSON.stringify(wall);\n const existing = this.walls.get(wall.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildWall(wall);\n this.root.add(object);\n this.walls.set(wall.id, { object, hash });\n }\n }\n\n private reconcileArtworks(artworks: Artwork[], wallById: Map<string, Wall>): void {\n const next = new Set(artworks.map((a) => a.id));\n for (const [id, entry] of this.artworks) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.artworks.delete(id);\n }\n }\n for (const artwork of artworks) {\n const wall = wallById.get(artwork.wall);\n if (!wall) continue;\n // Rebuild when either the artwork or the wall it hangs on changed.\n const hash = JSON.stringify({ a: artwork, w: wall });\n const existing = this.artworks.get(artwork.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildArtwork(artwork, wall);\n this.root.add(object);\n this.artworks.set(artwork.id, { object, hash });\n }\n }\n\n /** Look up the live three.js object for a wall or artwork `id`. */\n get(id: string): Object3D | undefined {\n return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;\n }\n\n dispose(): void {\n for (const entry of [...this.walls.values(), ...this.artworks.values()]) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n this.walls.clear();\n this.artworks.clear();\n }\n}\n","import {\n AmbientLight,\n Clock,\n HemisphereLight,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n type Object3D,\n} from \"three\";\nimport type { Scene } from \"@scenography/core\";\n\nimport { FreeRoamControls } from \"./controls\";\nimport { SceneManager } from \"./SceneManager\";\n\nexport interface GalleryOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n antialias?: boolean;\n}\n\n/**\n * High-level API handed to behavior code. Deliberately NOT raw three.js: this is\n * the supported surface. Reach into `three` only as a documented escape hatch.\n */\nexport interface GalleryContext {\n /** Resolve the live object for a wall/artwork `id`. */\n entity(id: string): Object3D | undefined;\n /** Escape hatch to the underlying three.js objects for advanced use. */\n readonly three: {\n scene: ThreeScene;\n camera: PerspectiveCamera;\n renderer: WebGLRenderer;\n };\n}\n\nexport interface Gallery {\n /** Apply a new scene definition, reconciling only what changed (hot-swap). */\n setScene(scene: Scene): void;\n /** Start the render loop. */\n start(): void;\n /** Stop the render loop. */\n stop(): void;\n /** Tear everything down and release GPU resources. */\n dispose(): void;\n /** Subscribe to a named event; returns an unsubscribe function. */\n on(event: string, handler: (payload?: unknown) => void): () => void;\n /** Emit a named event to subscribers. */\n emit(event: string, payload?: unknown): void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n readonly ctx: GalleryContext;\n}\n\n/**\n * Create and run a gallery from a scene definition. Sets up the three.js\n * renderer, camera, lights and free-roam controls, materializes the scene, and\n * starts the render loop. `setScene` later reconciles changes in place.\n */\nexport function createGallery(scene: Scene, options: GalleryOptions): Gallery {\n const { canvas } = options;\n const width = canvas.clientWidth || window.innerWidth;\n const height = canvas.clientHeight || window.innerHeight;\n\n const renderer = new WebGLRenderer({ canvas, antialias: options.antialias ?? true });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(75, width / height, 0.1, 5000);\n const { position, yaw } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n threeScene.add(new AmbientLight(0xffffff, 1.4));\n threeScene.add(new HemisphereLight(0xffffff, 0x444444, 1));\n\n const manager = new SceneManager(threeScene);\n manager.reconcile(scene);\n\n const controls = new FreeRoamControls(camera, canvas, {\n bounds: scene.camera.bounds,\n position: camera.position.clone(),\n yaw,\n });\n controls.attach();\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const clock = new Clock();\n let rafId: number | null = null;\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n controls.update(clock.getDelta());\n renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n if (next.camera.bounds !== undefined) controls.bounds = next.camera.bounds;\n },\n start() {\n if (rafId === null) {\n clock.start();\n renderFrame();\n }\n },\n stop() {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n },\n dispose() {\n this.stop();\n controls.detach();\n manager.dispose();\n renderer.dispose();\n listeners.clear();\n },\n on(event, handler) {\n const set = listeners.get(event) ?? new Set();\n set.add(handler);\n listeners.set(event, set);\n return () => set.delete(handler);\n },\n emit(event, payload) {\n listeners.get(event)?.forEach((handler) => handler(payload));\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n },\n };\n}\n"],"mappings":";;;;;;;AAMA,MAAa,iBAAiB,SAAkB,MAAe,UAA2B;CACxF,MAAM,OAAO,IAAIA,MAAAA,QAAQ,CAAC,CAAC,WAAW,SAAS,IAAI;CACnD,IAAI,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ;CACxD,IAAI,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ;CACxD,OAAO;AACT;;;ACLA,MAAa,4BAA0C;CAAE,QAAQ;CAAO,OAAO;AAAE;;;;;AAMjF,MAAa,iBAAiB,OAAqB,YAA0B;CAC3E,IAAI,CAAC,MAAM,QAAQ;CAEnB,MAAM,aAAa,KAAK;CAExB,MAAM,gBADW,IAAI,KAAK,OAAQ,MAAM,QAAQ,QAAQ,KAAM,UAAU,KACvC,aAAc;CAC/C,MAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,SAAS,YAAY;CAE1D,IAAI,MAAM,UAAU,cAAc,MAAM,SAAS;AACnD;;AAGA,MAAa,iBAAiB,UAAgC,KAAK,IAAI,MAAM,QAAQ,EAAE,IAAI;;;ACnB3F,MAAa,6BAA4C,CAAC;AAE1D,MAAa,aAAa,OAAsB,UAA+B;CAC7E,MAAM,MAAM,QAAQ;AACtB;AAEA,MAAa,WAAW,OAAsB,UAA+B;CAC3E,MAAM,MAAM,QAAQ;AACtB;;;;;AAMA,MAAa,wBAAwB,OAAsB,KAAa,UAA2B;CACjG,MAAM,OAAO,SAA0B,MAAM,QAAQ,IAAI;CACzD,MAAM,kBAAkB,IAAI,MAAM,IAAI,IAAI,MAAM;CAChD,MAAM,iBAAiB,IAAI,MAAM,IAAI,IAAI,MAAM;CAE/C,MAAM,SADU,MAAM,gBAAgB,MAAM,gBACnB,KAAK;CAE9B,MAAM,MAAM,IAAIC,MAAAA,WAAW,CAAC,CAAC,iBAAiB,IAAIC,MAAAA,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG;CAEvE,MAAM,UAAU,IAAIA,MAAAA,QAAQ,GAAG,GAAG,GAAG,CAAC,CACnC,gBAAgB,GAAG,CAAC,CACpB,eAAe,kBAAkB,QAAQ,MAAM;CAClD,MAAM,OAAO,IAAIA,MAAAA,QAAQ,KAAK,GAAG,CAAC,CAAC,CAChC,gBAAgB,GAAG,CAAC,CACpB,eAAe,iBAAiB,QAAQ,MAAM;CAEjD,OAAO,QAAQ,IAAI,IAAI;AACzB;;;ACnBA,MAAM,YAAY;AAClB,MAAM,cAAc;AAEpB,MAAa,0BAAsC;CACjD,UAAU;CACV,KAAK,IAAIC,MAAAA,QAAQ;CACjB,SAAS,IAAIA,MAAAA,QAAQ;CACrB,OAAO,IAAIA,MAAAA,QAAQ;AACrB;AAEA,MAAM,SAAS,GAAW,GAAW,MAAsB,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;AAErF,MAAa,eAAe,OAAmB,UAA4B;CACzE,MAAM,WAAW,MAAM,WAAW;AACpC;AAEA,MAAa,aAAa,UAA4B;CACpD,MAAM,WAAW;AACnB;AAEA,MAAa,eAAe,OAAmB,UAA4B;CACzE,MAAM,IAAI,IAAI,MAAM,QAAQ,OAAO,aAAa,GAAG,MAAM,QAAQ,OAAO,cAAc,CAAC;CACvF,MAAM,MAAM,WAAW,MAAM,KAAK,MAAM,OAAO;AACjD;AAEA,MAAa,oBAAoB,UAA4B;CAC3D,MAAM,QAAQ,KAAK,MAAM,GAAG;CAC5B,MAAM,MAAM,IAAI,GAAG,CAAC;AACtB;;;;;;AAOA,MAAa,uBAAuB,OAAmB,SAAgC;CACrF,MAAM,KAAK,MAAM,MAAM,IAAI,OAAO;CAClC,MAAM,KAAK,MAAM,MAAM,IAAI,OAAO;CAElC,KAAK,OAAO,CAAC,KAAK;CAClB,KAAK,QAAQ,MAAM,KAAK,QAAQ,CAAC,KAAK,aAAa,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC;CAE5E,MAAM,KAAK,IAAIC,MAAAA,WAAW,CAAC,CAAC,iBAAiB,IAAIC,MAAAA,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG;CAC3E,MAAM,KAAK,IAAID,MAAAA,WAAW,CAAC,CAAC,iBAAiB,IAAIC,MAAAA,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,KAAK;CAC7E,OAAO,IAAID,MAAAA,WAAW,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE;AAClD;;;;;;;;;;;;AC7BA,IAAa,mBAAb,MAA8B;CAC5B;CAEA;CACA;CACA,QAAyB,iBAAiB;CAC1C,WAA4B,oBAAoB;CAChD,UAA2B,mBAAmB;CAC9C,OAAmC;EAAE,KAAK;EAAG,OAAO;CAAE;CACtD,WAAmB,IAAIE,MAAAA,WAAW;CAClC,cAAsB,IAAIC,MAAAA,QAAQ,GAAG,KAAK,CAAC;CAE3C,WAAyE,CAAC;CAE1E,YAAY,QAA2B,YAAyB,UAA2B,CAAC,GAAG;EAC7F,KAAK,SAAS;EACd,KAAK,aAAa;EAClB,KAAK,SAAS,QAAQ,UAAU;EAChC,IAAI,QAAQ,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ;EAC5D,IAAI,QAAQ,QAAQ,KAAA,GAAW,KAAK,KAAK,MAAM,QAAQ;EACvD,KAAK,SAAS,iBAAiB,IAAIA,MAAAA,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,KAAK,GAAG;CACpE;CAEA,SAAe;EACb,MAAM,QAAQ,QAAqB,MAAc,aAAkC;GACjF,OAAO,iBAAiB,MAAM,QAAQ;GACtC,KAAK,SAAS,KAAK;IAAC;IAAQ;IAAM;GAAQ,CAAC;EAC7C;EAEA,KAAK,KAAK,YAAY,cAAc,MAAM,YAAY,KAAK,OAAO,CAAe,CAAC;EAClF,KAAK,QAAQ,cAAc,MAAM,YAAY,KAAK,OAAO,CAAe,CAAC;EACzE,KAAK,QAAQ,iBAAiB,UAAU,KAAK,KAAK,CAAC;EACnD,KAAK,QAAQ,YAAY,MAAM,UAAU,KAAK,UAAU,CAAkB,CAAC;EAC3E,KAAK,QAAQ,UAAU,MAAM,QAAQ,KAAK,UAAU,CAAkB,CAAC;CACzE;CAEA,SAAe;EACb,KAAK,MAAM,CAAC,QAAQ,MAAM,aAAa,KAAK,UAC1C,OAAO,oBAAoB,MAAM,QAAQ;EAC3C,KAAK,SAAS,SAAS;CACzB;;CAGA,OAAO,OAAqB;EAC1B,IAAI,KAAK,MAAM,UAAU,KAAK,WAAW,oBAAoB,KAAK,OAAO,KAAK,IAAI;EAElF,MAAM,OAAO,qBAAqB,KAAK,UAAU,KAAK,KAAK,KAAK,KAAK;EACrE,KAAK,cAAc,cAAc,KAAK,aAAa,MAAM,KAAK,MAAM;EACpE,IAAI,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ,SAAS;EACxD,cAAc,KAAK,SAAS,KAAK;EAEjC,KAAK,OAAO,WAAW,KAAK,KAAK,QAAQ;EACzC,KAAK,OAAO,SAAS,KAAK,KAAK,WAAW;EAC1C,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,OAAO;EAEpD,MAAM,SAAS,IAAIA,MAAAA,QAAQ,GAAG,GAAG,EAAE,CAAC,CACjC,gBAAgB,KAAK,QAAQ,CAAC,CAC9B,eAAe,GAAG,CAAC,CACnB,IAAI,KAAK,WAAW;EACvB,KAAK,OAAO,OAAO,MAAM;EAEzB,iBAAiB,KAAK,KAAK;CAC7B;AACF;;;;AC5FA,SAAgB,UAAU,MAAsB;CAC9C,MAAM,CAAC,OAAO,UAAU,KAAK;CAI7B,MAAM,OAAO,IAAIC,MAAAA,KAAK,IAHDC,MAAAA,cAAc,OAAO,MAGb,GAAG,IAFXC,MAAAA,qBAAqB;EAAE,OAAO,KAAK,SAAS;EAAW,MAAMC,MAAAA;CAAW,CAEtD,CAAC;CACxC,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE;CACtE,KAAK,SAAS,IAAI,KAAK,YAAY;CACnC,KAAK,OAAO,QAAQ,KAAK;CACzB,KAAK,SAAS,gBAAgB,KAAK;CACnC,OAAO;AACT;;;ACDA,MAAM,SAAS,IAAIC,MAAAA,cAAc;AACjC,MAAM,eAAe;AACrB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,aAAa,SAAkB,MAAsB;CACnE,MAAM,CAAC,OAAO,UAAU,QAAQ;CAChC,MAAM,CAAC,SAAS,WAAW,QAAQ,UAAU,CAAC,GAAG,CAAC;CAClD,MAAM,MAAM,KAAK,YAAY;CAE7B,MAAM,QAAQ,IAAIC,MAAAA,MAAM;CACxB,MAAM,WAAW,IAAIC,MAAAA,WAAW,CAAC,CAAC,iBAAiB,IAAIC,MAAAA,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG;CAC5E,MAAM,QAAQ,IAAIA,MAAAA,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB,QAAQ;CAC3D,MAAM,SAAS,IAAIA,MAAAA,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB,QAAQ;CAE5D,MAAM,WAAW,IAAIA,MAAAA,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE,CAAC,CAC/E,IAAI,MAAM,MAAM,CAAC,CAAC,eAAe,OAAO,CAAC,CAAC,CAC1C,IAAI,IAAIA,MAAAA,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAC/B,IAAI,OAAO,MAAM,CAAC,CAAC,eAAe,cAAc,CAAC;CACpD,MAAM,SAAS,KAAK,QAAQ;CAC5B,MAAM,SAAS,IAAI;CAGnB,MAAM,QAAQ,IAAIC,MAAAA,KAChB,IAAIC,MAAAA,cAAc,QAAQ,cAAc,SAAS,YAAY,GAC7D,IAAIC,MAAAA,qBAAqB,EAAE,OAAO,UAAU,CAAC,CAC/C;CACA,MAAM,IAAI,KAAK;CAGf,MAAM,UAAU,OAAO,KAAK,QAAQ,GAAG;CACvC,QAAQ,aAAaC,MAAAA;CACrB,MAAM,QAAQ,IAAIH,MAAAA,KAAK,IAAIC,MAAAA,cAAc,OAAO,MAAM,GAAG,IAAIG,MAAAA,kBAAkB,EAAE,KAAK,QAAQ,CAAC,CAAC;CAChG,MAAM,SAAS,IAAI;CACnB,MAAM,IAAI,KAAK;CAEf,MAAM,OAAO,WAAW,QAAQ;CAChC,MAAM,SAAS,gBAAgB,QAAQ;CACvC,MAAM,SAAS,SAAS,QAAQ;CAChC,OAAO;AACT;;;AC/CA,MAAM,iBAAiB,WAA2B;CAChD,OAAO,UAAU,UAAU;EACzB,MAAM,OAAO;EACb,KAAK,UAAU,UAAU;EACzB,MAAM,WAAW,KAAK;EACtB,MAAM,WAAW,MAAsB,GAAgC,UAAU;EACjF,IAAI,MAAM,QAAQ,QAAQ,GAAG,SAAS,QAAQ,OAAO;OAChD,QAAQ,QAAQ;CACvB,CAAC;AACH;;;;;;;;AASA,IAAa,eAAb,MAA0B;CACxB;CACA,wBAAyB,IAAI,IAAmB;CAChD,2BAA4B,IAAI,IAAmB;CAEnD,YAAY,MAAkB;EAC5B,KAAK,OAAO;CACd;CAEA,UAAU,OAAoB;EAC5B,MAAM,WAAW,IAAI,IAAI,MAAM,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAU,CAAC;EACnE,KAAK,eAAe,MAAM,KAAK;EAC/B,KAAK,kBAAkB,MAAM,UAAU,QAAQ;CACjD;CAEA,eAAuB,OAAqB;EAC1C,MAAM,OAAO,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,EAAE,CAAC;EAC3C,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,OAC7B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;GAC1B,KAAK,MAAM,OAAO,EAAE;EACtB;EAEF,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,KAAK,UAAU,IAAI;GAChC,MAAM,WAAW,KAAK,MAAM,IAAI,KAAK,EAAE;GACvC,IAAI,UAAU,SAAS,MAAM;GAC7B,IAAI,UAAU;IACZ,KAAK,KAAK,OAAO,SAAS,MAAM;IAChC,cAAc,SAAS,MAAM;GAC/B;GACA,MAAM,SAAS,UAAU,IAAI;GAC7B,KAAK,KAAK,IAAI,MAAM;GACpB,KAAK,MAAM,IAAI,KAAK,IAAI;IAAE;IAAQ;GAAK,CAAC;EAC1C;CACF;CAEA,kBAA0B,UAAqB,UAAmC;EAChF,MAAM,OAAO,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,EAAE,CAAC;EAC9C,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,UAC7B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;GAC1B,KAAK,SAAS,OAAO,EAAE;EACzB;EAEF,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI;GACtC,IAAI,CAAC,MAAM;GAEX,MAAM,OAAO,KAAK,UAAU;IAAE,GAAG;IAAS,GAAG;GAAK,CAAC;GACnD,MAAM,WAAW,KAAK,SAAS,IAAI,QAAQ,EAAE;GAC7C,IAAI,UAAU,SAAS,MAAM;GAC7B,IAAI,UAAU;IACZ,KAAK,KAAK,OAAO,SAAS,MAAM;IAChC,cAAc,SAAS,MAAM;GAC/B;GACA,MAAM,SAAS,aAAa,SAAS,IAAI;GACzC,KAAK,KAAK,IAAI,MAAM;GACpB,KAAK,SAAS,IAAI,QAAQ,IAAI;IAAE;IAAQ;GAAK,CAAC;EAChD;CACF;;CAGA,IAAI,IAAkC;EACpC,OAAO,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,UAAU,KAAK,SAAS,IAAI,EAAE,CAAC,EAAE;CAC9D;CAEA,UAAgB;EACd,KAAK,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM,OAAO,GAAG,GAAG,KAAK,SAAS,OAAO,CAAC,GAAG;GACvE,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;EAC5B;EACA,KAAK,MAAM,MAAM;EACjB,KAAK,SAAS,MAAM;CACtB;AACF;;;;;;;;AC9CA,SAAgB,cAAc,OAAc,SAAkC;CAC5E,MAAM,EAAE,WAAW;CACnB,MAAM,QAAQ,OAAO,eAAe,OAAO;CAC3C,MAAM,SAAS,OAAO,gBAAgB,OAAO;CAE7C,MAAM,WAAW,IAAIC,MAAAA,cAAc;EAAE;EAAQ,WAAW,QAAQ,aAAa;CAAK,CAAC;CACnF,SAAS,cAAc,KAAK,IAAI,OAAO,kBAAkB,CAAC,CAAC;CAC3D,SAAS,QAAQ,OAAO,QAAQ,KAAK;CAErC,MAAM,aAAa,IAAIC,MAAAA,MAAW;CAElC,MAAM,SAAS,IAAIC,MAAAA,kBAAkB,IAAI,QAAQ,QAAQ,IAAK,GAAI;CAClE,MAAM,EAAE,UAAU,QAAQ,MAAM,OAAO;CACvC,OAAO,SAAS,IAAI,SAAS,IAAI,SAAS,IAAI,SAAS,EAAE;CAEzD,WAAW,IAAI,IAAIC,MAAAA,aAAa,UAAU,GAAG,CAAC;CAC9C,WAAW,IAAI,IAAIC,MAAAA,gBAAgB,UAAU,SAAU,CAAC,CAAC;CAEzD,MAAM,UAAU,IAAI,aAAa,UAAU;CAC3C,QAAQ,UAAU,KAAK;CAEvB,MAAM,WAAW,IAAI,iBAAiB,QAAQ,QAAQ;EACpD,QAAQ,MAAM,OAAO;EACrB,UAAU,OAAO,SAAS,MAAM;EAChC;CACF,CAAC;CACD,SAAS,OAAO;CAEhB,MAAM,4BAAY,IAAI,IAA8C;CACpE,MAAM,QAAQ,IAAIC,MAAAA,MAAM;CACxB,IAAI,QAAuB;CAE3B,MAAM,oBAA0B;EAC9B,QAAQ,sBAAsB,WAAW;EACzC,SAAS,OAAO,MAAM,SAAS,CAAC;EAChC,SAAS,OAAO,YAAY,MAAM;CACpC;CAOA,OAAO;EACL,KAAA;GALA,SAAS,OAAO,QAAQ,IAAI,EAAE;GAC9B,OAAO;IAAE,OAAO;IAAY;IAAQ;GAAS;EAI3C;EACF,SAAS,MAAM;GACb,QAAQ,UAAU,IAAI;GACtB,IAAI,KAAK,OAAO,WAAW,KAAA,GAAW,SAAS,SAAS,KAAK,OAAO;EACtE;EACA,QAAQ;GACN,IAAI,UAAU,MAAM;IAClB,MAAM,MAAM;IACZ,YAAY;GACd;EACF;EACA,OAAO;GACL,IAAI,UAAU,MAAM;IAClB,qBAAqB,KAAK;IAC1B,QAAQ;GACV;EACF;EACA,UAAU;GACR,KAAK,KAAK;GACV,SAAS,OAAO;GAChB,QAAQ,QAAQ;GAChB,SAAS,QAAQ;GACjB,UAAU,MAAM;EAClB;EACA,GAAG,OAAO,SAAS;GACjB,MAAM,MAAM,UAAU,IAAI,KAAK,qBAAK,IAAI,IAAI;GAC5C,IAAI,IAAI,OAAO;GACf,UAAU,IAAI,OAAO,GAAG;GACxB,aAAa,IAAI,OAAO,OAAO;EACjC;EACA,KAAK,OAAO,SAAS;GACnB,UAAU,IAAI,KAAK,CAAC,EAAE,SAAS,YAAY,QAAQ,OAAO,CAAC;EAC7D;EACA,OAAO,GAAG,GAAG;GACX,OAAO,SAAS,IAAI;GACpB,OAAO,uBAAuB;GAC9B,SAAS,QAAQ,GAAG,GAAG,KAAK;EAC9B;CACF;AACF"}
@@ -0,0 +1,120 @@
1
+ import { Object3D, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from "three";
2
+ import { Artwork, Scene as Scene$1, Wall } from "@scenography/core";
3
+
4
+ //#region src/createGallery.d.ts
5
+ interface GalleryOptions {
6
+ /** Canvas the gallery renders into. */
7
+ canvas: HTMLCanvasElement;
8
+ /** Background color. Defaults to a soft gray. */
9
+ background?: string;
10
+ antialias?: boolean;
11
+ }
12
+ /**
13
+ * High-level API handed to behavior code. Deliberately NOT raw three.js: this is
14
+ * the supported surface. Reach into `three` only as a documented escape hatch.
15
+ */
16
+ interface GalleryContext {
17
+ /** Resolve the live object for a wall/artwork `id`. */
18
+ entity(id: string): Object3D | undefined;
19
+ /** Escape hatch to the underlying three.js objects for advanced use. */
20
+ readonly three: {
21
+ scene: Scene;
22
+ camera: PerspectiveCamera;
23
+ renderer: WebGLRenderer;
24
+ };
25
+ }
26
+ interface Gallery {
27
+ /** Apply a new scene definition, reconciling only what changed (hot-swap). */
28
+ setScene(scene: Scene$1): void;
29
+ /** Start the render loop. */
30
+ start(): void;
31
+ /** Stop the render loop. */
32
+ stop(): void;
33
+ /** Tear everything down and release GPU resources. */
34
+ dispose(): void;
35
+ /** Subscribe to a named event; returns an unsubscribe function. */
36
+ on(event: string, handler: (payload?: unknown) => void): () => void;
37
+ /** Emit a named event to subscribers. */
38
+ emit(event: string, payload?: unknown): void;
39
+ /** Resize the renderer/camera to the given pixel size. */
40
+ resize(width: number, height: number): void;
41
+ readonly ctx: GalleryContext;
42
+ }
43
+ /**
44
+ * Create and run a gallery from a scene definition. Sets up the three.js
45
+ * renderer, camera, lights and free-roam controls, materializes the scene, and
46
+ * starts the render loop. `setScene` later reconciles changes in place.
47
+ */
48
+ declare function createGallery(scene: Scene$1, options: GalleryOptions): Gallery;
49
+ //#endregion
50
+ //#region src/SceneManager.d.ts
51
+ /**
52
+ * Owns the live three.js scene graph and keeps it in sync with a `Scene`
53
+ * definition. `reconcile` diffs the incoming definition against what is already
54
+ * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what
55
+ * makes hot scene edits cheap: add a wing or drop one while moving and only the
56
+ * touched objects are rebuilt.
57
+ */
58
+ declare class SceneManager {
59
+ private readonly root;
60
+ private readonly walls;
61
+ private readonly artworks;
62
+ constructor(root: Scene);
63
+ reconcile(scene: Scene$1): void;
64
+ private reconcileWalls;
65
+ private reconcileArtworks;
66
+ /** Look up the live three.js object for a wall or artwork `id`. */
67
+ get(id: string): Object3D | undefined;
68
+ dispose(): void;
69
+ }
70
+ //#endregion
71
+ //#region src/controls/FreeRoamControls.d.ts
72
+ interface FreeRoamOptions {
73
+ /** Square half-extent that clamps XZ movement. */
74
+ bounds?: number;
75
+ /** Starting position. */
76
+ position?: Vector3;
77
+ /** Starting yaw in radians. */
78
+ yaw?: number;
79
+ }
80
+ /**
81
+ * First-person, free-roaming camera controls ported from the original Virtual
82
+ * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while
83
+ * moving, and square boundary clamping. The cardboard / device-orientation path
84
+ * was intentionally dropped.
85
+ *
86
+ * Framework-agnostic: construct it with a camera and the canvas element, call
87
+ * `attach()`, then `update(delta)` once per frame.
88
+ */
89
+ declare class FreeRoamControls {
90
+ bounds: number;
91
+ private readonly camera;
92
+ private readonly domElement;
93
+ private readonly mouse;
94
+ private readonly keyboard;
95
+ private readonly headBob;
96
+ private readonly look;
97
+ private rotation;
98
+ private translation;
99
+ private readonly handlers;
100
+ constructor(camera: PerspectiveCamera, domElement: HTMLElement, options?: FreeRoamOptions);
101
+ attach(): void;
102
+ detach(): void;
103
+ /** Advance the camera by `delta` seconds of input. */
104
+ update(delta: number): void;
105
+ }
106
+ //#endregion
107
+ //#region src/builders/wall.d.ts
108
+ /** Build the three.js object that materializes a `Wall` definition. */
109
+ declare function buildWall(wall: Wall): Object3D;
110
+ //#endregion
111
+ //#region src/builders/artwork.d.ts
112
+ /**
113
+ * Build the three.js object for an `Artwork`, placing it on its wall. The piece
114
+ * is positioned in the wall's local plane via `offset` and nudged forward along
115
+ * the wall normal so it sits in front of the surface.
116
+ */
117
+ declare function buildArtwork(artwork: Artwork, wall: Wall): Object3D;
118
+ //#endregion
119
+ export { FreeRoamControls, type FreeRoamOptions, type Gallery, type GalleryContext, type GalleryOptions, SceneManager, buildArtwork, buildWall, createGallery };
120
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,120 @@
1
+ import { Object3D, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from "three";
2
+ import { Artwork, Scene as Scene$1, Wall } from "@scenography/core";
3
+
4
+ //#region src/createGallery.d.ts
5
+ interface GalleryOptions {
6
+ /** Canvas the gallery renders into. */
7
+ canvas: HTMLCanvasElement;
8
+ /** Background color. Defaults to a soft gray. */
9
+ background?: string;
10
+ antialias?: boolean;
11
+ }
12
+ /**
13
+ * High-level API handed to behavior code. Deliberately NOT raw three.js: this is
14
+ * the supported surface. Reach into `three` only as a documented escape hatch.
15
+ */
16
+ interface GalleryContext {
17
+ /** Resolve the live object for a wall/artwork `id`. */
18
+ entity(id: string): Object3D | undefined;
19
+ /** Escape hatch to the underlying three.js objects for advanced use. */
20
+ readonly three: {
21
+ scene: Scene;
22
+ camera: PerspectiveCamera;
23
+ renderer: WebGLRenderer;
24
+ };
25
+ }
26
+ interface Gallery {
27
+ /** Apply a new scene definition, reconciling only what changed (hot-swap). */
28
+ setScene(scene: Scene$1): void;
29
+ /** Start the render loop. */
30
+ start(): void;
31
+ /** Stop the render loop. */
32
+ stop(): void;
33
+ /** Tear everything down and release GPU resources. */
34
+ dispose(): void;
35
+ /** Subscribe to a named event; returns an unsubscribe function. */
36
+ on(event: string, handler: (payload?: unknown) => void): () => void;
37
+ /** Emit a named event to subscribers. */
38
+ emit(event: string, payload?: unknown): void;
39
+ /** Resize the renderer/camera to the given pixel size. */
40
+ resize(width: number, height: number): void;
41
+ readonly ctx: GalleryContext;
42
+ }
43
+ /**
44
+ * Create and run a gallery from a scene definition. Sets up the three.js
45
+ * renderer, camera, lights and free-roam controls, materializes the scene, and
46
+ * starts the render loop. `setScene` later reconciles changes in place.
47
+ */
48
+ declare function createGallery(scene: Scene$1, options: GalleryOptions): Gallery;
49
+ //#endregion
50
+ //#region src/SceneManager.d.ts
51
+ /**
52
+ * Owns the live three.js scene graph and keeps it in sync with a `Scene`
53
+ * definition. `reconcile` diffs the incoming definition against what is already
54
+ * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what
55
+ * makes hot scene edits cheap: add a wing or drop one while moving and only the
56
+ * touched objects are rebuilt.
57
+ */
58
+ declare class SceneManager {
59
+ private readonly root;
60
+ private readonly walls;
61
+ private readonly artworks;
62
+ constructor(root: Scene);
63
+ reconcile(scene: Scene$1): void;
64
+ private reconcileWalls;
65
+ private reconcileArtworks;
66
+ /** Look up the live three.js object for a wall or artwork `id`. */
67
+ get(id: string): Object3D | undefined;
68
+ dispose(): void;
69
+ }
70
+ //#endregion
71
+ //#region src/controls/FreeRoamControls.d.ts
72
+ interface FreeRoamOptions {
73
+ /** Square half-extent that clamps XZ movement. */
74
+ bounds?: number;
75
+ /** Starting position. */
76
+ position?: Vector3;
77
+ /** Starting yaw in radians. */
78
+ yaw?: number;
79
+ }
80
+ /**
81
+ * First-person, free-roaming camera controls ported from the original Virtual
82
+ * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while
83
+ * moving, and square boundary clamping. The cardboard / device-orientation path
84
+ * was intentionally dropped.
85
+ *
86
+ * Framework-agnostic: construct it with a camera and the canvas element, call
87
+ * `attach()`, then `update(delta)` once per frame.
88
+ */
89
+ declare class FreeRoamControls {
90
+ bounds: number;
91
+ private readonly camera;
92
+ private readonly domElement;
93
+ private readonly mouse;
94
+ private readonly keyboard;
95
+ private readonly headBob;
96
+ private readonly look;
97
+ private rotation;
98
+ private translation;
99
+ private readonly handlers;
100
+ constructor(camera: PerspectiveCamera, domElement: HTMLElement, options?: FreeRoamOptions);
101
+ attach(): void;
102
+ detach(): void;
103
+ /** Advance the camera by `delta` seconds of input. */
104
+ update(delta: number): void;
105
+ }
106
+ //#endregion
107
+ //#region src/builders/wall.d.ts
108
+ /** Build the three.js object that materializes a `Wall` definition. */
109
+ declare function buildWall(wall: Wall): Object3D;
110
+ //#endregion
111
+ //#region src/builders/artwork.d.ts
112
+ /**
113
+ * Build the three.js object for an `Artwork`, placing it on its wall. The piece
114
+ * is positioned in the wall's local plane via `offset` and nudged forward along
115
+ * the wall normal so it sits in front of the surface.
116
+ */
117
+ declare function buildArtwork(artwork: Artwork, wall: Wall): Object3D;
118
+ //#endregion
119
+ export { FreeRoamControls, type FreeRoamOptions, type Gallery, type GalleryContext, type GalleryOptions, SceneManager, buildArtwork, buildWall, createGallery };
120
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,394 @@
1
+ import { AmbientLight, Clock, DoubleSide, Group, HemisphereLight, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PlaneGeometry, Quaternion, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
2
+ //#region src/controls/boundaries.ts
3
+ /**
4
+ * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped
5
+ * independently so sliding along a wall still works when one axis is blocked.
6
+ */
7
+ const clampToBounds = (current, step, bound) => {
8
+ const next = new Vector3().addVectors(current, step);
9
+ if (next.x < -bound || next.x > bound) next.x = current.x;
10
+ if (next.z < -bound || next.z > bound) next.z = current.z;
11
+ return next;
12
+ };
13
+ //#endregion
14
+ //#region src/controls/headBob.ts
15
+ const createHeadBobState = () => ({
16
+ active: false,
17
+ timer: 0
18
+ });
19
+ /**
20
+ * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength
21
+ * steps so the bob always settles back to neutral when movement stops.
22
+ */
23
+ const updateHeadBob = (state, elapsed) => {
24
+ if (!state.active) return;
25
+ const wavelength = Math.PI;
26
+ const nextStepTime = (1 + Math.floor((state.timer + 1e-6) * 10 / wavelength)) * wavelength / 10;
27
+ state.timer = Math.min(state.timer + elapsed, nextStepTime);
28
+ if (state.timer === nextStepTime) state.active = false;
29
+ };
30
+ /** Vertical offset to add to the camera, in world units. */
31
+ const headBobOffset = (state) => Math.sin(state.timer * 10) * 1.5;
32
+ //#endregion
33
+ //#region src/controls/keyboard.ts
34
+ const createKeyboardState = () => ({});
35
+ const onKeyDown = (state, event) => {
36
+ state[event.code] = true;
37
+ };
38
+ const onKeyUp = (state, event) => {
39
+ state[event.code] = false;
40
+ };
41
+ /**
42
+ * Compute the per-frame translation from WASD input, oriented by the current
43
+ * yaw (`phi`). Holding Shift acts as a speed booster.
44
+ */
45
+ const movementFromKeyboard = (state, phi, delta) => {
46
+ const has = (code) => state[code] ? 1 : 0;
47
+ const forwardVelocity = has("KeyW") - has("KeyS");
48
+ const strafeVelocity = has("KeyA") - has("KeyD");
49
+ const factor = state["ShiftLeft"] || state["ShiftRight"] ? 30 : 10;
50
+ const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);
51
+ const forward = new Vector3(0, 0, -10).applyQuaternion(yaw).multiplyScalar(forwardVelocity * delta * factor);
52
+ const left = new Vector3(-10, 0, 0).applyQuaternion(yaw).multiplyScalar(strafeVelocity * delta * factor);
53
+ return forward.add(left);
54
+ };
55
+ //#endregion
56
+ //#region src/controls/mouse.ts
57
+ const PHI_SPEED = 8;
58
+ const THETA_SPEED = 5;
59
+ const createMouseState = () => ({
60
+ dragging: false,
61
+ pos: new Vector2(),
62
+ prevPos: new Vector2(),
63
+ delta: new Vector2()
64
+ });
65
+ const clamp = (x, a, b) => Math.min(Math.max(x, a), b);
66
+ const onMouseDown = (state, event) => {
67
+ state.dragging = event.button === 0;
68
+ };
69
+ const onMouseUp = (state) => {
70
+ state.dragging = false;
71
+ };
72
+ const onMouseMove = (state, event) => {
73
+ state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);
74
+ state.delta.subVectors(state.pos, state.prevPos);
75
+ };
76
+ const commitMouseFrame = (state) => {
77
+ state.prevPos.copy(state.pos);
78
+ state.delta.set(0, 0);
79
+ };
80
+ /**
81
+ * Integrate the pointer delta into a look quaternion. Mutates `look` and
82
+ * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid
83
+ * flipping over.
84
+ */
85
+ const updateLookFromMouse = (mouse, look) => {
86
+ const xh = mouse.delta.x / window.innerWidth;
87
+ const yh = mouse.delta.y / window.innerHeight;
88
+ look.phi += -xh * PHI_SPEED;
89
+ look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);
90
+ const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);
91
+ const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);
92
+ return new Quaternion().multiply(qy).multiply(qx);
93
+ };
94
+ //#endregion
95
+ //#region src/controls/FreeRoamControls.ts
96
+ /**
97
+ * First-person, free-roaming camera controls ported from the original Virtual
98
+ * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while
99
+ * moving, and square boundary clamping. The cardboard / device-orientation path
100
+ * was intentionally dropped.
101
+ *
102
+ * Framework-agnostic: construct it with a camera and the canvas element, call
103
+ * `attach()`, then `update(delta)` once per frame.
104
+ */
105
+ var FreeRoamControls = class {
106
+ bounds;
107
+ camera;
108
+ domElement;
109
+ mouse = createMouseState();
110
+ keyboard = createKeyboardState();
111
+ headBob = createHeadBobState();
112
+ look = {
113
+ phi: 0,
114
+ theta: 0
115
+ };
116
+ rotation = new Quaternion();
117
+ translation = new Vector3(0, 170, 0);
118
+ handlers = [];
119
+ constructor(camera, domElement, options = {}) {
120
+ this.camera = camera;
121
+ this.domElement = domElement;
122
+ this.bounds = options.bounds ?? 450;
123
+ if (options.position) this.translation.copy(options.position);
124
+ if (options.yaw !== void 0) this.look.phi = options.yaw;
125
+ this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);
126
+ }
127
+ attach() {
128
+ const bind = (target, type, listener) => {
129
+ target.addEventListener(type, listener);
130
+ this.handlers.push([
131
+ target,
132
+ type,
133
+ listener
134
+ ]);
135
+ };
136
+ bind(this.domElement, "mousedown", (e) => onMouseDown(this.mouse, e));
137
+ bind(window, "mousemove", (e) => onMouseMove(this.mouse, e));
138
+ bind(window, "mouseup", () => onMouseUp(this.mouse));
139
+ bind(window, "keydown", (e) => onKeyDown(this.keyboard, e));
140
+ bind(window, "keyup", (e) => onKeyUp(this.keyboard, e));
141
+ }
142
+ detach() {
143
+ for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);
144
+ this.handlers.length = 0;
145
+ }
146
+ /** Advance the camera by `delta` seconds of input. */
147
+ update(delta) {
148
+ if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);
149
+ const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);
150
+ this.translation = clampToBounds(this.translation, step, this.bounds);
151
+ if (step.x !== 0 || step.z !== 0) this.headBob.active = true;
152
+ updateHeadBob(this.headBob, delta);
153
+ this.camera.quaternion.copy(this.rotation);
154
+ this.camera.position.copy(this.translation);
155
+ this.camera.position.y += headBobOffset(this.headBob);
156
+ const target = new Vector3(0, 0, -1).applyQuaternion(this.rotation).multiplyScalar(100).add(this.translation);
157
+ this.camera.lookAt(target);
158
+ commitMouseFrame(this.mouse);
159
+ }
160
+ };
161
+ //#endregion
162
+ //#region src/builders/wall.ts
163
+ /** Build the three.js object that materializes a `Wall` definition. */
164
+ function buildWall(wall) {
165
+ const [width, height] = wall.size;
166
+ const mesh = new Mesh(new PlaneGeometry(width, height), new MeshStandardMaterial({
167
+ color: wall.color ?? "#ffffff",
168
+ side: DoubleSide
169
+ }));
170
+ mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);
171
+ mesh.rotation.y = wall.rotation ?? 0;
172
+ mesh.name = `wall:${wall.id}`;
173
+ mesh.userData.scenographyId = wall.id;
174
+ return mesh;
175
+ }
176
+ //#endregion
177
+ //#region src/builders/artwork.ts
178
+ const loader = new TextureLoader();
179
+ const FRAME_MARGIN = 4;
180
+ const SURFACE_OFFSET = 1;
181
+ /**
182
+ * Build the three.js object for an `Artwork`, placing it on its wall. The piece
183
+ * is positioned in the wall's local plane via `offset` and nudged forward along
184
+ * the wall normal so it sits in front of the surface.
185
+ */
186
+ function buildArtwork(artwork, wall) {
187
+ const [width, height] = artwork.size;
188
+ const [offsetX, offsetY] = artwork.offset ?? [0, 0];
189
+ const yaw = wall.rotation ?? 0;
190
+ const group = new Group();
191
+ const wallQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), yaw);
192
+ const right = new Vector3(1, 0, 0).applyQuaternion(wallQuat);
193
+ const normal = new Vector3(0, 0, 1).applyQuaternion(wallQuat);
194
+ const position = new Vector3(wall.position[0], wall.position[1], wall.position[2]).add(right.clone().multiplyScalar(offsetX)).add(new Vector3(0, offsetY, 0)).add(normal.clone().multiplyScalar(SURFACE_OFFSET));
195
+ group.position.copy(position);
196
+ group.rotation.y = yaw;
197
+ const frame = new Mesh(new PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN), new MeshStandardMaterial({ color: "#1a1a1a" }));
198
+ group.add(frame);
199
+ const texture = loader.load(artwork.src);
200
+ texture.colorSpace = SRGBColorSpace;
201
+ const image = new Mesh(new PlaneGeometry(width, height), new MeshBasicMaterial({ map: texture }));
202
+ image.position.z = .1;
203
+ group.add(image);
204
+ group.name = `artwork:${artwork.id}`;
205
+ group.userData.scenographyId = artwork.id;
206
+ group.userData.wallId = artwork.wall;
207
+ return group;
208
+ }
209
+ //#endregion
210
+ //#region src/SceneManager.ts
211
+ const disposeObject = (object) => {
212
+ object.traverse((child) => {
213
+ const mesh = child;
214
+ mesh.geometry?.dispose?.();
215
+ const material = mesh.material;
216
+ const dispose = (m) => m?.dispose?.();
217
+ if (Array.isArray(material)) material.forEach(dispose);
218
+ else dispose(material);
219
+ });
220
+ };
221
+ /**
222
+ * Owns the live three.js scene graph and keeps it in sync with a `Scene`
223
+ * definition. `reconcile` diffs the incoming definition against what is already
224
+ * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what
225
+ * makes hot scene edits cheap: add a wing or drop one while moving and only the
226
+ * touched objects are rebuilt.
227
+ */
228
+ var SceneManager = class {
229
+ root;
230
+ walls = /* @__PURE__ */ new Map();
231
+ artworks = /* @__PURE__ */ new Map();
232
+ constructor(root) {
233
+ this.root = root;
234
+ }
235
+ reconcile(scene) {
236
+ const wallById = new Map(scene.walls.map((w) => [w.id, w]));
237
+ this.reconcileWalls(scene.walls);
238
+ this.reconcileArtworks(scene.artworks, wallById);
239
+ }
240
+ reconcileWalls(walls) {
241
+ const next = new Set(walls.map((w) => w.id));
242
+ for (const [id, entry] of this.walls) if (!next.has(id)) {
243
+ this.root.remove(entry.object);
244
+ disposeObject(entry.object);
245
+ this.walls.delete(id);
246
+ }
247
+ for (const wall of walls) {
248
+ const hash = JSON.stringify(wall);
249
+ const existing = this.walls.get(wall.id);
250
+ if (existing?.hash === hash) continue;
251
+ if (existing) {
252
+ this.root.remove(existing.object);
253
+ disposeObject(existing.object);
254
+ }
255
+ const object = buildWall(wall);
256
+ this.root.add(object);
257
+ this.walls.set(wall.id, {
258
+ object,
259
+ hash
260
+ });
261
+ }
262
+ }
263
+ reconcileArtworks(artworks, wallById) {
264
+ const next = new Set(artworks.map((a) => a.id));
265
+ for (const [id, entry] of this.artworks) if (!next.has(id)) {
266
+ this.root.remove(entry.object);
267
+ disposeObject(entry.object);
268
+ this.artworks.delete(id);
269
+ }
270
+ for (const artwork of artworks) {
271
+ const wall = wallById.get(artwork.wall);
272
+ if (!wall) continue;
273
+ const hash = JSON.stringify({
274
+ a: artwork,
275
+ w: wall
276
+ });
277
+ const existing = this.artworks.get(artwork.id);
278
+ if (existing?.hash === hash) continue;
279
+ if (existing) {
280
+ this.root.remove(existing.object);
281
+ disposeObject(existing.object);
282
+ }
283
+ const object = buildArtwork(artwork, wall);
284
+ this.root.add(object);
285
+ this.artworks.set(artwork.id, {
286
+ object,
287
+ hash
288
+ });
289
+ }
290
+ }
291
+ /** Look up the live three.js object for a wall or artwork `id`. */
292
+ get(id) {
293
+ return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;
294
+ }
295
+ dispose() {
296
+ for (const entry of [...this.walls.values(), ...this.artworks.values()]) {
297
+ this.root.remove(entry.object);
298
+ disposeObject(entry.object);
299
+ }
300
+ this.walls.clear();
301
+ this.artworks.clear();
302
+ }
303
+ };
304
+ //#endregion
305
+ //#region src/createGallery.ts
306
+ /**
307
+ * Create and run a gallery from a scene definition. Sets up the three.js
308
+ * renderer, camera, lights and free-roam controls, materializes the scene, and
309
+ * starts the render loop. `setScene` later reconciles changes in place.
310
+ */
311
+ function createGallery(scene, options) {
312
+ const { canvas } = options;
313
+ const width = canvas.clientWidth || window.innerWidth;
314
+ const height = canvas.clientHeight || window.innerHeight;
315
+ const renderer = new WebGLRenderer({
316
+ canvas,
317
+ antialias: options.antialias ?? true
318
+ });
319
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
320
+ renderer.setSize(width, height, false);
321
+ const threeScene = new Scene();
322
+ const camera = new PerspectiveCamera(75, width / height, .1, 5e3);
323
+ const { position, yaw } = scene.camera.start;
324
+ camera.position.set(position[0], position[1], position[2]);
325
+ threeScene.add(new AmbientLight(16777215, 1.4));
326
+ threeScene.add(new HemisphereLight(16777215, 4473924, 1));
327
+ const manager = new SceneManager(threeScene);
328
+ manager.reconcile(scene);
329
+ const controls = new FreeRoamControls(camera, canvas, {
330
+ bounds: scene.camera.bounds,
331
+ position: camera.position.clone(),
332
+ yaw
333
+ });
334
+ controls.attach();
335
+ const listeners = /* @__PURE__ */ new Map();
336
+ const clock = new Clock();
337
+ let rafId = null;
338
+ const renderFrame = () => {
339
+ rafId = requestAnimationFrame(renderFrame);
340
+ controls.update(clock.getDelta());
341
+ renderer.render(threeScene, camera);
342
+ };
343
+ return {
344
+ ctx: {
345
+ entity: (id) => manager.get(id),
346
+ three: {
347
+ scene: threeScene,
348
+ camera,
349
+ renderer
350
+ }
351
+ },
352
+ setScene(next) {
353
+ manager.reconcile(next);
354
+ if (next.camera.bounds !== void 0) controls.bounds = next.camera.bounds;
355
+ },
356
+ start() {
357
+ if (rafId === null) {
358
+ clock.start();
359
+ renderFrame();
360
+ }
361
+ },
362
+ stop() {
363
+ if (rafId !== null) {
364
+ cancelAnimationFrame(rafId);
365
+ rafId = null;
366
+ }
367
+ },
368
+ dispose() {
369
+ this.stop();
370
+ controls.detach();
371
+ manager.dispose();
372
+ renderer.dispose();
373
+ listeners.clear();
374
+ },
375
+ on(event, handler) {
376
+ const set = listeners.get(event) ?? /* @__PURE__ */ new Set();
377
+ set.add(handler);
378
+ listeners.set(event, set);
379
+ return () => set.delete(handler);
380
+ },
381
+ emit(event, payload) {
382
+ listeners.get(event)?.forEach((handler) => handler(payload));
383
+ },
384
+ resize(w, h) {
385
+ camera.aspect = w / h;
386
+ camera.updateProjectionMatrix();
387
+ renderer.setSize(w, h, false);
388
+ }
389
+ };
390
+ }
391
+ //#endregion
392
+ export { FreeRoamControls, SceneManager, buildArtwork, buildWall, createGallery };
393
+
394
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["ThreeScene"],"sources":["../src/controls/boundaries.ts","../src/controls/headBob.ts","../src/controls/keyboard.ts","../src/controls/mouse.ts","../src/controls/FreeRoamControls.ts","../src/builders/wall.ts","../src/builders/artwork.ts","../src/SceneManager.ts","../src/createGallery.ts"],"sourcesContent":["import { Vector3 } from \"three\";\n\n/**\n * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped\n * independently so sliding along a wall still works when one axis is blocked.\n */\nexport const clampToBounds = (current: Vector3, step: Vector3, bound: number): Vector3 => {\n const next = new Vector3().addVectors(current, step);\n if (next.x < -bound || next.x > bound) next.x = current.x;\n if (next.z < -bound || next.z > bound) next.z = current.z;\n return next;\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/**\n * Compute the per-frame translation from WASD input, oriented by the current\n * yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (code: string): number => (state[code] ? 1 : 0);\n const forwardVelocity = has(\"KeyW\") - has(\"KeyS\");\n const strafeVelocity = has(\"KeyA\") - has(\"KeyD\");\n const boosted = state[\"ShiftLeft\"] || state[\"ShiftRight\"];\n const factor = boosted ? 30 : 10;\n\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into a look quaternion. Mutates `look` and\n * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid\n * flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): Quaternion => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);\n\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\n\nimport { clampToBounds } from \"./boundaries\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport { createKeyboardState, movementFromKeyboard, onKeyDown, onKeyUp } from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\n\nexport interface FreeRoamOptions {\n /** Square half-extent that clamps XZ movement. */\n bounds?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n}\n\n/**\n * First-person, free-roaming camera controls ported from the original Virtual\n * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while\n * moving, and square boundary clamping. The cardboard / device-orientation path\n * was intentionally dropped.\n *\n * Framework-agnostic: construct it with a camera and the canvas element, call\n * `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n bounds: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(camera: PerspectiveCamera, domElement: HTMLElement, options: FreeRoamOptions = {}) {\n this.camera = camera;\n this.domElement = domElement;\n this.bounds = options.bounds ?? 450;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);\n this.translation = clampToBounds(this.translation, step, this.bounds);\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import { DoubleSide, Mesh, MeshStandardMaterial, PlaneGeometry, type Object3D } from \"three\";\nimport type { Wall } from \"@scenography/core\";\n\n/** Build the three.js object that materializes a `Wall` definition. */\nexport function buildWall(wall: Wall): Object3D {\n const [width, height] = wall.size;\n const geometry = new PlaneGeometry(width, height);\n const material = new MeshStandardMaterial({ color: wall.color ?? \"#ffffff\", side: DoubleSide });\n\n const mesh = new Mesh(geometry, material);\n mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);\n mesh.rotation.y = wall.rotation ?? 0;\n mesh.name = `wall:${wall.id}`;\n mesh.userData.scenographyId = wall.id;\n return mesh;\n}\n","import {\n Group,\n Mesh,\n MeshBasicMaterial,\n MeshStandardMaterial,\n PlaneGeometry,\n Quaternion,\n SRGBColorSpace,\n TextureLoader,\n Vector3,\n type Object3D,\n} from \"three\";\nimport type { Artwork, Wall } from \"@scenography/core\";\n\nconst loader = new TextureLoader();\nconst FRAME_MARGIN = 4;\nconst SURFACE_OFFSET = 1;\n\n/**\n * Build the three.js object for an `Artwork`, placing it on its wall. The piece\n * is positioned in the wall's local plane via `offset` and nudged forward along\n * the wall normal so it sits in front of the surface.\n */\nexport function buildArtwork(artwork: Artwork, wall: Wall): Object3D {\n const [width, height] = artwork.size;\n const [offsetX, offsetY] = artwork.offset ?? [0, 0];\n const yaw = wall.rotation ?? 0;\n\n const group = new Group();\n const wallQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), yaw);\n const right = new Vector3(1, 0, 0).applyQuaternion(wallQuat);\n const normal = new Vector3(0, 0, 1).applyQuaternion(wallQuat);\n\n const position = new Vector3(wall.position[0], wall.position[1], wall.position[2])\n .add(right.clone().multiplyScalar(offsetX))\n .add(new Vector3(0, offsetY, 0))\n .add(normal.clone().multiplyScalar(SURFACE_OFFSET));\n group.position.copy(position);\n group.rotation.y = yaw;\n\n // Frame: a slightly larger dark plane behind the image.\n const frame = new Mesh(\n new PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN),\n new MeshStandardMaterial({ color: \"#1a1a1a\" }),\n );\n group.add(frame);\n\n // Image: textured plane, nudged forward so it reads above the frame.\n const texture = loader.load(artwork.src);\n texture.colorSpace = SRGBColorSpace;\n const image = new Mesh(new PlaneGeometry(width, height), new MeshBasicMaterial({ map: texture }));\n image.position.z = 0.1;\n group.add(image);\n\n group.name = `artwork:${artwork.id}`;\n group.userData.scenographyId = artwork.id;\n group.userData.wallId = artwork.wall;\n return group;\n}\n","import type { Object3D, Scene as ThreeScene } from \"three\";\nimport type { Artwork, Scene, Wall } from \"@scenography/core\";\n\nimport { buildArtwork, buildWall } from \"./builders\";\n\ninterface Entry {\n object: Object3D;\n /** JSON snapshot used to detect changes between reconciles. */\n hash: string;\n}\n\nconst disposeObject = (object: Object3D): void => {\n object.traverse((child) => {\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const material = mesh.material;\n const dispose = (m: unknown): void => (m as { dispose?: () => void })?.dispose?.();\n if (Array.isArray(material)) material.forEach(dispose);\n else dispose(material);\n });\n};\n\n/**\n * Owns the live three.js scene graph and keeps it in sync with a `Scene`\n * definition. `reconcile` diffs the incoming definition against what is already\n * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what\n * makes hot scene edits cheap: add a wing or drop one while moving and only the\n * touched objects are rebuilt.\n */\nexport class SceneManager {\n private readonly root: ThreeScene;\n private readonly walls = new Map<string, Entry>();\n private readonly artworks = new Map<string, Entry>();\n\n constructor(root: ThreeScene) {\n this.root = root;\n }\n\n reconcile(scene: Scene): void {\n const wallById = new Map(scene.walls.map((w) => [w.id, w] as const));\n this.reconcileWalls(scene.walls);\n this.reconcileArtworks(scene.artworks, wallById);\n }\n\n private reconcileWalls(walls: Wall[]): void {\n const next = new Set(walls.map((w) => w.id));\n for (const [id, entry] of this.walls) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.walls.delete(id);\n }\n }\n for (const wall of walls) {\n const hash = JSON.stringify(wall);\n const existing = this.walls.get(wall.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildWall(wall);\n this.root.add(object);\n this.walls.set(wall.id, { object, hash });\n }\n }\n\n private reconcileArtworks(artworks: Artwork[], wallById: Map<string, Wall>): void {\n const next = new Set(artworks.map((a) => a.id));\n for (const [id, entry] of this.artworks) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.artworks.delete(id);\n }\n }\n for (const artwork of artworks) {\n const wall = wallById.get(artwork.wall);\n if (!wall) continue;\n // Rebuild when either the artwork or the wall it hangs on changed.\n const hash = JSON.stringify({ a: artwork, w: wall });\n const existing = this.artworks.get(artwork.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildArtwork(artwork, wall);\n this.root.add(object);\n this.artworks.set(artwork.id, { object, hash });\n }\n }\n\n /** Look up the live three.js object for a wall or artwork `id`. */\n get(id: string): Object3D | undefined {\n return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;\n }\n\n dispose(): void {\n for (const entry of [...this.walls.values(), ...this.artworks.values()]) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n this.walls.clear();\n this.artworks.clear();\n }\n}\n","import {\n AmbientLight,\n Clock,\n HemisphereLight,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n type Object3D,\n} from \"three\";\nimport type { Scene } from \"@scenography/core\";\n\nimport { FreeRoamControls } from \"./controls\";\nimport { SceneManager } from \"./SceneManager\";\n\nexport interface GalleryOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n antialias?: boolean;\n}\n\n/**\n * High-level API handed to behavior code. Deliberately NOT raw three.js: this is\n * the supported surface. Reach into `three` only as a documented escape hatch.\n */\nexport interface GalleryContext {\n /** Resolve the live object for a wall/artwork `id`. */\n entity(id: string): Object3D | undefined;\n /** Escape hatch to the underlying three.js objects for advanced use. */\n readonly three: {\n scene: ThreeScene;\n camera: PerspectiveCamera;\n renderer: WebGLRenderer;\n };\n}\n\nexport interface Gallery {\n /** Apply a new scene definition, reconciling only what changed (hot-swap). */\n setScene(scene: Scene): void;\n /** Start the render loop. */\n start(): void;\n /** Stop the render loop. */\n stop(): void;\n /** Tear everything down and release GPU resources. */\n dispose(): void;\n /** Subscribe to a named event; returns an unsubscribe function. */\n on(event: string, handler: (payload?: unknown) => void): () => void;\n /** Emit a named event to subscribers. */\n emit(event: string, payload?: unknown): void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n readonly ctx: GalleryContext;\n}\n\n/**\n * Create and run a gallery from a scene definition. Sets up the three.js\n * renderer, camera, lights and free-roam controls, materializes the scene, and\n * starts the render loop. `setScene` later reconciles changes in place.\n */\nexport function createGallery(scene: Scene, options: GalleryOptions): Gallery {\n const { canvas } = options;\n const width = canvas.clientWidth || window.innerWidth;\n const height = canvas.clientHeight || window.innerHeight;\n\n const renderer = new WebGLRenderer({ canvas, antialias: options.antialias ?? true });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(75, width / height, 0.1, 5000);\n const { position, yaw } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n threeScene.add(new AmbientLight(0xffffff, 1.4));\n threeScene.add(new HemisphereLight(0xffffff, 0x444444, 1));\n\n const manager = new SceneManager(threeScene);\n manager.reconcile(scene);\n\n const controls = new FreeRoamControls(camera, canvas, {\n bounds: scene.camera.bounds,\n position: camera.position.clone(),\n yaw,\n });\n controls.attach();\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const clock = new Clock();\n let rafId: number | null = null;\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n controls.update(clock.getDelta());\n renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n if (next.camera.bounds !== undefined) controls.bounds = next.camera.bounds;\n },\n start() {\n if (rafId === null) {\n clock.start();\n renderFrame();\n }\n },\n stop() {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n },\n dispose() {\n this.stop();\n controls.detach();\n manager.dispose();\n renderer.dispose();\n listeners.clear();\n },\n on(event, handler) {\n const set = listeners.get(event) ?? new Set();\n set.add(handler);\n listeners.set(event, set);\n return () => set.delete(handler);\n },\n emit(event, payload) {\n listeners.get(event)?.forEach((handler) => handler(payload));\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n },\n };\n}\n"],"mappings":";;;;;;AAMA,MAAa,iBAAiB,SAAkB,MAAe,UAA2B;CACxF,MAAM,OAAO,IAAI,QAAQ,CAAC,CAAC,WAAW,SAAS,IAAI;CACnD,IAAI,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ;CACxD,IAAI,KAAK,IAAI,CAAC,SAAS,KAAK,IAAI,OAAO,KAAK,IAAI,QAAQ;CACxD,OAAO;AACT;;;ACLA,MAAa,4BAA0C;CAAE,QAAQ;CAAO,OAAO;AAAE;;;;;AAMjF,MAAa,iBAAiB,OAAqB,YAA0B;CAC3E,IAAI,CAAC,MAAM,QAAQ;CAEnB,MAAM,aAAa,KAAK;CAExB,MAAM,gBADW,IAAI,KAAK,OAAQ,MAAM,QAAQ,QAAQ,KAAM,UAAU,KACvC,aAAc;CAC/C,MAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,SAAS,YAAY;CAE1D,IAAI,MAAM,UAAU,cAAc,MAAM,SAAS;AACnD;;AAGA,MAAa,iBAAiB,UAAgC,KAAK,IAAI,MAAM,QAAQ,EAAE,IAAI;;;ACnB3F,MAAa,6BAA4C,CAAC;AAE1D,MAAa,aAAa,OAAsB,UAA+B;CAC7E,MAAM,MAAM,QAAQ;AACtB;AAEA,MAAa,WAAW,OAAsB,UAA+B;CAC3E,MAAM,MAAM,QAAQ;AACtB;;;;;AAMA,MAAa,wBAAwB,OAAsB,KAAa,UAA2B;CACjG,MAAM,OAAO,SAA0B,MAAM,QAAQ,IAAI;CACzD,MAAM,kBAAkB,IAAI,MAAM,IAAI,IAAI,MAAM;CAChD,MAAM,iBAAiB,IAAI,MAAM,IAAI,IAAI,MAAM;CAE/C,MAAM,SADU,MAAM,gBAAgB,MAAM,gBACnB,KAAK;CAE9B,MAAM,MAAM,IAAI,WAAW,CAAC,CAAC,iBAAiB,IAAI,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG;CAEvE,MAAM,UAAU,IAAI,QAAQ,GAAG,GAAG,GAAG,CAAC,CACnC,gBAAgB,GAAG,CAAC,CACpB,eAAe,kBAAkB,QAAQ,MAAM;CAClD,MAAM,OAAO,IAAI,QAAQ,KAAK,GAAG,CAAC,CAAC,CAChC,gBAAgB,GAAG,CAAC,CACpB,eAAe,iBAAiB,QAAQ,MAAM;CAEjD,OAAO,QAAQ,IAAI,IAAI;AACzB;;;ACnBA,MAAM,YAAY;AAClB,MAAM,cAAc;AAEpB,MAAa,0BAAsC;CACjD,UAAU;CACV,KAAK,IAAI,QAAQ;CACjB,SAAS,IAAI,QAAQ;CACrB,OAAO,IAAI,QAAQ;AACrB;AAEA,MAAM,SAAS,GAAW,GAAW,MAAsB,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;AAErF,MAAa,eAAe,OAAmB,UAA4B;CACzE,MAAM,WAAW,MAAM,WAAW;AACpC;AAEA,MAAa,aAAa,UAA4B;CACpD,MAAM,WAAW;AACnB;AAEA,MAAa,eAAe,OAAmB,UAA4B;CACzE,MAAM,IAAI,IAAI,MAAM,QAAQ,OAAO,aAAa,GAAG,MAAM,QAAQ,OAAO,cAAc,CAAC;CACvF,MAAM,MAAM,WAAW,MAAM,KAAK,MAAM,OAAO;AACjD;AAEA,MAAa,oBAAoB,UAA4B;CAC3D,MAAM,QAAQ,KAAK,MAAM,GAAG;CAC5B,MAAM,MAAM,IAAI,GAAG,CAAC;AACtB;;;;;;AAOA,MAAa,uBAAuB,OAAmB,SAAgC;CACrF,MAAM,KAAK,MAAM,MAAM,IAAI,OAAO;CAClC,MAAM,KAAK,MAAM,MAAM,IAAI,OAAO;CAElC,KAAK,OAAO,CAAC,KAAK;CAClB,KAAK,QAAQ,MAAM,KAAK,QAAQ,CAAC,KAAK,aAAa,CAAC,KAAK,KAAK,GAAG,KAAK,KAAK,CAAC;CAE5E,MAAM,KAAK,IAAI,WAAW,CAAC,CAAC,iBAAiB,IAAI,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG;CAC3E,MAAM,KAAK,IAAI,WAAW,CAAC,CAAC,iBAAiB,IAAI,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,KAAK;CAC7E,OAAO,IAAI,WAAW,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE;AAClD;;;;;;;;;;;;AC7BA,IAAa,mBAAb,MAA8B;CAC5B;CAEA;CACA;CACA,QAAyB,iBAAiB;CAC1C,WAA4B,oBAAoB;CAChD,UAA2B,mBAAmB;CAC9C,OAAmC;EAAE,KAAK;EAAG,OAAO;CAAE;CACtD,WAAmB,IAAI,WAAW;CAClC,cAAsB,IAAI,QAAQ,GAAG,KAAK,CAAC;CAE3C,WAAyE,CAAC;CAE1E,YAAY,QAA2B,YAAyB,UAA2B,CAAC,GAAG;EAC7F,KAAK,SAAS;EACd,KAAK,aAAa;EAClB,KAAK,SAAS,QAAQ,UAAU;EAChC,IAAI,QAAQ,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ;EAC5D,IAAI,QAAQ,QAAQ,KAAA,GAAW,KAAK,KAAK,MAAM,QAAQ;EACvD,KAAK,SAAS,iBAAiB,IAAI,QAAQ,GAAG,GAAG,CAAC,GAAG,KAAK,KAAK,GAAG;CACpE;CAEA,SAAe;EACb,MAAM,QAAQ,QAAqB,MAAc,aAAkC;GACjF,OAAO,iBAAiB,MAAM,QAAQ;GACtC,KAAK,SAAS,KAAK;IAAC;IAAQ;IAAM;GAAQ,CAAC;EAC7C;EAEA,KAAK,KAAK,YAAY,cAAc,MAAM,YAAY,KAAK,OAAO,CAAe,CAAC;EAClF,KAAK,QAAQ,cAAc,MAAM,YAAY,KAAK,OAAO,CAAe,CAAC;EACzE,KAAK,QAAQ,iBAAiB,UAAU,KAAK,KAAK,CAAC;EACnD,KAAK,QAAQ,YAAY,MAAM,UAAU,KAAK,UAAU,CAAkB,CAAC;EAC3E,KAAK,QAAQ,UAAU,MAAM,QAAQ,KAAK,UAAU,CAAkB,CAAC;CACzE;CAEA,SAAe;EACb,KAAK,MAAM,CAAC,QAAQ,MAAM,aAAa,KAAK,UAC1C,OAAO,oBAAoB,MAAM,QAAQ;EAC3C,KAAK,SAAS,SAAS;CACzB;;CAGA,OAAO,OAAqB;EAC1B,IAAI,KAAK,MAAM,UAAU,KAAK,WAAW,oBAAoB,KAAK,OAAO,KAAK,IAAI;EAElF,MAAM,OAAO,qBAAqB,KAAK,UAAU,KAAK,KAAK,KAAK,KAAK;EACrE,KAAK,cAAc,cAAc,KAAK,aAAa,MAAM,KAAK,MAAM;EACpE,IAAI,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ,SAAS;EACxD,cAAc,KAAK,SAAS,KAAK;EAEjC,KAAK,OAAO,WAAW,KAAK,KAAK,QAAQ;EACzC,KAAK,OAAO,SAAS,KAAK,KAAK,WAAW;EAC1C,KAAK,OAAO,SAAS,KAAK,cAAc,KAAK,OAAO;EAEpD,MAAM,SAAS,IAAI,QAAQ,GAAG,GAAG,EAAE,CAAC,CACjC,gBAAgB,KAAK,QAAQ,CAAC,CAC9B,eAAe,GAAG,CAAC,CACnB,IAAI,KAAK,WAAW;EACvB,KAAK,OAAO,OAAO,MAAM;EAEzB,iBAAiB,KAAK,KAAK;CAC7B;AACF;;;;AC5FA,SAAgB,UAAU,MAAsB;CAC9C,MAAM,CAAC,OAAO,UAAU,KAAK;CAI7B,MAAM,OAAO,IAAI,KAAK,IAHD,cAAc,OAAO,MAGb,GAAG,IAFX,qBAAqB;EAAE,OAAO,KAAK,SAAS;EAAW,MAAM;CAAW,CAEtD,CAAC;CACxC,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE;CACtE,KAAK,SAAS,IAAI,KAAK,YAAY;CACnC,KAAK,OAAO,QAAQ,KAAK;CACzB,KAAK,SAAS,gBAAgB,KAAK;CACnC,OAAO;AACT;;;ACDA,MAAM,SAAS,IAAI,cAAc;AACjC,MAAM,eAAe;AACrB,MAAM,iBAAiB;;;;;;AAOvB,SAAgB,aAAa,SAAkB,MAAsB;CACnE,MAAM,CAAC,OAAO,UAAU,QAAQ;CAChC,MAAM,CAAC,SAAS,WAAW,QAAQ,UAAU,CAAC,GAAG,CAAC;CAClD,MAAM,MAAM,KAAK,YAAY;CAE7B,MAAM,QAAQ,IAAI,MAAM;CACxB,MAAM,WAAW,IAAI,WAAW,CAAC,CAAC,iBAAiB,IAAI,QAAQ,GAAG,GAAG,CAAC,GAAG,GAAG;CAC5E,MAAM,QAAQ,IAAI,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB,QAAQ;CAC3D,MAAM,SAAS,IAAI,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,gBAAgB,QAAQ;CAE5D,MAAM,WAAW,IAAI,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE,CAAC,CAC/E,IAAI,MAAM,MAAM,CAAC,CAAC,eAAe,OAAO,CAAC,CAAC,CAC1C,IAAI,IAAI,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAC/B,IAAI,OAAO,MAAM,CAAC,CAAC,eAAe,cAAc,CAAC;CACpD,MAAM,SAAS,KAAK,QAAQ;CAC5B,MAAM,SAAS,IAAI;CAGnB,MAAM,QAAQ,IAAI,KAChB,IAAI,cAAc,QAAQ,cAAc,SAAS,YAAY,GAC7D,IAAI,qBAAqB,EAAE,OAAO,UAAU,CAAC,CAC/C;CACA,MAAM,IAAI,KAAK;CAGf,MAAM,UAAU,OAAO,KAAK,QAAQ,GAAG;CACvC,QAAQ,aAAa;CACrB,MAAM,QAAQ,IAAI,KAAK,IAAI,cAAc,OAAO,MAAM,GAAG,IAAI,kBAAkB,EAAE,KAAK,QAAQ,CAAC,CAAC;CAChG,MAAM,SAAS,IAAI;CACnB,MAAM,IAAI,KAAK;CAEf,MAAM,OAAO,WAAW,QAAQ;CAChC,MAAM,SAAS,gBAAgB,QAAQ;CACvC,MAAM,SAAS,SAAS,QAAQ;CAChC,OAAO;AACT;;;AC/CA,MAAM,iBAAiB,WAA2B;CAChD,OAAO,UAAU,UAAU;EACzB,MAAM,OAAO;EACb,KAAK,UAAU,UAAU;EACzB,MAAM,WAAW,KAAK;EACtB,MAAM,WAAW,MAAsB,GAAgC,UAAU;EACjF,IAAI,MAAM,QAAQ,QAAQ,GAAG,SAAS,QAAQ,OAAO;OAChD,QAAQ,QAAQ;CACvB,CAAC;AACH;;;;;;;;AASA,IAAa,eAAb,MAA0B;CACxB;CACA,wBAAyB,IAAI,IAAmB;CAChD,2BAA4B,IAAI,IAAmB;CAEnD,YAAY,MAAkB;EAC5B,KAAK,OAAO;CACd;CAEA,UAAU,OAAoB;EAC5B,MAAM,WAAW,IAAI,IAAI,MAAM,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAU,CAAC;EACnE,KAAK,eAAe,MAAM,KAAK;EAC/B,KAAK,kBAAkB,MAAM,UAAU,QAAQ;CACjD;CAEA,eAAuB,OAAqB;EAC1C,MAAM,OAAO,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,EAAE,CAAC;EAC3C,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,OAC7B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;GAC1B,KAAK,MAAM,OAAO,EAAE;EACtB;EAEF,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,OAAO,KAAK,UAAU,IAAI;GAChC,MAAM,WAAW,KAAK,MAAM,IAAI,KAAK,EAAE;GACvC,IAAI,UAAU,SAAS,MAAM;GAC7B,IAAI,UAAU;IACZ,KAAK,KAAK,OAAO,SAAS,MAAM;IAChC,cAAc,SAAS,MAAM;GAC/B;GACA,MAAM,SAAS,UAAU,IAAI;GAC7B,KAAK,KAAK,IAAI,MAAM;GACpB,KAAK,MAAM,IAAI,KAAK,IAAI;IAAE;IAAQ;GAAK,CAAC;EAC1C;CACF;CAEA,kBAA0B,UAAqB,UAAmC;EAChF,MAAM,OAAO,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,EAAE,CAAC;EAC9C,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,UAC7B,IAAI,CAAC,KAAK,IAAI,EAAE,GAAG;GACjB,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;GAC1B,KAAK,SAAS,OAAO,EAAE;EACzB;EAEF,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI;GACtC,IAAI,CAAC,MAAM;GAEX,MAAM,OAAO,KAAK,UAAU;IAAE,GAAG;IAAS,GAAG;GAAK,CAAC;GACnD,MAAM,WAAW,KAAK,SAAS,IAAI,QAAQ,EAAE;GAC7C,IAAI,UAAU,SAAS,MAAM;GAC7B,IAAI,UAAU;IACZ,KAAK,KAAK,OAAO,SAAS,MAAM;IAChC,cAAc,SAAS,MAAM;GAC/B;GACA,MAAM,SAAS,aAAa,SAAS,IAAI;GACzC,KAAK,KAAK,IAAI,MAAM;GACpB,KAAK,SAAS,IAAI,QAAQ,IAAI;IAAE;IAAQ;GAAK,CAAC;EAChD;CACF;;CAGA,IAAI,IAAkC;EACpC,OAAO,KAAK,MAAM,IAAI,EAAE,CAAC,EAAE,UAAU,KAAK,SAAS,IAAI,EAAE,CAAC,EAAE;CAC9D;CAEA,UAAgB;EACd,KAAK,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM,OAAO,GAAG,GAAG,KAAK,SAAS,OAAO,CAAC,GAAG;GACvE,KAAK,KAAK,OAAO,MAAM,MAAM;GAC7B,cAAc,MAAM,MAAM;EAC5B;EACA,KAAK,MAAM,MAAM;EACjB,KAAK,SAAS,MAAM;CACtB;AACF;;;;;;;;AC9CA,SAAgB,cAAc,OAAc,SAAkC;CAC5E,MAAM,EAAE,WAAW;CACnB,MAAM,QAAQ,OAAO,eAAe,OAAO;CAC3C,MAAM,SAAS,OAAO,gBAAgB,OAAO;CAE7C,MAAM,WAAW,IAAI,cAAc;EAAE;EAAQ,WAAW,QAAQ,aAAa;CAAK,CAAC;CACnF,SAAS,cAAc,KAAK,IAAI,OAAO,kBAAkB,CAAC,CAAC;CAC3D,SAAS,QAAQ,OAAO,QAAQ,KAAK;CAErC,MAAM,aAAa,IAAIA,MAAW;CAElC,MAAM,SAAS,IAAI,kBAAkB,IAAI,QAAQ,QAAQ,IAAK,GAAI;CAClE,MAAM,EAAE,UAAU,QAAQ,MAAM,OAAO;CACvC,OAAO,SAAS,IAAI,SAAS,IAAI,SAAS,IAAI,SAAS,EAAE;CAEzD,WAAW,IAAI,IAAI,aAAa,UAAU,GAAG,CAAC;CAC9C,WAAW,IAAI,IAAI,gBAAgB,UAAU,SAAU,CAAC,CAAC;CAEzD,MAAM,UAAU,IAAI,aAAa,UAAU;CAC3C,QAAQ,UAAU,KAAK;CAEvB,MAAM,WAAW,IAAI,iBAAiB,QAAQ,QAAQ;EACpD,QAAQ,MAAM,OAAO;EACrB,UAAU,OAAO,SAAS,MAAM;EAChC;CACF,CAAC;CACD,SAAS,OAAO;CAEhB,MAAM,4BAAY,IAAI,IAA8C;CACpE,MAAM,QAAQ,IAAI,MAAM;CACxB,IAAI,QAAuB;CAE3B,MAAM,oBAA0B;EAC9B,QAAQ,sBAAsB,WAAW;EACzC,SAAS,OAAO,MAAM,SAAS,CAAC;EAChC,SAAS,OAAO,YAAY,MAAM;CACpC;CAOA,OAAO;EACL,KAAA;GALA,SAAS,OAAO,QAAQ,IAAI,EAAE;GAC9B,OAAO;IAAE,OAAO;IAAY;IAAQ;GAAS;EAI3C;EACF,SAAS,MAAM;GACb,QAAQ,UAAU,IAAI;GACtB,IAAI,KAAK,OAAO,WAAW,KAAA,GAAW,SAAS,SAAS,KAAK,OAAO;EACtE;EACA,QAAQ;GACN,IAAI,UAAU,MAAM;IAClB,MAAM,MAAM;IACZ,YAAY;GACd;EACF;EACA,OAAO;GACL,IAAI,UAAU,MAAM;IAClB,qBAAqB,KAAK;IAC1B,QAAQ;GACV;EACF;EACA,UAAU;GACR,KAAK,KAAK;GACV,SAAS,OAAO;GAChB,QAAQ,QAAQ;GAChB,SAAS,QAAQ;GACjB,UAAU,MAAM;EAClB;EACA,GAAG,OAAO,SAAS;GACjB,MAAM,MAAM,UAAU,IAAI,KAAK,qBAAK,IAAI,IAAI;GAC5C,IAAI,IAAI,OAAO;GACf,UAAU,IAAI,OAAO,GAAG;GACxB,aAAa,IAAI,OAAO,OAAO;EACjC;EACA,KAAK,OAAO,SAAS;GACnB,UAAU,IAAI,KAAK,CAAC,EAAE,SAAS,YAAY,QAAQ,OAAO,CAAC;EAC7D;EACA,OAAO,GAAG,GAAG;GACX,OAAO,SAAS,IAAI;GACpB,OAAO,uBAAuB;GAC9B,SAAS,QAAQ,GAAG,GAAG,KAAK;EAC9B;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@scenography/engine",
3
+ "version": "0.1.0",
4
+ "description": "The scenography runtime: materializes a scene definition with three.js and runs it. Reconciles by id for hot scene changes.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.cts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "@scenography/core": "0.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/three": "^0.160.0",
30
+ "three": "^0.160.0"
31
+ },
32
+ "peerDependencies": {
33
+ "three": ">=0.160.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "dev": "tsdown --watch",
38
+ "typecheck": "tsc --noEmit"
39
+ }
40
+ }