@scenography/engine 0.3.0 → 0.4.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/LICENSE +21 -0
- package/dist/index.cjs +131 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +131 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -478,8 +478,126 @@ const EDGES = "edges";
|
|
|
478
478
|
*/
|
|
479
479
|
const EYE_HEIGHT = "eyeHeight";
|
|
480
480
|
//#endregion
|
|
481
|
+
//#region src/builders/textureCache.ts
|
|
482
|
+
/** Two surfaces can share a texture only at the same tiling and color space. */
|
|
483
|
+
const textureKey = (src, repeat, srgb) => `${src}|${repeat[0]}x${repeat[1]}|${srgb ? "srgb" : "linear"}`;
|
|
484
|
+
/** Tiling and color space of a texture; the image arrives later from the loader. */
|
|
485
|
+
const configureTexture = (texture, repeat, srgb) => {
|
|
486
|
+
texture.wrapS = RepeatWrapping;
|
|
487
|
+
texture.wrapT = RepeatWrapping;
|
|
488
|
+
texture.repeat.set(repeat[0], repeat[1]);
|
|
489
|
+
texture.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* Shares textures between materials: the same source at the same tiling and
|
|
493
|
+
* color space becomes one texture, loaded once and uploaded to the GPU once.
|
|
494
|
+
* Reference counted, so releasing a material keeps the texture alive while
|
|
495
|
+
* another one still samples it.
|
|
496
|
+
*/
|
|
497
|
+
var TextureCache = class {
|
|
498
|
+
source;
|
|
499
|
+
entries = /* @__PURE__ */ new Map();
|
|
500
|
+
owners = /* @__PURE__ */ new Map();
|
|
501
|
+
constructor(source) {
|
|
502
|
+
this.source = source;
|
|
503
|
+
}
|
|
504
|
+
/** The shared texture for a source, created on first use. */
|
|
505
|
+
acquire(src, repeat, srgb) {
|
|
506
|
+
const key = textureKey(src, repeat, srgb);
|
|
507
|
+
const existing = this.entries.get(key);
|
|
508
|
+
if (existing) {
|
|
509
|
+
existing.refs += 1;
|
|
510
|
+
return existing.texture;
|
|
511
|
+
}
|
|
512
|
+
const texture = this.source(src);
|
|
513
|
+
configureTexture(texture, repeat, srgb);
|
|
514
|
+
const entry = {
|
|
515
|
+
key,
|
|
516
|
+
texture,
|
|
517
|
+
refs: 1
|
|
518
|
+
};
|
|
519
|
+
this.entries.set(key, entry);
|
|
520
|
+
this.owners.set(texture, entry);
|
|
521
|
+
return texture;
|
|
522
|
+
}
|
|
523
|
+
/** Drop one reference; the texture is disposed when the last one goes. */
|
|
524
|
+
release(texture) {
|
|
525
|
+
const entry = this.owners.get(texture);
|
|
526
|
+
if (!entry) {
|
|
527
|
+
texture.dispose();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
entry.refs -= 1;
|
|
531
|
+
if (entry.refs > 0) return;
|
|
532
|
+
this.entries.delete(entry.key);
|
|
533
|
+
this.owners.delete(texture);
|
|
534
|
+
texture.dispose();
|
|
535
|
+
}
|
|
536
|
+
/** Textures currently shared. */
|
|
537
|
+
get size() {
|
|
538
|
+
return this.entries.size;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/builders/texture.ts
|
|
543
|
+
const textureLoader = new TextureLoader();
|
|
544
|
+
const cache = new TextureCache((src) => textureLoader.load(src));
|
|
545
|
+
/**
|
|
546
|
+
* Load one map into `material[slot]`. Textures are shared: every material that
|
|
547
|
+
* samples the same source at the same tiling reuses one texture object. `.exr`
|
|
548
|
+
* maps go through EXRLoader, imported on demand, and are not shared because
|
|
549
|
+
* they resolve asynchronously.
|
|
550
|
+
*/
|
|
551
|
+
const loadMap = (material, slot, src, repeat, srgb) => {
|
|
552
|
+
if (src.toLowerCase().endsWith(".exr")) {
|
|
553
|
+
import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
|
|
554
|
+
new EXRLoader().load(src, (texture) => {
|
|
555
|
+
configureTexture(texture, repeat, srgb);
|
|
556
|
+
texture.needsUpdate = true;
|
|
557
|
+
material[slot] = texture;
|
|
558
|
+
material.needsUpdate = true;
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
material[slot] = cache.acquire(src, repeat, srgb);
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Apply a `SurfaceTexture` to a standard material. Returns whether a second UV
|
|
567
|
+
* set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must
|
|
568
|
+
* provide one.
|
|
569
|
+
*/
|
|
570
|
+
const applyTexture = (material, texture) => {
|
|
571
|
+
const repeat = texture.repeat ?? [1, 1];
|
|
572
|
+
loadMap(material, "map", texture.src, repeat, true);
|
|
573
|
+
if (texture.normal) {
|
|
574
|
+
loadMap(material, "normalMap", texture.normal, repeat, false);
|
|
575
|
+
const scale = texture.normalScale ?? 1;
|
|
576
|
+
material.normalScale = new Vector2(scale, scale);
|
|
577
|
+
}
|
|
578
|
+
if (texture.roughness) loadMap(material, "roughnessMap", texture.roughness, repeat, false);
|
|
579
|
+
if (texture.ao) loadMap(material, "aoMap", texture.ao, repeat, false);
|
|
580
|
+
material.needsUpdate = true;
|
|
581
|
+
return texture.ao !== void 0;
|
|
582
|
+
};
|
|
583
|
+
/**
|
|
584
|
+
* Release one reference to a texture assigned by `applyTexture`. Shared
|
|
585
|
+
* textures stay alive while another material still samples them.
|
|
586
|
+
*/
|
|
587
|
+
const releaseTexture = (texture) => {
|
|
588
|
+
cache.release(texture);
|
|
589
|
+
};
|
|
590
|
+
//#endregion
|
|
481
591
|
//#region src/SceneManager.ts
|
|
592
|
+
const TEXTURE_SLOTS = [
|
|
593
|
+
"map",
|
|
594
|
+
"normalMap",
|
|
595
|
+
"roughnessMap",
|
|
596
|
+
"aoMap",
|
|
597
|
+
"metalnessMap"
|
|
598
|
+
];
|
|
482
599
|
const disposeObject = (object) => {
|
|
600
|
+
const materials = /* @__PURE__ */ new Set();
|
|
483
601
|
object.traverse((child) => {
|
|
484
602
|
const video = child.userData?.video;
|
|
485
603
|
if (video) {
|
|
@@ -489,21 +607,18 @@ const disposeObject = (object) => {
|
|
|
489
607
|
}
|
|
490
608
|
const mesh = child;
|
|
491
609
|
mesh.geometry?.dispose?.();
|
|
492
|
-
const dispose = (m) => {
|
|
493
|
-
const material = m;
|
|
494
|
-
for (const slot of [
|
|
495
|
-
"map",
|
|
496
|
-
"normalMap",
|
|
497
|
-
"roughnessMap",
|
|
498
|
-
"aoMap",
|
|
499
|
-
"metalnessMap"
|
|
500
|
-
]) (material?.[slot])?.dispose?.();
|
|
501
|
-
material?.dispose?.();
|
|
502
|
-
};
|
|
503
610
|
const material = mesh.material;
|
|
504
|
-
if (Array.isArray(material)) material.
|
|
505
|
-
else
|
|
611
|
+
if (Array.isArray(material)) for (const entry of material) materials.add(entry);
|
|
612
|
+
else if (material) materials.add(material);
|
|
506
613
|
});
|
|
614
|
+
for (const entry of materials) {
|
|
615
|
+
const material = entry;
|
|
616
|
+
for (const slot of TEXTURE_SLOTS) {
|
|
617
|
+
const texture = material[slot];
|
|
618
|
+
if (texture) releaseTexture(texture);
|
|
619
|
+
}
|
|
620
|
+
material.dispose?.();
|
|
621
|
+
}
|
|
507
622
|
};
|
|
508
623
|
/**
|
|
509
624
|
* Owns the live three.js scene graph and keeps it in sync with a `Scene`
|
|
@@ -693,7 +808,8 @@ function createGallery(scene, options) {
|
|
|
693
808
|
centerArtwork: () => {},
|
|
694
809
|
release: () => {},
|
|
695
810
|
reset: () => {},
|
|
696
|
-
isAnimating: () => false
|
|
811
|
+
isAnimating: () => false,
|
|
812
|
+
isFocused: () => false
|
|
697
813
|
};
|
|
698
814
|
const headTracking = apis.get(CARDBOARD_SYSTEM);
|
|
699
815
|
const initialFlags = controlSettings.get();
|
|
@@ -739,7 +855,7 @@ function createGallery(scene, options) {
|
|
|
739
855
|
const gated = s.artworks ?? [];
|
|
740
856
|
return new Map(gated.filter((a) => a.infoRadius !== void 0).map((a) => [a.id, a.infoRadius]));
|
|
741
857
|
};
|
|
742
|
-
const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating());
|
|
858
|
+
const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating() && !cameraApi.isFocused());
|
|
743
859
|
interaction.setRadii(radiiOf(scene));
|
|
744
860
|
interaction.attach();
|
|
745
861
|
const clock = new Clock();
|
|
@@ -952,50 +1068,6 @@ function createGallery(scene, options) {
|
|
|
952
1068
|
};
|
|
953
1069
|
}
|
|
954
1070
|
//#endregion
|
|
955
|
-
//#region src/builders/texture.ts
|
|
956
|
-
const textureLoader = new TextureLoader();
|
|
957
|
-
/** Configure a loaded texture: tiling + color space (sRGB only for color maps). */
|
|
958
|
-
const configure = (tex, repeat, srgb) => {
|
|
959
|
-
tex.wrapS = RepeatWrapping;
|
|
960
|
-
tex.wrapT = RepeatWrapping;
|
|
961
|
-
tex.repeat.set(repeat[0], repeat[1]);
|
|
962
|
-
tex.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;
|
|
963
|
-
tex.needsUpdate = true;
|
|
964
|
-
};
|
|
965
|
-
/**
|
|
966
|
-
* Load one map and assign it to `material[slot]` once ready. `.exr` maps go
|
|
967
|
-
* through EXRLoader, imported on demand so it only ships in the bundle when a
|
|
968
|
-
* scene actually uses an EXR map; everything else uses the image loader.
|
|
969
|
-
*/
|
|
970
|
-
const loadMap = (material, slot, src, repeat, srgb) => {
|
|
971
|
-
const assign = (tex) => {
|
|
972
|
-
configure(tex, repeat, srgb);
|
|
973
|
-
material[slot] = tex;
|
|
974
|
-
material.needsUpdate = true;
|
|
975
|
-
};
|
|
976
|
-
if (src.toLowerCase().endsWith(".exr")) import("three/examples/jsm/loaders/EXRLoader.js").then(({ EXRLoader }) => {
|
|
977
|
-
new EXRLoader().load(src, assign);
|
|
978
|
-
});
|
|
979
|
-
else textureLoader.load(src, assign);
|
|
980
|
-
};
|
|
981
|
-
/**
|
|
982
|
-
* Apply a `SurfaceTexture` to a standard material. Returns whether a second UV
|
|
983
|
-
* set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must
|
|
984
|
-
* provide one.
|
|
985
|
-
*/
|
|
986
|
-
const applyTexture = (material, texture) => {
|
|
987
|
-
const repeat = texture.repeat ?? [1, 1];
|
|
988
|
-
loadMap(material, "map", texture.src, repeat, true);
|
|
989
|
-
if (texture.normal) {
|
|
990
|
-
loadMap(material, "normalMap", texture.normal, repeat, false);
|
|
991
|
-
const s = texture.normalScale ?? 1;
|
|
992
|
-
material.normalScale = new Vector2(s, s);
|
|
993
|
-
}
|
|
994
|
-
if (texture.roughness) loadMap(material, "roughnessMap", texture.roughness, repeat, false);
|
|
995
|
-
if (texture.ao) loadMap(material, "aoMap", texture.ao, repeat, false);
|
|
996
|
-
return texture.ao !== void 0;
|
|
997
|
-
};
|
|
998
|
-
//#endregion
|
|
999
1071
|
export { COLLIDERS, CapabilityRegistry, ControlSettings, DEFAULT_CONTROL_FLAGS, EDGES, EYE_HEIGHT, InputBus, InteractionController, MODE_PRESETS, ROAM_CONTROL, SceneManager, TERRAIN, applyMode, applyTexture, createGallery, isSystemPlugin, modeOf };
|
|
1000
1072
|
|
|
1001
1073
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["FLAG_KEYS","ThreeScene"],"sources":["../src/environment/EnvironmentController.ts","../src/interaction/InteractionController.ts","../src/input/InputBus.ts","../src/input/controlSettings.ts","../src/input/controlStorage.ts","../src/plugin/CapabilityRegistry.ts","../src/plugin/systemPlugin.ts","../src/plugin/roamControl.ts","../src/plugin/terrain.ts","../src/plugin/colliders.ts","../src/plugin/edges.ts","../src/plugin/eyeHeight.ts","../src/SceneManager.ts","../src/createGallery.ts","../src/builders/texture.ts"],"sourcesContent":["import { EquirectangularReflectionMapping, type Scene as ThreeScene, type Texture } from \"three\";\nimport type { Environment } from \"@scenography/scene\";\n\n/** Load an equirectangular HDRI, picking the loader by extension. */\nconst loadHDRI = (src: string, onLoad: (tex: Texture) => void): void => {\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, onLoad);\n });\n } else {\n void import(\"three/examples/jsm/loaders/RGBELoader.js\").then(({ RGBELoader }) => {\n new RGBELoader().load(src, onLoad);\n });\n }\n};\n\n/**\n * Owns the scene's HDRI sky / image-based lighting and keeps it in sync with the\n * `Environment` definition. Loads the equirectangular map on demand (its loader\n * ships in its own chunk) and assigns it to `scene.background` and/or\n * `scene.environment`. Reconciles by a JSON hash so hot edits only reload when\n * the definition actually changes.\n */\nexport class EnvironmentController {\n private readonly scene: ThreeScene;\n private hash: string | null = null;\n private texture: Texture | null = null;\n\n constructor(scene: ThreeScene) {\n this.scene = scene;\n }\n\n reconcile(env: Environment | undefined): void {\n const hash = env ? JSON.stringify(env) : null;\n if (hash === this.hash) return;\n this.hash = hash;\n this.clear();\n if (!env) return;\n\n loadHDRI(env.src, (tex) => {\n // A newer reconcile superseded this load before it finished.\n if (this.hash !== hash) {\n tex.dispose();\n return;\n }\n tex.mapping = EquirectangularReflectionMapping;\n this.texture = tex;\n if (env.background ?? true) {\n this.scene.background = tex;\n this.scene.backgroundIntensity = env.intensity ?? 1;\n }\n if (env.lighting ?? true) this.scene.environment = tex;\n });\n }\n\n /** Set the visible sky background brightness (0 = black). Live, for behavior. */\n setBackgroundIntensity(value: number): void {\n this.scene.backgroundIntensity = value;\n }\n\n private clear(): void {\n this.scene.background = null;\n this.scene.environment = null;\n this.texture?.dispose();\n this.texture = null;\n }\n\n dispose(): void {\n this.clear();\n this.hash = null;\n }\n}\n","import {\n BoxGeometry,\n Group,\n Mesh,\n MeshBasicMaterial,\n Quaternion,\n Raycaster,\n Vector2,\n Vector3,\n type Object3D,\n type PerspectiveCamera,\n type Scene as ThreeScene,\n} from \"three\";\n\nconst HIGHLIGHT_COLOR = \"#ef4444\";\nconst HIGHLIGHT_THICKNESS = 1.5; // world units (cm)\nconst HIGHLIGHT_MARGIN = 1.5; // gap between the piece and the frame\nconst CLICK_SLOP = 4; // px of movement still counted as a click, not a drag\n\ninterface PickInfo {\n width: number;\n height: number;\n frontOffset: number;\n}\n\ninterface Pick {\n id: string;\n holder: Object3D;\n info: PickInfo;\n}\n\n/**\n * Pointer picking for artworks: hovering a piece's click target (its label, or\n * the piece itself when it has none) draws a pulsing red frame and shows a\n * pointer cursor; clicking it emits `artwork:select { id }`. Targets are marked\n * in the scene graph (`userData.pickTarget`/`pickInfo`) by the artwork builder,\n * so this just raycasts and walks up to find them. An optional per-id radius\n * gates interaction to when the camera is reasonably close.\n */\nexport class InteractionController {\n private readonly raycaster = new Raycaster();\n private readonly pointer = new Vector2();\n private over = false;\n private downPos: { x: number; y: number } | null = null;\n private radii = new Map<string, number>();\n private hovered: string | null = null;\n private time = 0;\n\n private readonly highlight = new Group();\n private highlightMat: MeshBasicMaterial | null = null;\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n private readonly camera: PerspectiveCamera,\n private readonly canvas: HTMLCanvasElement,\n private readonly scene: ThreeScene,\n private readonly emit: (event: string, payload?: unknown) => void,\n private readonly canInteract: () => boolean,\n ) {\n this.highlight.visible = false;\n this.scene.add(this.highlight);\n }\n\n /** Per-artwork max interaction distance (XZ). Absent id = no distance gate. */\n setRadii(radii: Map<string, number>): void {\n this.radii = radii;\n }\n\n attach(): void {\n const bind = (type: string, listener: EventListener): void => {\n this.canvas.addEventListener(type, listener);\n this.handlers.push([this.canvas, type, listener]);\n };\n bind(\"pointermove\", (e) => this.onMove(e as PointerEvent));\n bind(\"pointerdown\", (e) => this.onDown(e as PointerEvent));\n bind(\"pointerup\", (e) => this.onUp(e as PointerEvent));\n bind(\"pointerleave\", () => {\n this.over = false;\n });\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);\n this.handlers.length = 0;\n this.scene.remove(this.highlight);\n this.clearHighlight();\n }\n\n private setPointer(e: PointerEvent): void {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.set(\n ((e.clientX - rect.left) / rect.width) * 2 - 1,\n -((e.clientY - rect.top) / rect.height) * 2 + 1,\n );\n this.over = true;\n }\n\n private onMove(e: PointerEvent): void {\n this.setPointer(e);\n }\n\n private onDown(e: PointerEvent): void {\n this.downPos = { x: e.clientX, y: e.clientY };\n }\n\n private onUp(e: PointerEvent): void {\n const down = this.downPos;\n this.downPos = null;\n if (!down || !this.canInteract()) return;\n if (Math.hypot(e.clientX - down.x, e.clientY - down.y) > CLICK_SLOP) return; // a drag, not a click\n this.setPointer(e);\n const pick = this.raycast();\n if (pick) this.emit(\"artwork:select\", { id: pick.id });\n }\n\n /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */\n private raycast(): Pick | null {\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const hits = this.raycaster.intersectObjects(this.scene.children, true);\n for (const hit of hits) {\n let node: Object3D | null = hit.object;\n while (node && node.userData.pickTarget === undefined) node = node.parent;\n if (!node) continue;\n const id = node.userData.pickTarget as string;\n const radius = this.radii.get(id);\n if (radius !== undefined) {\n const p = node.getWorldPosition(new Vector3());\n if (Math.hypot(this.camera.position.x - p.x, this.camera.position.z - p.z) > radius) continue;\n }\n return { id, holder: node, info: node.userData.pickInfo as PickInfo };\n }\n return null;\n }\n\n /** Recompute hover state and animate the highlight. Call once per frame. */\n update(deltaMs: number): void {\n this.time += deltaMs;\n const pick = this.over && this.canInteract() ? this.raycast() : null;\n\n if ((pick?.id ?? null) !== this.hovered) {\n this.hovered = pick?.id ?? null;\n this.canvas.style.cursor = pick ? \"pointer\" : \"\";\n if (pick) this.buildHighlight(pick);\n else this.highlight.visible = false;\n }\n\n if (this.highlight.visible && this.highlightMat) {\n this.highlightMat.opacity = 0.45 + 0.45 * (0.5 + 0.5 * Math.sin(this.time / 320));\n }\n }\n\n private clearHighlight(): void {\n for (const child of [...this.highlight.children]) {\n this.highlight.remove(child);\n (child as Mesh).geometry?.dispose();\n }\n this.highlightMat?.dispose();\n this.highlightMat = null;\n }\n\n /** Position a fresh pulsing red frame over the picked target's front face. */\n private buildHighlight(pick: Pick): void {\n this.clearHighlight();\n const mat = new MeshBasicMaterial({ color: HIGHLIGHT_COLOR, transparent: true, depthTest: false });\n this.highlightMat = mat;\n\n const w = pick.info.width + 2 * HIGHLIGHT_MARGIN;\n const h = pick.info.height + 2 * HIGHLIGHT_MARGIN;\n const t = HIGHLIGHT_THICKNESS;\n // Four bars forming a border (horizontals span the corners; verticals fill in).\n const bars: [number, number, number, number][] = [\n [w + 2 * t, t, 0, h / 2 + t / 2],\n [w + 2 * t, t, 0, -(h / 2 + t / 2)],\n [t, h, -(w / 2 + t / 2), 0],\n [t, h, w / 2 + t / 2, 0],\n ];\n for (const [bw, bh, x, y] of bars) {\n const bar = new Mesh(new BoxGeometry(bw, bh, 0.5), mat);\n bar.position.set(x, y, 0);\n bar.renderOrder = 999;\n this.highlight.add(bar);\n }\n\n const pos = pick.holder.getWorldPosition(new Vector3());\n const quat = pick.holder.getWorldQuaternion(new Quaternion());\n const normal = new Vector3(0, 0, 1).applyQuaternion(quat);\n pos.addScaledVector(normal, pick.info.frontOffset);\n this.highlight.position.copy(pos);\n this.highlight.quaternion.copy(quat);\n this.highlight.visible = true;\n }\n}\n","/**\n * The shared analog-input channel: the current movement and look intent, each\n * axis in `-1..1` and held until a producer changes it. Producers (on-screen\n * joysticks in the overlay, a gamepad…) write to it; whatever camera system is\n * active reads it and interprets it its own way — free-roam walks with it, a\n * fixed camera might only use the look axes to glance around. This is what makes\n * the joysticks reusable across camera types. Pure state — no three.js.\n */\nexport class InputBus {\n /** Forward (`+1`) / backward (`-1`). */\n moveForward = 0;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe = 0;\n /** Look right (`+1`) / left (`-1`). */\n lookX = 0;\n /** Look up (`+1`) / down (`-1`). */\n lookY = 0;\n\n /** Set the movement intent (forward, strafe). */\n setMove(forward: number, strafe = 0): void {\n this.moveForward = forward;\n this.moveStrafe = strafe;\n }\n\n /** Set the look intent (turn, pitch). */\n setLook(x: number, y = 0): void {\n this.lookX = x;\n this.lookY = y;\n }\n}\n","/**\n * The visitor's camera-control preferences: a set of orthogonal capability flags\n * plus a derived, friendly \"mode\". Pure state — no three.js, no DOM. The engine\n * wires the flags to effects (gyroscope head-look, stereo split); consumers read\n * them (the analog look-invert axes); the overlay drives it from a mode picker\n * (the `ScanEye` button) and the settings panel. Persistence and device\n * feature-detection live in those callers, not here, so this stays trivially\n * unit-testable.\n */\n\n/**\n * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a\n * mode fixes; the two `invertLook*` axes are independent look-feel preferences\n * (some people feel the right stick inverted), kept separate from the mode.\n */\nexport interface ControlFlags {\n /** Orient the camera from the device gyroscope (head-look). */\n gyro: boolean;\n /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */\n splitScreen: boolean;\n /** Show the on-screen virtual joysticks. */\n joysticks: boolean;\n /** Invert the horizontal (turn) axis of analog look. */\n invertLookX: boolean;\n /** Invert the vertical (pitch) axis of analog look. */\n invertLookY: boolean;\n}\n\n/** A named preset over the mode-defining flags; `custom` when none matches. */\nexport type ControlMode = \"touch\" | \"vr\" | \"cardboard\" | \"custom\";\n\n/** The three flags a mode fixes (the look-invert axes stay a separate preference). */\ntype ModeFlags = Pick<ControlFlags, \"gyro\" | \"splitScreen\" | \"joysticks\">;\n\n/**\n * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single\n * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze\n * select (no joysticks). The move-only single stick in VR is derived downstream\n * from `gyro && !splitScreen`, so it needs no flag of its own.\n */\nexport const MODE_PRESETS: Record<Exclude<ControlMode, \"custom\">, ModeFlags> = {\n touch: { gyro: false, splitScreen: false, joysticks: true },\n vr: { gyro: true, splitScreen: false, joysticks: true },\n cardboard: { gyro: true, splitScreen: true, joysticks: false },\n};\n\n/** Default flags: plain touch, no look inversion. */\nexport const DEFAULT_CONTROL_FLAGS: ControlFlags = {\n ...MODE_PRESETS.touch,\n invertLookX: false,\n invertLookY: false,\n};\n\nconst FLAG_KEYS: readonly (keyof ControlFlags)[] = [\n \"gyro\",\n \"splitScreen\",\n \"joysticks\",\n \"invertLookX\",\n \"invertLookY\",\n];\n\nconst equalFlags = (a: ControlFlags, b: ControlFlags): boolean =>\n FLAG_KEYS.every((key) => a[key] === b[key]);\n\n/** The mode a flag set represents, or `custom` when no preset matches. */\nexport function modeOf(flags: ControlFlags): ControlMode {\n for (const mode of [\"touch\", \"vr\", \"cardboard\"] as const) {\n const preset = MODE_PRESETS[mode];\n if (\n flags.gyro === preset.gyro &&\n flags.splitScreen === preset.splitScreen &&\n flags.joysticks === preset.joysticks\n )\n return mode;\n }\n return \"custom\";\n}\n\n/** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */\nexport function applyMode(\n flags: ControlFlags,\n mode: Exclude<ControlMode, \"custom\">,\n): ControlFlags {\n return { ...flags, ...MODE_PRESETS[mode] };\n}\n\n/**\n * Live control-preferences state: holds the flags, derives the mode, and notifies\n * subscribers on every real change. Callers apply the effects — the engine wires\n * gyro and split, the overlay redraws — so this object is just the shared source\n * of truth they all read and write.\n */\nexport class ControlSettings {\n private flags: ControlFlags;\n private readonly subscribers = new Set<(flags: ControlFlags) => void>();\n\n constructor(initial: Partial<ControlFlags> = {}) {\n this.flags = { ...DEFAULT_CONTROL_FLAGS, ...initial };\n }\n\n /** The current flags (a copy — mutate through `setMode` / `setFlag`). */\n get(): ControlFlags {\n return { ...this.flags };\n }\n\n /** The mode the current flags represent (`custom` when hand-tuned). */\n getMode(): ControlMode {\n return modeOf(this.flags);\n }\n\n /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */\n setMode(mode: Exclude<ControlMode, \"custom\">): void {\n this.replace(applyMode(this.flags, mode));\n }\n\n /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void {\n this.replace({ ...this.flags, [key]: value });\n }\n\n /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */\n subscribe(fn: (flags: ControlFlags) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n private replace(next: ControlFlags): void {\n if (equalFlags(this.flags, next)) return;\n this.flags = next;\n const snapshot = this.get();\n for (const fn of this.subscribers) fn(snapshot);\n }\n}\n","import { DEFAULT_CONTROL_FLAGS, type ControlFlags } from \"./controlSettings\";\n\n/**\n * localStorage persistence for the control flags, kept out of the pure\n * `ControlSettings` so that stays DOM-free and unit-testable. Both functions are\n * best-effort and guarded: no `localStorage` (SSR, privacy mode) or malformed\n * data just falls back to defaults, never throws.\n */\nconst STORAGE_KEY = \"scenography:controls\";\n\nconst FLAG_KEYS = Object.keys(DEFAULT_CONTROL_FLAGS) as (keyof ControlFlags)[];\n\n/** Read persisted control flags; `null` when absent, unavailable or malformed. */\nexport function loadControlFlags(): Partial<ControlFlags> | null {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const flags: Partial<ControlFlags> = {};\n // Copy only known boolean keys, so stale/garbage fields can't leak in.\n for (const key of FLAG_KEYS) {\n if (typeof parsed[key] === \"boolean\") flags[key] = parsed[key] as boolean;\n }\n return flags;\n } catch {\n return null;\n }\n}\n\n/** Persist control flags (best-effort; ignores a full/blocked store). */\nexport function saveControlFlags(flags: ControlFlags): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(flags));\n } catch {\n /* storage unavailable or full — persistence is best-effort */\n }\n}\n","/**\n * Shared runtime contracts between plugins, keyed by name. A plugin (or the\n * kernel) *provides* a capability — e.g. the floor provides `terrain` — and\n * other plugins *consume* it if present, without depending on the provider's\n * package. Each name keeps a monotonic version counter so consumers can hash\n * their dependencies cheaply and rebuild when a provider changes.\n */\nexport class CapabilityRegistry {\n private readonly values = new Map<string, unknown>();\n private readonly versions = new Map<string, number>();\n /** Last change-token seen per name, to suppress redundant version bumps. */\n private readonly tokens = new Map<string, string>();\n\n /**\n * Publish (or replace) the capability under `name`. Pass a `token` (a cheap\n * hash of the inputs) to dedupe: re-providing with an unchanged token is a\n * no-op, so consumers that depend on this capability don't rebuild when the\n * provider recomputes an identical value.\n */\n provide<T>(name: string, value: T, token?: string): void {\n if (token !== undefined && this.values.has(name) && this.tokens.get(name) === token) return;\n this.values.set(name, value);\n if (token !== undefined) this.tokens.set(name, token);\n else this.tokens.delete(name);\n this.bump(name);\n }\n\n /** Remove the capability; consumers see `undefined` and a version bump. */\n revoke(name: string): void {\n if (this.values.delete(name)) {\n this.tokens.delete(name);\n this.bump(name);\n }\n }\n\n get<T>(name: string): T | undefined {\n return this.values.get(name) as T | undefined;\n }\n\n /** Monotonic change counter for `name` (`0` if never provided). */\n version(name: string): number {\n return this.versions.get(name) ?? 0;\n }\n\n private bump(name: string): void {\n this.versions.set(name, (this.versions.get(name) ?? 0) + 1);\n }\n}\n","import type {\n Object3D,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n} from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport type { CapabilityRegistry } from \"./CapabilityRegistry\";\nimport type { ControlFlags, InputBus } from \"../input\";\n\n/**\n * What the kernel hands a system on setup. A *system* plugin — unlike an element\n * plugin — produces no scene object; it drives shared runtime state (the camera,\n * input…) each frame. The camera is a kernel primitive (the renderer needs one),\n * so systems receive it and drive it, publishing/consuming capabilities to\n * cooperate (e.g. the camera plugin publishes `roamControl`).\n */\nexport interface SystemContext {\n /** The current scene (its data keys, start pose, boundary…); also `setScene` on hot-swap. */\n scene: Scene;\n /** The render camera the system may drive. */\n camera: PerspectiveCamera;\n /** The canvas, for pointer/keyboard listeners. */\n canvas: HTMLElement;\n /** The three.js scene graph, for systems that manage their own objects (sky…). */\n scene3d: ThreeScene;\n /** The renderer, for systems that need it (e.g. sky IBL generation). */\n renderer: WebGLRenderer;\n /** Shared analog input (joysticks feed it); systems interpret it. */\n input: InputBus;\n /** The visitor's current control flags (e.g. look-invert axes), read live. */\n controlFlags: () => ControlFlags;\n /** Capability registry for cross-plugin contracts (terrain, colliders…). */\n capabilities: CapabilityRegistry;\n /** Emit a gallery event. */\n emit: (event: string, payload?: unknown) => void;\n /** Resolve a live scene object by id (e.g. for a framing system). */\n resolve: (id: string) => Object3D | undefined;\n}\n\n/** A running system: its per-frame update, hot-swap and teardown hooks. */\nexport interface SystemInstance<Api = unknown> {\n /** Advance the system by the frame delta (ms). */\n update?(deltaMs: number): void;\n /** React to a hot-swapped scene definition. */\n setScene?(scene: Scene): void;\n /** Tear down (detach listeners, revoke capabilities). */\n dispose?(): void;\n /**\n * Public API the system exposes to userland/overlay. The kernel maps known\n * system names onto `ctx` (e.g. a `focus` system's api → `ctx.camera`).\n */\n api?: Api;\n}\n\n/**\n * A system plugin: sets up once against the kernel handles and returns its\n * running instance. Registered alongside element plugins in `plugins`; the\n * kernel tells them apart by shape (`setup` vs `build`).\n */\nexport interface SystemPlugin<Api = unknown> {\n name: string;\n setup(ctx: SystemContext): SystemInstance<Api>;\n}\n\n/** Narrow a mixed plugin to a system plugin (has `setup`, not `build`). */\nexport const isSystemPlugin = (plugin: unknown): plugin is SystemPlugin =>\n typeof plugin === \"object\" && plugin !== null && \"setup\" in plugin;\n","import type { Vector3 } from \"three\";\n\n/** Capability name under which a roaming camera system exposes pose control. */\nexport const ROAM_CONTROL = \"roamControl\";\n\n/**\n * Handle a camera plugin publishes so other camera systems can cooperate with\n * it — chiefly a framing/tour system that suspends free-roam while it animates\n * the camera, then reads/writes the pose to hand control back cleanly.\n */\nexport interface RoamControl {\n /** Enable/disable free-roam input (disabled while another system owns the camera). */\n setEnabled(enabled: boolean): void;\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number };\n /** Force the pose so free-roam resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void;\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which the floor publishes its terrain. */\nexport const TERRAIN = \"terrain\";\n\n/**\n * The geometry a terrain consumer relies on — the fields needed to sample the\n * same ground a floor was displaced by (e.g. the water shader). A floor\n * definition satisfies this structurally; the kernel stays floor-agnostic.\n */\nexport interface TerrainDefinition {\n /** Extent on the ground plane, `[width (x), depth (z)]`. */\n size: Vec2;\n /** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */\n center?: Vec2;\n /** Base vertical position (world Y). Defaults to `0`. */\n y?: number;\n /** Grayscale heightmap URL that displaces the ground. */\n heightmap?: string;\n /** Max displacement (world units) at white in the heightmap. */\n heightScale?: number;\n}\n\n/**\n * The walkable ground, published by the floor: its definition (for shaders that\n * sample the same heightmap), the live height function (heightmap-aware once the\n * image loads) and a promise that resolves once that sampling is in effect.\n */\nexport interface TerrainCapability {\n definition: TerrainDefinition;\n heightAt: (x: number, z: number) => number;\n /** Resolves once `heightAt` reflects the heightmap (immediately when there is none). */\n ready: Promise<void>;\n}\n","/** Capability name under which walkable-space obstacles are published. */\nexport const COLLIDERS = \"colliders\";\n\n/**\n * A collision obstacle as an oriented box on the XZ plane. The camera controls\n * consume these to keep the visitor out of walls (and any other plugin that\n * wants to block movement can provide more of them). Purely a movement contract\n * — no geometry, no three.js.\n */\nexport interface Collider {\n /** Box center `[x, z]`. */\n cx: number;\n cz: number;\n /** Unit direction along the box's length. */\n dx: number;\n dz: number;\n /** Half the box length. */\n halfLength: number;\n /** Half the box thickness. */\n halfThickness: number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which named wall edges are published for hanging. */\nexport const EDGES = \"edges\";\n\n/** A named anchor an artwork can hang on: a wall segment resolved to geometry. */\nexport interface EdgeAnchor {\n a: Vec2;\n b: Vec2;\n height: number;\n thickness: number;\n}\n\n/** The `name -> anchor` index a provider (e.g. the wall plugin) publishes. */\nexport type EdgeIndex = ReadonlyMap<string, EdgeAnchor>;\n","/**\n * Capability name under which the visitor's eye height (world units) is\n * published — the default vertical placement for wall-hung artworks. The kernel\n * provides it from the camera start; a camera plugin will own it later.\n */\nexport const EYE_HEIGHT = \"eyeHeight\";\n","import type { Object3D, PerspectiveCamera, Scene as ThreeScene, WebGLRenderer } from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport {\n CapabilityRegistry,\n EYE_HEIGHT,\n type BuiltElement,\n type EnginePlugin,\n} from \"./plugin\";\n\n/** A mounted plugin element: what the plugin built plus its content hash. */\ntype MountedElement = BuiltElement & { hash: string };\n\nconst disposeObject = (object: Object3D): void => {\n object.traverse((child) => {\n // Release any HTML video element backing a VideoTexture.\n const video = child.userData?.video as HTMLVideoElement | undefined;\n if (video) {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n }\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const dispose = (m: unknown): void => {\n const material = m as { dispose?: () => void } & Record<string, unknown>;\n for (const slot of [\"map\", \"normalMap\", \"roughnessMap\", \"aoMap\", \"metalnessMap\"]) {\n (material?.[slot] as { dispose?: () => void } | undefined)?.dispose?.();\n }\n material?.dispose?.();\n };\n const material = mesh.material;\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 plugins: EnginePlugin[];\n private readonly capabilities: CapabilityRegistry;\n /** Mounted plugin elements, per kind, keyed by element id (\"\" for singletons). */\n private readonly pluginElements = new Map<string, Map<string, MountedElement>>();\n\n constructor(\n root: ThreeScene,\n plugins: EnginePlugin[] = [],\n capabilities: CapabilityRegistry = new CapabilityRegistry(),\n ) {\n this.root = root;\n this.plugins = plugins;\n this.capabilities = capabilities;\n }\n\n reconcile(scene: Scene): void {\n // Eye height (default hang height for wall art) — a camera concern the\n // kernel owns for now; a camera plugin will publish it later.\n const eye = scene.camera.start.position[1];\n this.capabilities.provide(EYE_HEIGHT, eye, String(eye));\n // Plugins go last so kernel-provided capabilities (terrain, eyeHeight) are fresh.\n this.reconcilePlugins(scene);\n }\n\n /**\n * Reconcile every registered plugin's elements by content: an element\n * rebuilds when its definition changes or when a capability it depends on\n * was re-provided (the capability versions are part of the hash).\n */\n private reconcilePlugins(scene: Scene): void {\n for (const plugin of this.plugins) {\n const { kind, cardinality } = plugin.data;\n let mounted = this.pluginElements.get(kind);\n if (!mounted) {\n mounted = new Map();\n this.pluginElements.set(kind, mounted);\n }\n\n const raw = (scene as unknown as Record<string, unknown>)[kind];\n const items: Array<[string, unknown]> =\n cardinality === \"many\"\n ? Array.isArray(raw)\n ? raw.map((item, i) => [String((item as { id?: string }).id ?? i), item])\n : []\n : raw !== undefined\n ? [[\"\", raw]]\n : [];\n const deps = (plugin.dependsOn ?? []).map((name) => this.capabilities.version(name));\n\n const seen = new Set<string>();\n for (const [key, value] of items) {\n seen.add(key);\n const hash = JSON.stringify({ value, deps });\n const existing = mounted.get(key);\n if (existing?.hash === hash) continue;\n if (existing) this.removePluginElement(mounted, key, existing);\n const built = plugin.build(value as never, { capabilities: this.capabilities });\n if (built.object) this.root.add(built.object);\n mounted.set(key, { ...built, hash });\n }\n for (const [key, entry] of mounted)\n if (!seen.has(key)) this.removePluginElement(mounted, key, entry);\n\n // Publish this plugin's aggregate capabilities now that its elements are\n // in sync, so a later plugin that consumes them builds against fresh data.\n plugin.provide?.(\n items.map(([, value]) => value),\n { capabilities: this.capabilities },\n );\n }\n }\n\n private removePluginElement(\n mounted: Map<string, MountedElement>,\n key: string,\n entry: MountedElement,\n ): void {\n if (entry.object) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n entry.dispose?.();\n mounted.delete(key);\n }\n\n\n /** Advance time-based plugin elements (e.g. water ripple). Call once per frame. */\n updateElements(deltaMs: number): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.update?.(deltaMs);\n }\n\n /** Run every element's auxiliary pre-render pass (e.g. water reflection). */\n beforeRender(renderer: WebGLRenderer, scene: ThreeScene, camera: PerspectiveCamera): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.beforeRender?.(renderer, scene, camera);\n }\n\n\n /** Look up the live three.js object for any plugin element `id`. */\n get(id: string): Object3D | undefined {\n for (const mounted of this.pluginElements.values()) {\n const object = mounted.get(id)?.object;\n if (object) return object;\n }\n return undefined;\n }\n\n /** Live objects of one plugin kind, keyed by `id` — for that plugin's controller. */\n elementsOf(kind: string): ReadonlyMap<string, Object3D> {\n const out = new Map<string, Object3D>();\n const mounted = this.pluginElements.get(kind);\n if (mounted) for (const [id, entry] of mounted) if (entry.object) out.set(id, entry.object);\n return out;\n }\n\n dispose(): void {\n for (const [kind, mounted] of this.pluginElements) {\n for (const [key, entry] of mounted) this.removePluginElement(mounted, key, entry);\n this.pluginElements.delete(kind);\n }\n }\n}\n","import {\n Clock,\n HalfFloatType,\n PCFShadowMap,\n PerspectiveCamera,\n Scene as ThreeScene,\n StereoCamera,\n Vector2,\n WebGLRenderer,\n WebGLRenderTarget,\n type Object3D,\n} from \"three\";\nimport type { EffectComposer } from \"three/examples/jsm/postprocessing/EffectComposer.js\";\nimport type { Pass } from \"three/examples/jsm/postprocessing/Pass.js\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport { EnvironmentController } from \"./environment\";\nimport type { CameraApi } from \"./camera\";\nimport type { LightsApi, MediaController, SkyApi, VideoApi } from \"./api\";\nimport { InteractionController } from \"./interaction\";\nimport {\n ControlSettings,\n InputBus,\n MODE_PRESETS,\n loadControlFlags,\n saveControlFlags,\n type ControlFlags,\n type ControlMode,\n} from \"./input\";\nimport {\n CapabilityRegistry,\n isSystemPlugin,\n type ElementController,\n type EnginePlugin,\n type SystemInstance,\n type SystemPlugin,\n} from \"./plugin\";\nimport { SceneManager } from \"./SceneManager\";\n\n/** Maps a plugin kind/name onto the `ctx` slot its api fills. */\nconst CTX_SLOT = {\n camera: \"focus\",\n lights: \"lights\",\n video: \"videos\",\n sky: \"sky\",\n media: \"audio\",\n} as const;\n/** The system the cardboard render toggle drives (gyroscope head-look). */\nconst CARDBOARD_SYSTEM = \"cardboard\";\n\n/** MSAA sample count for antialiasing. `0` disables it. */\nexport type Antialias = 0 | 2 | 4 | 8;\n\n/** Toggleable rendering features. All can be flipped live via `setRender`. */\nexport interface RenderOptions {\n /**\n * Antialiasing as MSAA sample count (`0` off, `2`/`4`/`8`). Applied to the\n * post-process render target so it survives SSAO/bloom and toggles live,\n * unlike the canvas's native MSAA. Defaults to `4`.\n */\n antialias?: Antialias;\n /** Screen-space ambient occlusion as a post-process. Defaults to `false`. */\n ssao?: boolean;\n /**\n * Bloom strength: a soft glow bleeding out of the brightest areas. `0` off;\n * gentle around `0.3`, strong around `1`. Defaults to `0`.\n */\n bloom?: number;\n /** Real-time shadow mapping (PCF). Defaults to `true`. */\n shadows?: boolean;\n /**\n * Cardboard / stereoscopic mode: split the view into a left/right eye pair for\n * a phone-in-a-headset viewer. Skips post-processing while active. Defaults to\n * `false`.\n */\n cardboard?: boolean;\n /**\n * Interocular distance for cardboard mode, in world units. The physical ≈6.4 cm\n * is too much parallax to fuse comfortably through cardboard lenses, so this\n * defaults to `3`; tune it live for the viewer. `0` flattens to mono.\n */\n eyeSeparation?: number;\n}\n\nexport interface GalleryOptions extends RenderOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n /**\n * Runtime plugins: element plugins materialize a scene key; system plugins\n * (e.g. a camera) drive shared runtime state. Mixed freely — the kernel tells\n * them apart by shape.\n */\n plugins?: (EnginePlugin | SystemPlugin)[];\n}\n\nconst DEFAULT_EYE_SEPARATION = 3;\n\n/**\n * Drive the free-roam camera from outside the engine — e.g. on-screen touch\n * controls in the overlay. Inputs are analog (`-1..1`) and held until changed.\n */\nexport interface ControlsApi {\n /** Walk forward (`+1`) / back (`-1`); optional strafe right (`+1`) / left (`-1`). */\n setMoveInput(forward: number, strafe?: number): void;\n /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */\n setLookInput(x: number, y?: number): void;\n}\n\n/**\n * The visitor's camera-control preferences (view mode + capability flags). The\n * engine applies the effects that touch rendering/input — gyroscope head-look and\n * the stereo split — as the flags change; the overlay drives it from the mode\n * picker and settings, and reads it to show the right joysticks. See\n * `ControlSettings`.\n */\nexport interface ViewControlsApi {\n /** Current capability flags (a copy). */\n get(): ControlFlags;\n /** The mode the flags represent (`custom` when hand-tuned in settings). */\n getMode(): ControlMode;\n /** Switch to a mode's preset (keeps the look-invert axes). */\n setMode(mode: Exclude<ControlMode, \"custom\">): void;\n /** Set one capability flag (may drop the mode into `custom`). */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;\n /** Subscribe to changes; returns an unsubscribe. */\n subscribe(fn: (flags: ControlFlags) => void): () => void;\n}\n\n/** Control the HDRI sky / environment at runtime, for behavior code. */\nexport interface EnvironmentApi {\n /** Set the visible sky background brightness (`0` = black). */\n setBackgroundIntensity(value: number): void;\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/light `id`. */\n entity(id: string): Object3D | undefined;\n /** Control scene lights (dim, set intensity/color) by `id`. */\n readonly lights: LightsApi;\n /** Control background audio (play, pause, fade) by `id`. */\n readonly media: MediaController;\n /** Drive the camera externally (on-screen touch controls, etc.). */\n readonly controls: ControlsApi;\n /** The visitor's camera-control preferences (view mode + capability flags). */\n readonly viewControls: ViewControlsApi;\n /** Control the HDRI sky / environment at runtime. */\n readonly environment: EnvironmentApi;\n /** Drive the procedural sky's time of day. */\n readonly sky: SkyApi;\n /** Animate the camera to frame artworks (e.g. when an info panel opens/closes). */\n readonly camera: CameraApi;\n /** Control video screens (play, pause, show/hide) by `id`. */\n readonly video: VideoApi;\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 /** Run a handler every frame with the frame delta in ms; returns unsubscribe. */\n onFrame(handler: (deltaMs: number) => void): () => void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n /** Toggle rendering features (antialias, ssao, bloom, shadows) live. */\n setRender(options: RenderOptions): 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 // Native MSAA is off: antialiasing is an SMAA post-pass so it survives SSAO\n // and toggles live. PCF shadow filtering set up once; shadows enabled below.\n const renderer = new WebGLRenderer({ canvas, antialias: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n renderer.shadowMap.type = PCFShadowMap;\n\n // Live-toggleable render features.\n const render: Required<RenderOptions> = {\n antialias: options.antialias ?? 4,\n ssao: options.ssao ?? false,\n bloom: options.bloom ?? 0,\n shadows: options.shadows ?? true,\n cardboard: options.cardboard ?? false,\n eyeSeparation: options.eyeSeparation ?? DEFAULT_EYE_SEPARATION,\n };\n\n // Stereoscopic (cardboard) rendering. StereoCamera is core three (not the\n // WebGL-only StereoEffect addon); we render each eye with a scissored viewport,\n // which is portable to WebGPU. aspect 0.5 keeps each half undistorted.\n const stereo = new StereoCamera();\n stereo.aspect = 0.5;\n stereo.eyeSep = render.eyeSeparation;\n const stereoSize = new Vector2();\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(scene.camera.fov ?? 55, width / height, 0.1, 5000);\n const { position } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n // Split element plugins (materialize a scene key) from system plugins (drive\n // shared state like the camera). Elements go to the manager; systems are set\n // up below, once elements have published their capabilities.\n const allPlugins = options.plugins ?? [];\n const systemPlugins = allPlugins.filter(isSystemPlugin);\n const elementPlugins = allPlugins.filter((p): p is EnginePlugin => !isSystemPlugin(p));\n\n const capabilities = new CapabilityRegistry();\n const manager = new SceneManager(threeScene, elementPlugins, capabilities);\n manager.reconcile(scene);\n\n const input = new InputBus();\n // The visitor's control preferences (view mode + flags). Created early so system\n // plugins can read the flags (e.g. fpc reads the look-invert axes); the effects\n // that need the head-look system / render are wired below, once they exist. A\n // persisted choice (localStorage) wins over the initial cardboard option so the\n // visitor's last mode is restored across reloads.\n const controlSettings = new ControlSettings(\n loadControlFlags() ?? (options.cardboard ? MODE_PRESETS.cardboard : {}),\n );\n const environment = new EnvironmentController(threeScene);\n environment.reconcile(scene.environment);\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const emit = (event: string, payload?: unknown): void => {\n listeners.get(event)?.forEach((handler) => handler(payload));\n };\n const frameHandlers = new Set<(deltaMs: number) => void>();\n\n // Plugins publish their public apis here, keyed by kind/name; the kernel maps\n // them onto `ctx` slots (see `CTX_SLOT`). Element-plugin controllers (lights,\n // video) and system apis (camera, sky, media) both land in this one map.\n const apis = new Map<string, unknown>();\n\n // Element-plugin controllers: an aggregate api over each plugin's live objects\n // (e.g. `ctx.lights`). They advance in the render loop and tear down on dispose.\n const controllers: ElementController[] = [];\n for (const plugin of elementPlugins) {\n const controller = plugin.controller?.(() => manager.elementsOf(plugin.data.kind));\n if (!controller) continue;\n if (controller.api !== undefined) apis.set(plugin.data.kind, controller.api);\n controllers.push(controller);\n }\n\n // System plugins (camera, sky, audio…): drive shared runtime state each frame\n // and may expose an api. They get the scene graph + renderer for subsystems\n // that manage their own objects (sky) or listen on the camera (audio).\n const systems: SystemInstance[] = systemPlugins.map((plugin) => {\n const instance = plugin.setup({\n scene,\n camera,\n canvas,\n scene3d: threeScene,\n renderer,\n input,\n controlFlags: () => controlSettings.get(),\n capabilities,\n emit,\n resolve: (id) => manager.get(id),\n });\n if (instance.api !== undefined) apis.set(plugin.name, instance.api);\n return instance;\n });\n\n // `ctx.camera` is filled by the framing system (\"focus\"). Without it, a no-op\n // keeps callers (the overlay) safe.\n const cameraApi: CameraApi = (apis.get(CTX_SLOT.camera) as CameraApi | undefined) ?? {\n focusArtwork: () => {},\n centerArtwork: () => {},\n release: () => {},\n reset: () => {},\n isAnimating: () => false,\n };\n\n // Cardboard render (stereo split) is a kernel feature; the gyroscope head-look\n // that rides with it is the `@scenography/plugin-cardboard` system. The render\n // toggle below drives its `setEnabled`.\n type HeadTracking = { setEnabled(enabled: boolean): void };\n const headTracking = apis.get(CARDBOARD_SYSTEM) as HeadTracking | undefined;\n // Apply the restored (or seeded) flags on load: enable gyro head-look and sync\n // the stereo render. (On iOS the gyro permission can't be granted outside a user\n // gesture, so a restored gyro mode only truly tracks once the visitor interacts —\n // enable() fails gracefully, nothing breaks.)\n const initialFlags = controlSettings.get();\n render.cardboard = initialFlags.splitScreen;\n if (initialFlags.gyro) headTracking?.setEnabled(true);\n\n // gyro and splitScreen are *orthogonal* here — unlike the legacy `cardboard`\n // render toggle that fused them — so a \"VR\" mode can aim with the gyro without\n // the stereo split. Apply those two effects (head-look system + stereo render)\n // as the flags change; the others are read by the overlay and input consumers.\n // render.cardboard / the initial setEnabled above already reflect the seed, so\n // the subscriber only handles subsequent changes.\n let prevFlags = controlSettings.get();\n controlSettings.subscribe((flags) => {\n if (flags.gyro !== prevFlags.gyro) headTracking?.setEnabled(flags.gyro);\n if (flags.splitScreen !== prevFlags.splitScreen) render.cardboard = flags.splitScreen;\n prevFlags = flags;\n saveControlFlags(flags); // remember the choice across reloads\n emit(\"controls:change\", { flags, mode: controlSettings.getMode() });\n });\n\n // No-op fallbacks so the overlay/behavior stay safe when a plugin is absent.\n const lightsApi = (apis.get(CTX_SLOT.lights) as LightsApi | undefined) ?? {\n set: () => {},\n dimAll: () => {},\n restore: () => {},\n };\n const videoApi = (apis.get(CTX_SLOT.video) as VideoApi | undefined) ?? {\n play: () => {},\n pause: () => {},\n setVisible: () => {},\n };\n const skyApi = (apis.get(CTX_SLOT.sky) as SkyApi | undefined) ?? {\n setTime: () => {},\n getTime: () => 12,\n setAzimuth: () => {},\n getAzimuth: () => 0,\n };\n const mediaApi = (apis.get(CTX_SLOT.media) as MediaController | undefined) ?? {\n play: () => {},\n pause: () => {},\n stop: () => {},\n setVolume: () => {},\n playing: () => false,\n setMuted: () => {},\n muted: () => false,\n };\n\n // Per-element interaction-distance gate, derived from each pickable element's\n // `infoRadius`. Read structurally: `infoRadius` is a convention a plugin (the\n // artwork plugin) can put on its elements, not a kernel field. A later\n // interaction pass can move this onto the built object's userData entirely.\n type Gated = { id: string; infoRadius?: number };\n const radiiOf = (s: Scene): Map<string, number> => {\n const gated = (s as { artworks?: Gated[] }).artworks ?? [];\n return new Map(\n gated.filter((a) => a.infoRadius !== undefined).map((a) => [a.id, a.infoRadius as number]),\n );\n };\n\n const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating());\n interaction.setRadii(radiiOf(scene));\n interaction.attach();\n const clock = new Clock();\n let rafId: number | null = null;\n\n // Post-processing (MSAA antialias + SSAO + bloom). The whole chain is imported\n // on demand so it only ships when a gallery uses it; until it resolves (or when\n // everything is off) we render straight to the canvas. Antialiasing is MSAA on\n // the composer's render target (`samples`), so changing the sample count\n // rebuilds the composer; SSAO/bloom just flip `.enabled`.\n // Bloom spread/threshold are fixed; its strength is the live `render.bloom`.\n const BLOOM = { radius: 0.4, threshold: 0.85 };\n type PostClasses = {\n EffectComposer: typeof import(\"three/examples/jsm/postprocessing/EffectComposer.js\").EffectComposer;\n RenderPass: typeof import(\"three/examples/jsm/postprocessing/RenderPass.js\").RenderPass;\n SSAOPass: typeof import(\"three/examples/jsm/postprocessing/SSAOPass.js\").SSAOPass;\n UnrealBloomPass: typeof import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\").UnrealBloomPass;\n OutputPass: typeof import(\"three/examples/jsm/postprocessing/OutputPass.js\").OutputPass;\n };\n let postClasses: PostClasses | null = null;\n let composer: EffectComposer | null = null;\n let ssaoPass: (Pass & { kernelRadius: number }) | null = null;\n let bloomPass: (Pass & { setSize(w: number, h: number): void; strength: number }) | null = null;\n let composerLoading = false;\n\n const pixelRatio = Math.min(window.devicePixelRatio, 2);\n const needsComposer = (): boolean => render.antialias > 0 || render.ssao || render.bloom > 0;\n\n // (Re)build the composer from the loaded classes at the current sample count.\n const buildComposer = (): void => {\n if (!postClasses) return;\n composer?.dispose();\n const { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass } = postClasses;\n // Multisampled target → MSAA that survives the post chain. `samples` is\n // preserved across resizes (setSize only changes width/height). HalfFloat\n // matches the composer's own default and gives bloom its HDR range.\n const target = new WebGLRenderTarget(1, 1, { samples: render.antialias, type: HalfFloatType });\n const c = new EffectComposer(renderer, target);\n c.setPixelRatio(pixelRatio);\n c.setSize(width, height);\n c.addPass(new RenderPass(threeScene, camera));\n const ssao = new SSAOPass(threeScene, camera, width, height);\n ssao.kernelRadius = 16;\n ssao.enabled = render.ssao;\n c.addPass(ssao);\n const bloom = new UnrealBloomPass(new Vector2(width, height), render.bloom, BLOOM.radius, BLOOM.threshold);\n bloom.enabled = render.bloom > 0;\n c.addPass(bloom);\n // Restores tone mapping / sRGB that the composer would otherwise skip.\n c.addPass(new OutputPass());\n composer = c;\n ssaoPass = ssao;\n bloomPass = bloom;\n };\n\n const ensureComposer = (): void => {\n if (!needsComposer()) return;\n if (postClasses) {\n if (!composer) buildComposer();\n return;\n }\n if (composerLoading) return;\n composerLoading = true;\n void Promise.all([\n import(\"three/examples/jsm/postprocessing/EffectComposer.js\"),\n import(\"three/examples/jsm/postprocessing/RenderPass.js\"),\n import(\"three/examples/jsm/postprocessing/SSAOPass.js\"),\n import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\"),\n import(\"three/examples/jsm/postprocessing/OutputPass.js\"),\n ]).then(([{ EffectComposer }, { RenderPass }, { SSAOPass }, { UnrealBloomPass }, { OutputPass }]) => {\n postClasses = { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass };\n composerLoading = false;\n buildComposer();\n });\n };\n\n // Recompile materials so a shadowMap.enabled change takes effect.\n const refreshShadows = (): void => {\n renderer.shadowMap.enabled = render.shadows;\n threeScene.traverse((obj) => {\n const mat = (obj as { material?: unknown }).material;\n if (!mat) return;\n for (const m of Array.isArray(mat) ? mat : [mat]) (m as { needsUpdate: boolean }).needsUpdate = true;\n });\n };\n refreshShadows();\n ensureComposer();\n\n // Split the canvas into a left/right eye pair. Mirrors what StereoEffect does\n // internally, but stays in our loop and uses only setViewport/setScissor.\n const renderStereo = (): void => {\n camera.updateWorldMatrix(true, false);\n stereo.update(camera);\n renderer.getSize(stereoSize);\n const w = stereoSize.x;\n const h = stereoSize.y;\n if (renderer.autoClear) renderer.clear();\n renderer.setScissorTest(true);\n renderer.setScissor(0, 0, w / 2, h);\n renderer.setViewport(0, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraL);\n renderer.setScissor(w / 2, 0, w / 2, h);\n renderer.setViewport(w / 2, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraR);\n renderer.setScissorTest(false);\n renderer.setViewport(0, 0, w, h);\n };\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n const delta = clock.getDelta();\n const deltaMs = delta * 1000;\n // Systems drive the camera in registration order: the roaming camera first,\n // then the framing system, which may override it during a focus animation.\n for (const system of systems) system.update?.(deltaMs);\n interaction.update(deltaMs);\n manager.updateElements(deltaMs);\n for (const controller of controllers) controller.update?.(deltaMs);\n // Behavior runs after the camera moved this frame, before rendering. (The\n // cardboard system, last in `systems`, already had the final say on\n // orientation above.)\n for (const handler of frameHandlers) handler(deltaMs);\n // Auxiliary passes (e.g. the water's planar reflection) render into their\n // own targets from the current camera, before the main frame.\n manager.beforeRender(renderer, threeScene, camera);\n if (render.cardboard) renderStereo();\n else if (composer && needsComposer()) composer.render(delta);\n else renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n lights: lightsApi,\n media: mediaApi,\n controls: {\n setMoveInput: (forward, strafe) => input.setMove(forward, strafe),\n setLookInput: (x, y) => input.setLook(x, y),\n },\n viewControls: {\n get: () => controlSettings.get(),\n getMode: () => controlSettings.getMode(),\n setMode: (mode) => controlSettings.setMode(mode),\n setFlag: (key, value) => controlSettings.setFlag(key, value),\n subscribe: (fn) => controlSettings.subscribe(fn),\n },\n environment: {\n setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v),\n },\n sky: skyApi,\n camera: cameraApi,\n video: videoApi,\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n interaction.setRadii(radiiOf(next));\n environment.reconcile(next.environment);\n for (const system of systems) system.setScene?.(next);\n const fov = next.camera.fov ?? 55;\n if (camera.fov !== fov) {\n camera.fov = fov;\n camera.updateProjectionMatrix();\n }\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 for (const system of systems) system.dispose?.();\n for (const controller of controllers) controller.dispose?.();\n interaction.detach();\n environment.dispose();\n manager.dispose();\n composer?.dispose();\n renderer.dispose();\n listeners.clear();\n frameHandlers.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 emit(event, payload);\n },\n onFrame(handler) {\n frameHandlers.add(handler);\n return () => frameHandlers.delete(handler);\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n composer?.setSize(w, h);\n ssaoPass?.setSize(w, h);\n bloomPass?.setSize(w, h);\n },\n setRender(next) {\n if (next.shadows !== undefined && next.shadows !== render.shadows) {\n render.shadows = next.shadows;\n refreshShadows();\n }\n if (next.ssao !== undefined) render.ssao = next.ssao;\n if (next.bloom !== undefined) render.bloom = next.bloom;\n // The sample count lives on the render target, so a change rebuilds the\n // composer (cheap, and only on user toggle).\n const aaChanged = next.antialias !== undefined && next.antialias !== render.antialias;\n if (next.antialias !== undefined) render.antialias = next.antialias;\n if (next.cardboard !== undefined && next.cardboard !== render.cardboard) {\n // Legacy stereo toggle: route through the control state so mode/flags and\n // the render stay one source of truth (the subscriber applies gyro +\n // split; setEnabled thus still runs in this click gesture, which iOS needs\n // for the motion-permission prompt). Superseded by `ctx.viewControls` once\n // the mode picker replaces the cardboard button.\n controlSettings.setMode(next.cardboard ? \"cardboard\" : \"touch\");\n }\n if (next.eyeSeparation !== undefined) {\n render.eyeSeparation = next.eyeSeparation;\n stereo.eyeSep = next.eyeSeparation;\n }\n if (ssaoPass) ssaoPass.enabled = render.ssao;\n if (bloomPass) {\n bloomPass.strength = render.bloom;\n bloomPass.enabled = render.bloom > 0;\n }\n if (aaChanged && composer) buildComposer();\n ensureComposer(); // load/build the chain if an effect was just turned on\n },\n };\n}\n","import {\n MeshStandardMaterial,\n NoColorSpace,\n RepeatWrapping,\n SRGBColorSpace,\n type Texture,\n TextureLoader,\n Vector2,\n} from \"three\";\nimport type { SurfaceTexture, Vec2 } from \"@scenography/scene\";\n\nconst textureLoader = new TextureLoader();\n\ntype MapSlot = \"map\" | \"normalMap\" | \"roughnessMap\" | \"aoMap\";\n\n/** Configure a loaded texture: tiling + color space (sRGB only for color maps). */\nconst configure = (tex: Texture, repeat: Vec2, srgb: boolean): void => {\n tex.wrapS = RepeatWrapping;\n tex.wrapT = RepeatWrapping;\n tex.repeat.set(repeat[0], repeat[1]);\n tex.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;\n tex.needsUpdate = true;\n};\n\n/**\n * Load one map and assign it to `material[slot]` once ready. `.exr` maps go\n * through EXRLoader, imported on demand so it only ships in the bundle when a\n * scene actually uses an EXR map; everything else uses the image loader.\n */\nconst loadMap = (\n material: MeshStandardMaterial,\n slot: MapSlot,\n src: string,\n repeat: Vec2,\n srgb: boolean,\n): void => {\n const assign = (tex: Texture): void => {\n configure(tex, repeat, srgb);\n material[slot] = tex;\n material.needsUpdate = true;\n };\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, assign);\n });\n } else {\n textureLoader.load(src, assign);\n }\n};\n\n/**\n * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV\n * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must\n * provide one.\n */\nexport const applyTexture = (\n material: MeshStandardMaterial,\n texture: SurfaceTexture,\n): boolean => {\n const repeat = texture.repeat ?? [1, 1];\n loadMap(material, \"map\", texture.src, repeat, true);\n if (texture.normal) {\n loadMap(material, \"normalMap\", texture.normal, repeat, false);\n const s = texture.normalScale ?? 1;\n material.normalScale = new Vector2(s, s);\n }\n if (texture.roughness) loadMap(material, \"roughnessMap\", texture.roughness, repeat, false);\n if (texture.ao) loadMap(material, \"aoMap\", texture.ao, repeat, false);\n return texture.ao !== undefined;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["FLAG_KEYS","ThreeScene"],"sources":["../src/environment/EnvironmentController.ts","../src/interaction/InteractionController.ts","../src/input/InputBus.ts","../src/input/controlSettings.ts","../src/input/controlStorage.ts","../src/plugin/CapabilityRegistry.ts","../src/plugin/systemPlugin.ts","../src/plugin/roamControl.ts","../src/plugin/terrain.ts","../src/plugin/colliders.ts","../src/plugin/edges.ts","../src/plugin/eyeHeight.ts","../src/builders/textureCache.ts","../src/builders/texture.ts","../src/SceneManager.ts","../src/createGallery.ts"],"sourcesContent":["import { EquirectangularReflectionMapping, type Scene as ThreeScene, type Texture } from \"three\";\nimport type { Environment } from \"@scenography/scene\";\n\n/** Load an equirectangular HDRI, picking the loader by extension. */\nconst loadHDRI = (src: string, onLoad: (tex: Texture) => void): void => {\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, onLoad);\n });\n } else {\n void import(\"three/examples/jsm/loaders/RGBELoader.js\").then(({ RGBELoader }) => {\n new RGBELoader().load(src, onLoad);\n });\n }\n};\n\n/**\n * Owns the scene's HDRI sky / image-based lighting and keeps it in sync with the\n * `Environment` definition. Loads the equirectangular map on demand (its loader\n * ships in its own chunk) and assigns it to `scene.background` and/or\n * `scene.environment`. Reconciles by a JSON hash so hot edits only reload when\n * the definition actually changes.\n */\nexport class EnvironmentController {\n private readonly scene: ThreeScene;\n private hash: string | null = null;\n private texture: Texture | null = null;\n\n constructor(scene: ThreeScene) {\n this.scene = scene;\n }\n\n reconcile(env: Environment | undefined): void {\n const hash = env ? JSON.stringify(env) : null;\n if (hash === this.hash) return;\n this.hash = hash;\n this.clear();\n if (!env) return;\n\n loadHDRI(env.src, (tex) => {\n // A newer reconcile superseded this load before it finished.\n if (this.hash !== hash) {\n tex.dispose();\n return;\n }\n tex.mapping = EquirectangularReflectionMapping;\n this.texture = tex;\n if (env.background ?? true) {\n this.scene.background = tex;\n this.scene.backgroundIntensity = env.intensity ?? 1;\n }\n if (env.lighting ?? true) this.scene.environment = tex;\n });\n }\n\n /** Set the visible sky background brightness (0 = black). Live, for behavior. */\n setBackgroundIntensity(value: number): void {\n this.scene.backgroundIntensity = value;\n }\n\n private clear(): void {\n this.scene.background = null;\n this.scene.environment = null;\n this.texture?.dispose();\n this.texture = null;\n }\n\n dispose(): void {\n this.clear();\n this.hash = null;\n }\n}\n","import {\n BoxGeometry,\n Group,\n Mesh,\n MeshBasicMaterial,\n Quaternion,\n Raycaster,\n Vector2,\n Vector3,\n type Object3D,\n type PerspectiveCamera,\n type Scene as ThreeScene,\n} from \"three\";\n\nconst HIGHLIGHT_COLOR = \"#ef4444\";\nconst HIGHLIGHT_THICKNESS = 1.5; // world units (cm)\nconst HIGHLIGHT_MARGIN = 1.5; // gap between the piece and the frame\nconst CLICK_SLOP = 4; // px of movement still counted as a click, not a drag\n\ninterface PickInfo {\n width: number;\n height: number;\n frontOffset: number;\n}\n\ninterface Pick {\n id: string;\n holder: Object3D;\n info: PickInfo;\n}\n\n/**\n * Pointer picking for artworks: hovering a piece's click target (its label, or\n * the piece itself when it has none) draws a pulsing red frame and shows a\n * pointer cursor; clicking it emits `artwork:select { id }`. Targets are marked\n * in the scene graph (`userData.pickTarget`/`pickInfo`) by the artwork builder,\n * so this just raycasts and walks up to find them. An optional per-id radius\n * gates interaction to when the camera is reasonably close.\n */\nexport class InteractionController {\n private readonly raycaster = new Raycaster();\n private readonly pointer = new Vector2();\n private over = false;\n private downPos: { x: number; y: number } | null = null;\n private radii = new Map<string, number>();\n private hovered: string | null = null;\n private time = 0;\n\n private readonly highlight = new Group();\n private highlightMat: MeshBasicMaterial | null = null;\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n private readonly camera: PerspectiveCamera,\n private readonly canvas: HTMLCanvasElement,\n private readonly scene: ThreeScene,\n private readonly emit: (event: string, payload?: unknown) => void,\n private readonly canInteract: () => boolean,\n ) {\n this.highlight.visible = false;\n this.scene.add(this.highlight);\n }\n\n /** Per-artwork max interaction distance (XZ). Absent id = no distance gate. */\n setRadii(radii: Map<string, number>): void {\n this.radii = radii;\n }\n\n attach(): void {\n const bind = (type: string, listener: EventListener): void => {\n this.canvas.addEventListener(type, listener);\n this.handlers.push([this.canvas, type, listener]);\n };\n bind(\"pointermove\", (e) => this.onMove(e as PointerEvent));\n bind(\"pointerdown\", (e) => this.onDown(e as PointerEvent));\n bind(\"pointerup\", (e) => this.onUp(e as PointerEvent));\n bind(\"pointerleave\", () => {\n this.over = false;\n });\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);\n this.handlers.length = 0;\n this.scene.remove(this.highlight);\n this.clearHighlight();\n }\n\n private setPointer(e: PointerEvent): void {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.set(\n ((e.clientX - rect.left) / rect.width) * 2 - 1,\n -((e.clientY - rect.top) / rect.height) * 2 + 1,\n );\n this.over = true;\n }\n\n private onMove(e: PointerEvent): void {\n this.setPointer(e);\n }\n\n private onDown(e: PointerEvent): void {\n this.downPos = { x: e.clientX, y: e.clientY };\n }\n\n private onUp(e: PointerEvent): void {\n const down = this.downPos;\n this.downPos = null;\n if (!down || !this.canInteract()) return;\n if (Math.hypot(e.clientX - down.x, e.clientY - down.y) > CLICK_SLOP) return; // a drag, not a click\n this.setPointer(e);\n const pick = this.raycast();\n if (pick) this.emit(\"artwork:select\", { id: pick.id });\n }\n\n /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */\n private raycast(): Pick | null {\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const hits = this.raycaster.intersectObjects(this.scene.children, true);\n for (const hit of hits) {\n let node: Object3D | null = hit.object;\n while (node && node.userData.pickTarget === undefined) node = node.parent;\n if (!node) continue;\n const id = node.userData.pickTarget as string;\n const radius = this.radii.get(id);\n if (radius !== undefined) {\n const p = node.getWorldPosition(new Vector3());\n if (Math.hypot(this.camera.position.x - p.x, this.camera.position.z - p.z) > radius) continue;\n }\n return { id, holder: node, info: node.userData.pickInfo as PickInfo };\n }\n return null;\n }\n\n /** Recompute hover state and animate the highlight. Call once per frame. */\n update(deltaMs: number): void {\n this.time += deltaMs;\n const pick = this.over && this.canInteract() ? this.raycast() : null;\n\n if ((pick?.id ?? null) !== this.hovered) {\n this.hovered = pick?.id ?? null;\n this.canvas.style.cursor = pick ? \"pointer\" : \"\";\n if (pick) this.buildHighlight(pick);\n else this.highlight.visible = false;\n }\n\n if (this.highlight.visible && this.highlightMat) {\n this.highlightMat.opacity = 0.45 + 0.45 * (0.5 + 0.5 * Math.sin(this.time / 320));\n }\n }\n\n private clearHighlight(): void {\n for (const child of [...this.highlight.children]) {\n this.highlight.remove(child);\n (child as Mesh).geometry?.dispose();\n }\n this.highlightMat?.dispose();\n this.highlightMat = null;\n }\n\n /** Position a fresh pulsing red frame over the picked target's front face. */\n private buildHighlight(pick: Pick): void {\n this.clearHighlight();\n const mat = new MeshBasicMaterial({ color: HIGHLIGHT_COLOR, transparent: true, depthTest: false });\n this.highlightMat = mat;\n\n const w = pick.info.width + 2 * HIGHLIGHT_MARGIN;\n const h = pick.info.height + 2 * HIGHLIGHT_MARGIN;\n const t = HIGHLIGHT_THICKNESS;\n // Four bars forming a border (horizontals span the corners; verticals fill in).\n const bars: [number, number, number, number][] = [\n [w + 2 * t, t, 0, h / 2 + t / 2],\n [w + 2 * t, t, 0, -(h / 2 + t / 2)],\n [t, h, -(w / 2 + t / 2), 0],\n [t, h, w / 2 + t / 2, 0],\n ];\n for (const [bw, bh, x, y] of bars) {\n const bar = new Mesh(new BoxGeometry(bw, bh, 0.5), mat);\n bar.position.set(x, y, 0);\n bar.renderOrder = 999;\n this.highlight.add(bar);\n }\n\n const pos = pick.holder.getWorldPosition(new Vector3());\n const quat = pick.holder.getWorldQuaternion(new Quaternion());\n const normal = new Vector3(0, 0, 1).applyQuaternion(quat);\n pos.addScaledVector(normal, pick.info.frontOffset);\n this.highlight.position.copy(pos);\n this.highlight.quaternion.copy(quat);\n this.highlight.visible = true;\n }\n}\n","/**\n * The shared analog-input channel: the current movement and look intent, each\n * axis in `-1..1` and held until a producer changes it. Producers (on-screen\n * joysticks in the overlay, a gamepad…) write to it; whatever camera system is\n * active reads it and interprets it its own way — free-roam walks with it, a\n * fixed camera might only use the look axes to glance around. This is what makes\n * the joysticks reusable across camera types. Pure state — no three.js.\n */\nexport class InputBus {\n /** Forward (`+1`) / backward (`-1`). */\n moveForward = 0;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe = 0;\n /** Look right (`+1`) / left (`-1`). */\n lookX = 0;\n /** Look up (`+1`) / down (`-1`). */\n lookY = 0;\n\n /** Set the movement intent (forward, strafe). */\n setMove(forward: number, strafe = 0): void {\n this.moveForward = forward;\n this.moveStrafe = strafe;\n }\n\n /** Set the look intent (turn, pitch). */\n setLook(x: number, y = 0): void {\n this.lookX = x;\n this.lookY = y;\n }\n}\n","/**\n * The visitor's camera-control preferences: a set of orthogonal capability flags\n * plus a derived, friendly \"mode\". Pure state — no three.js, no DOM. The engine\n * wires the flags to effects (gyroscope head-look, stereo split); consumers read\n * them (the analog look-invert axes); the overlay drives it from a mode picker\n * (the `ScanEye` button) and the settings panel. Persistence and device\n * feature-detection live in those callers, not here, so this stays trivially\n * unit-testable.\n */\n\n/**\n * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a\n * mode fixes; the two `invertLook*` axes are independent look-feel preferences\n * (some people feel the right stick inverted), kept separate from the mode.\n */\nexport interface ControlFlags {\n /** Orient the camera from the device gyroscope (head-look). */\n gyro: boolean;\n /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */\n splitScreen: boolean;\n /** Show the on-screen virtual joysticks. */\n joysticks: boolean;\n /** Invert the horizontal (turn) axis of analog look. */\n invertLookX: boolean;\n /** Invert the vertical (pitch) axis of analog look. */\n invertLookY: boolean;\n}\n\n/** A named preset over the mode-defining flags; `custom` when none matches. */\nexport type ControlMode = \"touch\" | \"vr\" | \"cardboard\" | \"custom\";\n\n/** The three flags a mode fixes (the look-invert axes stay a separate preference). */\ntype ModeFlags = Pick<ControlFlags, \"gyro\" | \"splitScreen\" | \"joysticks\">;\n\n/**\n * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single\n * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze\n * select (no joysticks). The move-only single stick in VR is derived downstream\n * from `gyro && !splitScreen`, so it needs no flag of its own.\n */\nexport const MODE_PRESETS: Record<Exclude<ControlMode, \"custom\">, ModeFlags> = {\n touch: { gyro: false, splitScreen: false, joysticks: true },\n vr: { gyro: true, splitScreen: false, joysticks: true },\n cardboard: { gyro: true, splitScreen: true, joysticks: false },\n};\n\n/** Default flags: plain touch, no look inversion. */\nexport const DEFAULT_CONTROL_FLAGS: ControlFlags = {\n ...MODE_PRESETS.touch,\n invertLookX: false,\n invertLookY: false,\n};\n\nconst FLAG_KEYS: readonly (keyof ControlFlags)[] = [\n \"gyro\",\n \"splitScreen\",\n \"joysticks\",\n \"invertLookX\",\n \"invertLookY\",\n];\n\nconst equalFlags = (a: ControlFlags, b: ControlFlags): boolean =>\n FLAG_KEYS.every((key) => a[key] === b[key]);\n\n/** The mode a flag set represents, or `custom` when no preset matches. */\nexport function modeOf(flags: ControlFlags): ControlMode {\n for (const mode of [\"touch\", \"vr\", \"cardboard\"] as const) {\n const preset = MODE_PRESETS[mode];\n if (\n flags.gyro === preset.gyro &&\n flags.splitScreen === preset.splitScreen &&\n flags.joysticks === preset.joysticks\n )\n return mode;\n }\n return \"custom\";\n}\n\n/** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */\nexport function applyMode(\n flags: ControlFlags,\n mode: Exclude<ControlMode, \"custom\">,\n): ControlFlags {\n return { ...flags, ...MODE_PRESETS[mode] };\n}\n\n/**\n * Live control-preferences state: holds the flags, derives the mode, and notifies\n * subscribers on every real change. Callers apply the effects — the engine wires\n * gyro and split, the overlay redraws — so this object is just the shared source\n * of truth they all read and write.\n */\nexport class ControlSettings {\n private flags: ControlFlags;\n private readonly subscribers = new Set<(flags: ControlFlags) => void>();\n\n constructor(initial: Partial<ControlFlags> = {}) {\n this.flags = { ...DEFAULT_CONTROL_FLAGS, ...initial };\n }\n\n /** The current flags (a copy — mutate through `setMode` / `setFlag`). */\n get(): ControlFlags {\n return { ...this.flags };\n }\n\n /** The mode the current flags represent (`custom` when hand-tuned). */\n getMode(): ControlMode {\n return modeOf(this.flags);\n }\n\n /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */\n setMode(mode: Exclude<ControlMode, \"custom\">): void {\n this.replace(applyMode(this.flags, mode));\n }\n\n /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void {\n this.replace({ ...this.flags, [key]: value });\n }\n\n /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */\n subscribe(fn: (flags: ControlFlags) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n private replace(next: ControlFlags): void {\n if (equalFlags(this.flags, next)) return;\n this.flags = next;\n const snapshot = this.get();\n for (const fn of this.subscribers) fn(snapshot);\n }\n}\n","import { DEFAULT_CONTROL_FLAGS, type ControlFlags } from \"./controlSettings\";\n\n/**\n * localStorage persistence for the control flags, kept out of the pure\n * `ControlSettings` so that stays DOM-free and unit-testable. Both functions are\n * best-effort and guarded: no `localStorage` (SSR, privacy mode) or malformed\n * data just falls back to defaults, never throws.\n */\nconst STORAGE_KEY = \"scenography:controls\";\n\nconst FLAG_KEYS = Object.keys(DEFAULT_CONTROL_FLAGS) as (keyof ControlFlags)[];\n\n/** Read persisted control flags; `null` when absent, unavailable or malformed. */\nexport function loadControlFlags(): Partial<ControlFlags> | null {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const flags: Partial<ControlFlags> = {};\n // Copy only known boolean keys, so stale/garbage fields can't leak in.\n for (const key of FLAG_KEYS) {\n if (typeof parsed[key] === \"boolean\") flags[key] = parsed[key] as boolean;\n }\n return flags;\n } catch {\n return null;\n }\n}\n\n/** Persist control flags (best-effort; ignores a full/blocked store). */\nexport function saveControlFlags(flags: ControlFlags): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(flags));\n } catch {\n /* storage unavailable or full — persistence is best-effort */\n }\n}\n","/**\n * Shared runtime contracts between plugins, keyed by name. A plugin (or the\n * kernel) *provides* a capability — e.g. the floor provides `terrain` — and\n * other plugins *consume* it if present, without depending on the provider's\n * package. Each name keeps a monotonic version counter so consumers can hash\n * their dependencies cheaply and rebuild when a provider changes.\n */\nexport class CapabilityRegistry {\n private readonly values = new Map<string, unknown>();\n private readonly versions = new Map<string, number>();\n /** Last change-token seen per name, to suppress redundant version bumps. */\n private readonly tokens = new Map<string, string>();\n\n /**\n * Publish (or replace) the capability under `name`. Pass a `token` (a cheap\n * hash of the inputs) to dedupe: re-providing with an unchanged token is a\n * no-op, so consumers that depend on this capability don't rebuild when the\n * provider recomputes an identical value.\n */\n provide<T>(name: string, value: T, token?: string): void {\n if (token !== undefined && this.values.has(name) && this.tokens.get(name) === token) return;\n this.values.set(name, value);\n if (token !== undefined) this.tokens.set(name, token);\n else this.tokens.delete(name);\n this.bump(name);\n }\n\n /** Remove the capability; consumers see `undefined` and a version bump. */\n revoke(name: string): void {\n if (this.values.delete(name)) {\n this.tokens.delete(name);\n this.bump(name);\n }\n }\n\n get<T>(name: string): T | undefined {\n return this.values.get(name) as T | undefined;\n }\n\n /** Monotonic change counter for `name` (`0` if never provided). */\n version(name: string): number {\n return this.versions.get(name) ?? 0;\n }\n\n private bump(name: string): void {\n this.versions.set(name, (this.versions.get(name) ?? 0) + 1);\n }\n}\n","import type {\n Object3D,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n} from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport type { CapabilityRegistry } from \"./CapabilityRegistry\";\nimport type { ControlFlags, InputBus } from \"../input\";\n\n/**\n * What the kernel hands a system on setup. A *system* plugin — unlike an element\n * plugin — produces no scene object; it drives shared runtime state (the camera,\n * input…) each frame. The camera is a kernel primitive (the renderer needs one),\n * so systems receive it and drive it, publishing/consuming capabilities to\n * cooperate (e.g. the camera plugin publishes `roamControl`).\n */\nexport interface SystemContext {\n /** The current scene (its data keys, start pose, boundary…); also `setScene` on hot-swap. */\n scene: Scene;\n /** The render camera the system may drive. */\n camera: PerspectiveCamera;\n /** The canvas, for pointer/keyboard listeners. */\n canvas: HTMLElement;\n /** The three.js scene graph, for systems that manage their own objects (sky…). */\n scene3d: ThreeScene;\n /** The renderer, for systems that need it (e.g. sky IBL generation). */\n renderer: WebGLRenderer;\n /** Shared analog input (joysticks feed it); systems interpret it. */\n input: InputBus;\n /** The visitor's current control flags (e.g. look-invert axes), read live. */\n controlFlags: () => ControlFlags;\n /** Capability registry for cross-plugin contracts (terrain, colliders…). */\n capabilities: CapabilityRegistry;\n /** Emit a gallery event. */\n emit: (event: string, payload?: unknown) => void;\n /** Resolve a live scene object by id (e.g. for a framing system). */\n resolve: (id: string) => Object3D | undefined;\n}\n\n/** A running system: its per-frame update, hot-swap and teardown hooks. */\nexport interface SystemInstance<Api = unknown> {\n /** Advance the system by the frame delta (ms). */\n update?(deltaMs: number): void;\n /** React to a hot-swapped scene definition. */\n setScene?(scene: Scene): void;\n /** Tear down (detach listeners, revoke capabilities). */\n dispose?(): void;\n /**\n * Public API the system exposes to userland/overlay. The kernel maps known\n * system names onto `ctx` (e.g. a `focus` system's api → `ctx.camera`).\n */\n api?: Api;\n}\n\n/**\n * A system plugin: sets up once against the kernel handles and returns its\n * running instance. Registered alongside element plugins in `plugins`; the\n * kernel tells them apart by shape (`setup` vs `build`).\n */\nexport interface SystemPlugin<Api = unknown> {\n name: string;\n setup(ctx: SystemContext): SystemInstance<Api>;\n}\n\n/** Narrow a mixed plugin to a system plugin (has `setup`, not `build`). */\nexport const isSystemPlugin = (plugin: unknown): plugin is SystemPlugin =>\n typeof plugin === \"object\" && plugin !== null && \"setup\" in plugin;\n","import type { Vector3 } from \"three\";\n\n/** Capability name under which a roaming camera system exposes pose control. */\nexport const ROAM_CONTROL = \"roamControl\";\n\n/**\n * Handle a camera plugin publishes so other camera systems can cooperate with\n * it — chiefly a framing/tour system that suspends free-roam while it animates\n * the camera, then reads/writes the pose to hand control back cleanly.\n */\nexport interface RoamControl {\n /** Enable/disable free-roam input (disabled while another system owns the camera). */\n setEnabled(enabled: boolean): void;\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number };\n /** Force the pose so free-roam resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void;\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which the floor publishes its terrain. */\nexport const TERRAIN = \"terrain\";\n\n/**\n * The geometry a terrain consumer relies on — the fields needed to sample the\n * same ground a floor was displaced by (e.g. the water shader). A floor\n * definition satisfies this structurally; the kernel stays floor-agnostic.\n */\nexport interface TerrainDefinition {\n /** Extent on the ground plane, `[width (x), depth (z)]`. */\n size: Vec2;\n /** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */\n center?: Vec2;\n /** Base vertical position (world Y). Defaults to `0`. */\n y?: number;\n /** Grayscale heightmap URL that displaces the ground. */\n heightmap?: string;\n /** Max displacement (world units) at white in the heightmap. */\n heightScale?: number;\n}\n\n/**\n * The walkable ground, published by the floor: its definition (for shaders that\n * sample the same heightmap), the live height function (heightmap-aware once the\n * image loads) and a promise that resolves once that sampling is in effect.\n */\nexport interface TerrainCapability {\n definition: TerrainDefinition;\n heightAt: (x: number, z: number) => number;\n /** Resolves once `heightAt` reflects the heightmap (immediately when there is none). */\n ready: Promise<void>;\n}\n","/** Capability name under which walkable-space obstacles are published. */\nexport const COLLIDERS = \"colliders\";\n\n/**\n * A collision obstacle as an oriented box on the XZ plane. The camera controls\n * consume these to keep the visitor out of walls (and any other plugin that\n * wants to block movement can provide more of them). Purely a movement contract\n * — no geometry, no three.js.\n */\nexport interface Collider {\n /** Box center `[x, z]`. */\n cx: number;\n cz: number;\n /** Unit direction along the box's length. */\n dx: number;\n dz: number;\n /** Half the box length. */\n halfLength: number;\n /** Half the box thickness. */\n halfThickness: number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which named wall edges are published for hanging. */\nexport const EDGES = \"edges\";\n\n/** A named anchor an artwork can hang on: a wall segment resolved to geometry. */\nexport interface EdgeAnchor {\n a: Vec2;\n b: Vec2;\n height: number;\n thickness: number;\n}\n\n/** The `name -> anchor` index a provider (e.g. the wall plugin) publishes. */\nexport type EdgeIndex = ReadonlyMap<string, EdgeAnchor>;\n","/**\n * Capability name under which the visitor's eye height (world units) is\n * published — the default vertical placement for wall-hung artworks. The kernel\n * provides it from the camera start; a camera plugin will own it later.\n */\nexport const EYE_HEIGHT = \"eyeHeight\";\n","import { NoColorSpace, RepeatWrapping, SRGBColorSpace, type Texture } from \"three\";\nimport type { Vec2 } from \"@scenography/scene\";\n\n/** Creates the texture object for a source; its image may still be loading. */\nexport type TextureSource = (src: string) => Texture;\n\ntype Entry = {\n key: string;\n texture: Texture;\n refs: number;\n};\n\n/** Two surfaces can share a texture only at the same tiling and color space. */\nexport const textureKey = (src: string, repeat: Vec2, srgb: boolean): string =>\n `${src}|${repeat[0]}x${repeat[1]}|${srgb ? \"srgb\" : \"linear\"}`;\n\n/** Tiling and color space of a texture; the image arrives later from the loader. */\nexport const configureTexture = (texture: Texture, repeat: Vec2, srgb: boolean): void => {\n texture.wrapS = RepeatWrapping;\n texture.wrapT = RepeatWrapping;\n texture.repeat.set(repeat[0], repeat[1]);\n texture.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;\n};\n\n/**\n * Shares textures between materials: the same source at the same tiling and\n * color space becomes one texture, loaded once and uploaded to the GPU once.\n * Reference counted, so releasing a material keeps the texture alive while\n * another one still samples it.\n */\nexport class TextureCache {\n private readonly entries = new Map<string, Entry>();\n private readonly owners = new Map<Texture, Entry>();\n\n constructor(private readonly source: TextureSource) {}\n\n /** The shared texture for a source, created on first use. */\n acquire(src: string, repeat: Vec2, srgb: boolean): Texture {\n const key = textureKey(src, repeat, srgb);\n const existing = this.entries.get(key);\n if (existing) {\n existing.refs += 1;\n return existing.texture;\n }\n const texture = this.source(src);\n configureTexture(texture, repeat, srgb);\n const entry = { key, texture, refs: 1 };\n this.entries.set(key, entry);\n this.owners.set(texture, entry);\n return texture;\n }\n\n /** Drop one reference; the texture is disposed when the last one goes. */\n release(texture: Texture): void {\n const entry = this.owners.get(texture);\n if (!entry) {\n texture.dispose();\n return;\n }\n entry.refs -= 1;\n if (entry.refs > 0) return;\n this.entries.delete(entry.key);\n this.owners.delete(texture);\n texture.dispose();\n }\n\n /** Textures currently shared. */\n get size(): number {\n return this.entries.size;\n }\n}\n","import { MeshStandardMaterial, TextureLoader, Vector2, type Texture } from \"three\";\nimport type { SurfaceTexture, Vec2 } from \"@scenography/scene\";\n\nimport { configureTexture, TextureCache } from \"./textureCache\";\n\nconst textureLoader = new TextureLoader();\nconst cache = new TextureCache((src) => textureLoader.load(src));\n\ntype MapSlot = \"map\" | \"normalMap\" | \"roughnessMap\" | \"aoMap\";\n\n/**\n * Load one map into `material[slot]`. Textures are shared: every material that\n * samples the same source at the same tiling reuses one texture object. `.exr`\n * maps go through EXRLoader, imported on demand, and are not shared because\n * they resolve asynchronously.\n */\nconst loadMap = (\n material: MeshStandardMaterial,\n slot: MapSlot,\n src: string,\n repeat: Vec2,\n srgb: boolean,\n): void => {\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, (texture) => {\n configureTexture(texture, repeat, srgb);\n texture.needsUpdate = true;\n material[slot] = texture;\n material.needsUpdate = true;\n });\n });\n return;\n }\n material[slot] = cache.acquire(src, repeat, srgb);\n};\n\n/**\n * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV\n * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must\n * provide one.\n */\nexport const applyTexture = (\n material: MeshStandardMaterial,\n texture: SurfaceTexture,\n): boolean => {\n const repeat = texture.repeat ?? [1, 1];\n loadMap(material, \"map\", texture.src, repeat, true);\n if (texture.normal) {\n loadMap(material, \"normalMap\", texture.normal, repeat, false);\n const scale = texture.normalScale ?? 1;\n material.normalScale = new Vector2(scale, scale);\n }\n if (texture.roughness) loadMap(material, \"roughnessMap\", texture.roughness, repeat, false);\n if (texture.ao) loadMap(material, \"aoMap\", texture.ao, repeat, false);\n material.needsUpdate = true;\n return texture.ao !== undefined;\n};\n\n/**\n * Release one reference to a texture assigned by `applyTexture`. Shared\n * textures stay alive while another material still samples them.\n */\nexport const releaseTexture = (texture: Texture): void => {\n cache.release(texture);\n};\n","import type {\n Object3D,\n PerspectiveCamera,\n Scene as ThreeScene,\n Texture,\n WebGLRenderer,\n} from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport { releaseTexture } from \"./builders\";\nimport {\n CapabilityRegistry,\n EYE_HEIGHT,\n type BuiltElement,\n type EnginePlugin,\n} from \"./plugin\";\n\n/** A mounted plugin element: what the plugin built plus its content hash. */\ntype MountedElement = BuiltElement & { hash: string };\n\nconst TEXTURE_SLOTS = [\"map\", \"normalMap\", \"roughnessMap\", \"aoMap\", \"metalnessMap\"];\n\nconst disposeObject = (object: Object3D): void => {\n const materials = new Set<unknown>();\n object.traverse((child) => {\n // Release any HTML video element backing a VideoTexture.\n const video = child.userData?.video as HTMLVideoElement | undefined;\n if (video) {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n }\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const material = mesh.material;\n if (Array.isArray(material)) for (const entry of material) materials.add(entry);\n else if (material) materials.add(material);\n });\n // A material can be shared by several meshes; dispose it, and the textures it\n // samples, once. Textures come from a shared cache, so they are released\n // rather than disposed outright.\n for (const entry of materials) {\n const material = entry as Record<string, unknown> & { dispose?: () => void };\n for (const slot of TEXTURE_SLOTS) {\n const texture = material[slot] as Texture | undefined;\n if (texture) releaseTexture(texture);\n }\n material.dispose?.();\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 plugins: EnginePlugin[];\n private readonly capabilities: CapabilityRegistry;\n /** Mounted plugin elements, per kind, keyed by element id (\"\" for singletons). */\n private readonly pluginElements = new Map<string, Map<string, MountedElement>>();\n\n constructor(\n root: ThreeScene,\n plugins: EnginePlugin[] = [],\n capabilities: CapabilityRegistry = new CapabilityRegistry(),\n ) {\n this.root = root;\n this.plugins = plugins;\n this.capabilities = capabilities;\n }\n\n reconcile(scene: Scene): void {\n // Eye height (default hang height for wall art) — a camera concern the\n // kernel owns for now; a camera plugin will publish it later.\n const eye = scene.camera.start.position[1];\n this.capabilities.provide(EYE_HEIGHT, eye, String(eye));\n // Plugins go last so kernel-provided capabilities (terrain, eyeHeight) are fresh.\n this.reconcilePlugins(scene);\n }\n\n /**\n * Reconcile every registered plugin's elements by content: an element\n * rebuilds when its definition changes or when a capability it depends on\n * was re-provided (the capability versions are part of the hash).\n */\n private reconcilePlugins(scene: Scene): void {\n for (const plugin of this.plugins) {\n const { kind, cardinality } = plugin.data;\n let mounted = this.pluginElements.get(kind);\n if (!mounted) {\n mounted = new Map();\n this.pluginElements.set(kind, mounted);\n }\n\n const raw = (scene as unknown as Record<string, unknown>)[kind];\n const items: Array<[string, unknown]> =\n cardinality === \"many\"\n ? Array.isArray(raw)\n ? raw.map((item, i) => [String((item as { id?: string }).id ?? i), item])\n : []\n : raw !== undefined\n ? [[\"\", raw]]\n : [];\n const deps = (plugin.dependsOn ?? []).map((name) => this.capabilities.version(name));\n\n const seen = new Set<string>();\n for (const [key, value] of items) {\n seen.add(key);\n const hash = JSON.stringify({ value, deps });\n const existing = mounted.get(key);\n if (existing?.hash === hash) continue;\n if (existing) this.removePluginElement(mounted, key, existing);\n const built = plugin.build(value as never, { capabilities: this.capabilities });\n if (built.object) this.root.add(built.object);\n mounted.set(key, { ...built, hash });\n }\n for (const [key, entry] of mounted)\n if (!seen.has(key)) this.removePluginElement(mounted, key, entry);\n\n // Publish this plugin's aggregate capabilities now that its elements are\n // in sync, so a later plugin that consumes them builds against fresh data.\n plugin.provide?.(\n items.map(([, value]) => value),\n { capabilities: this.capabilities },\n );\n }\n }\n\n private removePluginElement(\n mounted: Map<string, MountedElement>,\n key: string,\n entry: MountedElement,\n ): void {\n if (entry.object) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n entry.dispose?.();\n mounted.delete(key);\n }\n\n\n /** Advance time-based plugin elements (e.g. water ripple). Call once per frame. */\n updateElements(deltaMs: number): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.update?.(deltaMs);\n }\n\n /** Run every element's auxiliary pre-render pass (e.g. water reflection). */\n beforeRender(renderer: WebGLRenderer, scene: ThreeScene, camera: PerspectiveCamera): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.beforeRender?.(renderer, scene, camera);\n }\n\n\n /** Look up the live three.js object for any plugin element `id`. */\n get(id: string): Object3D | undefined {\n for (const mounted of this.pluginElements.values()) {\n const object = mounted.get(id)?.object;\n if (object) return object;\n }\n return undefined;\n }\n\n /** Live objects of one plugin kind, keyed by `id` — for that plugin's controller. */\n elementsOf(kind: string): ReadonlyMap<string, Object3D> {\n const out = new Map<string, Object3D>();\n const mounted = this.pluginElements.get(kind);\n if (mounted) for (const [id, entry] of mounted) if (entry.object) out.set(id, entry.object);\n return out;\n }\n\n dispose(): void {\n for (const [kind, mounted] of this.pluginElements) {\n for (const [key, entry] of mounted) this.removePluginElement(mounted, key, entry);\n this.pluginElements.delete(kind);\n }\n }\n}\n","import {\n Clock,\n HalfFloatType,\n PCFShadowMap,\n PerspectiveCamera,\n Scene as ThreeScene,\n StereoCamera,\n Vector2,\n WebGLRenderer,\n WebGLRenderTarget,\n type Object3D,\n} from \"three\";\nimport type { EffectComposer } from \"three/examples/jsm/postprocessing/EffectComposer.js\";\nimport type { Pass } from \"three/examples/jsm/postprocessing/Pass.js\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport { EnvironmentController } from \"./environment\";\nimport type { CameraApi } from \"./camera\";\nimport type { LightsApi, MediaController, SkyApi, VideoApi } from \"./api\";\nimport { InteractionController } from \"./interaction\";\nimport {\n ControlSettings,\n InputBus,\n MODE_PRESETS,\n loadControlFlags,\n saveControlFlags,\n type ControlFlags,\n type ControlMode,\n} from \"./input\";\nimport {\n CapabilityRegistry,\n isSystemPlugin,\n type ElementController,\n type EnginePlugin,\n type SystemInstance,\n type SystemPlugin,\n} from \"./plugin\";\nimport { SceneManager } from \"./SceneManager\";\n\n/** Maps a plugin kind/name onto the `ctx` slot its api fills. */\nconst CTX_SLOT = {\n camera: \"focus\",\n lights: \"lights\",\n video: \"videos\",\n sky: \"sky\",\n media: \"audio\",\n} as const;\n/** The system the cardboard render toggle drives (gyroscope head-look). */\nconst CARDBOARD_SYSTEM = \"cardboard\";\n\n/** MSAA sample count for antialiasing. `0` disables it. */\nexport type Antialias = 0 | 2 | 4 | 8;\n\n/** Toggleable rendering features. All can be flipped live via `setRender`. */\nexport interface RenderOptions {\n /**\n * Antialiasing as MSAA sample count (`0` off, `2`/`4`/`8`). Applied to the\n * post-process render target so it survives SSAO/bloom and toggles live,\n * unlike the canvas's native MSAA. Defaults to `4`.\n */\n antialias?: Antialias;\n /** Screen-space ambient occlusion as a post-process. Defaults to `false`. */\n ssao?: boolean;\n /**\n * Bloom strength: a soft glow bleeding out of the brightest areas. `0` off;\n * gentle around `0.3`, strong around `1`. Defaults to `0`.\n */\n bloom?: number;\n /** Real-time shadow mapping (PCF). Defaults to `true`. */\n shadows?: boolean;\n /**\n * Cardboard / stereoscopic mode: split the view into a left/right eye pair for\n * a phone-in-a-headset viewer. Skips post-processing while active. Defaults to\n * `false`.\n */\n cardboard?: boolean;\n /**\n * Interocular distance for cardboard mode, in world units. The physical ≈6.4 cm\n * is too much parallax to fuse comfortably through cardboard lenses, so this\n * defaults to `3`; tune it live for the viewer. `0` flattens to mono.\n */\n eyeSeparation?: number;\n}\n\nexport interface GalleryOptions extends RenderOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n /**\n * Runtime plugins: element plugins materialize a scene key; system plugins\n * (e.g. a camera) drive shared runtime state. Mixed freely — the kernel tells\n * them apart by shape.\n */\n plugins?: (EnginePlugin | SystemPlugin)[];\n}\n\nconst DEFAULT_EYE_SEPARATION = 3;\n\n/**\n * Drive the free-roam camera from outside the engine — e.g. on-screen touch\n * controls in the overlay. Inputs are analog (`-1..1`) and held until changed.\n */\nexport interface ControlsApi {\n /** Walk forward (`+1`) / back (`-1`); optional strafe right (`+1`) / left (`-1`). */\n setMoveInput(forward: number, strafe?: number): void;\n /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */\n setLookInput(x: number, y?: number): void;\n}\n\n/**\n * The visitor's camera-control preferences (view mode + capability flags). The\n * engine applies the effects that touch rendering/input — gyroscope head-look and\n * the stereo split — as the flags change; the overlay drives it from the mode\n * picker and settings, and reads it to show the right joysticks. See\n * `ControlSettings`.\n */\nexport interface ViewControlsApi {\n /** Current capability flags (a copy). */\n get(): ControlFlags;\n /** The mode the flags represent (`custom` when hand-tuned in settings). */\n getMode(): ControlMode;\n /** Switch to a mode's preset (keeps the look-invert axes). */\n setMode(mode: Exclude<ControlMode, \"custom\">): void;\n /** Set one capability flag (may drop the mode into `custom`). */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;\n /** Subscribe to changes; returns an unsubscribe. */\n subscribe(fn: (flags: ControlFlags) => void): () => void;\n}\n\n/** Control the HDRI sky / environment at runtime, for behavior code. */\nexport interface EnvironmentApi {\n /** Set the visible sky background brightness (`0` = black). */\n setBackgroundIntensity(value: number): void;\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/light `id`. */\n entity(id: string): Object3D | undefined;\n /** Control scene lights (dim, set intensity/color) by `id`. */\n readonly lights: LightsApi;\n /** Control background audio (play, pause, fade) by `id`. */\n readonly media: MediaController;\n /** Drive the camera externally (on-screen touch controls, etc.). */\n readonly controls: ControlsApi;\n /** The visitor's camera-control preferences (view mode + capability flags). */\n readonly viewControls: ViewControlsApi;\n /** Control the HDRI sky / environment at runtime. */\n readonly environment: EnvironmentApi;\n /** Drive the procedural sky's time of day. */\n readonly sky: SkyApi;\n /** Animate the camera to frame artworks (e.g. when an info panel opens/closes). */\n readonly camera: CameraApi;\n /** Control video screens (play, pause, show/hide) by `id`. */\n readonly video: VideoApi;\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 /** Run a handler every frame with the frame delta in ms; returns unsubscribe. */\n onFrame(handler: (deltaMs: number) => void): () => void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n /** Toggle rendering features (antialias, ssao, bloom, shadows) live. */\n setRender(options: RenderOptions): 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 // Native MSAA is off: antialiasing is an SMAA post-pass so it survives SSAO\n // and toggles live. PCF shadow filtering set up once; shadows enabled below.\n const renderer = new WebGLRenderer({ canvas, antialias: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n renderer.shadowMap.type = PCFShadowMap;\n\n // Live-toggleable render features.\n const render: Required<RenderOptions> = {\n antialias: options.antialias ?? 4,\n ssao: options.ssao ?? false,\n bloom: options.bloom ?? 0,\n shadows: options.shadows ?? true,\n cardboard: options.cardboard ?? false,\n eyeSeparation: options.eyeSeparation ?? DEFAULT_EYE_SEPARATION,\n };\n\n // Stereoscopic (cardboard) rendering. StereoCamera is core three (not the\n // WebGL-only StereoEffect addon); we render each eye with a scissored viewport,\n // which is portable to WebGPU. aspect 0.5 keeps each half undistorted.\n const stereo = new StereoCamera();\n stereo.aspect = 0.5;\n stereo.eyeSep = render.eyeSeparation;\n const stereoSize = new Vector2();\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(scene.camera.fov ?? 55, width / height, 0.1, 5000);\n const { position } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n // Split element plugins (materialize a scene key) from system plugins (drive\n // shared state like the camera). Elements go to the manager; systems are set\n // up below, once elements have published their capabilities.\n const allPlugins = options.plugins ?? [];\n const systemPlugins = allPlugins.filter(isSystemPlugin);\n const elementPlugins = allPlugins.filter((p): p is EnginePlugin => !isSystemPlugin(p));\n\n const capabilities = new CapabilityRegistry();\n const manager = new SceneManager(threeScene, elementPlugins, capabilities);\n manager.reconcile(scene);\n\n const input = new InputBus();\n // The visitor's control preferences (view mode + flags). Created early so system\n // plugins can read the flags (e.g. fpc reads the look-invert axes); the effects\n // that need the head-look system / render are wired below, once they exist. A\n // persisted choice (localStorage) wins over the initial cardboard option so the\n // visitor's last mode is restored across reloads.\n const controlSettings = new ControlSettings(\n loadControlFlags() ?? (options.cardboard ? MODE_PRESETS.cardboard : {}),\n );\n const environment = new EnvironmentController(threeScene);\n environment.reconcile(scene.environment);\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const emit = (event: string, payload?: unknown): void => {\n listeners.get(event)?.forEach((handler) => handler(payload));\n };\n const frameHandlers = new Set<(deltaMs: number) => void>();\n\n // Plugins publish their public apis here, keyed by kind/name; the kernel maps\n // them onto `ctx` slots (see `CTX_SLOT`). Element-plugin controllers (lights,\n // video) and system apis (camera, sky, media) both land in this one map.\n const apis = new Map<string, unknown>();\n\n // Element-plugin controllers: an aggregate api over each plugin's live objects\n // (e.g. `ctx.lights`). They advance in the render loop and tear down on dispose.\n const controllers: ElementController[] = [];\n for (const plugin of elementPlugins) {\n const controller = plugin.controller?.(() => manager.elementsOf(plugin.data.kind));\n if (!controller) continue;\n if (controller.api !== undefined) apis.set(plugin.data.kind, controller.api);\n controllers.push(controller);\n }\n\n // System plugins (camera, sky, audio…): drive shared runtime state each frame\n // and may expose an api. They get the scene graph + renderer for subsystems\n // that manage their own objects (sky) or listen on the camera (audio).\n const systems: SystemInstance[] = systemPlugins.map((plugin) => {\n const instance = plugin.setup({\n scene,\n camera,\n canvas,\n scene3d: threeScene,\n renderer,\n input,\n controlFlags: () => controlSettings.get(),\n capabilities,\n emit,\n resolve: (id) => manager.get(id),\n });\n if (instance.api !== undefined) apis.set(plugin.name, instance.api);\n return instance;\n });\n\n // `ctx.camera` is filled by the framing system (\"focus\"). Without it, a no-op\n // keeps callers (the overlay) safe.\n const cameraApi: CameraApi = (apis.get(CTX_SLOT.camera) as CameraApi | undefined) ?? {\n focusArtwork: () => {},\n centerArtwork: () => {},\n release: () => {},\n reset: () => {},\n isAnimating: () => false,\n isFocused: () => false,\n };\n\n // Cardboard render (stereo split) is a kernel feature; the gyroscope head-look\n // that rides with it is the `@scenography/plugin-cardboard` system. The render\n // toggle below drives its `setEnabled`.\n type HeadTracking = { setEnabled(enabled: boolean): void };\n const headTracking = apis.get(CARDBOARD_SYSTEM) as HeadTracking | undefined;\n // Apply the restored (or seeded) flags on load: enable gyro head-look and sync\n // the stereo render. (On iOS the gyro permission can't be granted outside a user\n // gesture, so a restored gyro mode only truly tracks once the visitor interacts —\n // enable() fails gracefully, nothing breaks.)\n const initialFlags = controlSettings.get();\n render.cardboard = initialFlags.splitScreen;\n if (initialFlags.gyro) headTracking?.setEnabled(true);\n\n // gyro and splitScreen are *orthogonal* here — unlike the legacy `cardboard`\n // render toggle that fused them — so a \"VR\" mode can aim with the gyro without\n // the stereo split. Apply those two effects (head-look system + stereo render)\n // as the flags change; the others are read by the overlay and input consumers.\n // render.cardboard / the initial setEnabled above already reflect the seed, so\n // the subscriber only handles subsequent changes.\n let prevFlags = controlSettings.get();\n controlSettings.subscribe((flags) => {\n if (flags.gyro !== prevFlags.gyro) headTracking?.setEnabled(flags.gyro);\n if (flags.splitScreen !== prevFlags.splitScreen) render.cardboard = flags.splitScreen;\n prevFlags = flags;\n saveControlFlags(flags); // remember the choice across reloads\n emit(\"controls:change\", { flags, mode: controlSettings.getMode() });\n });\n\n // No-op fallbacks so the overlay/behavior stay safe when a plugin is absent.\n const lightsApi = (apis.get(CTX_SLOT.lights) as LightsApi | undefined) ?? {\n set: () => {},\n dimAll: () => {},\n restore: () => {},\n };\n const videoApi = (apis.get(CTX_SLOT.video) as VideoApi | undefined) ?? {\n play: () => {},\n pause: () => {},\n setVisible: () => {},\n };\n const skyApi = (apis.get(CTX_SLOT.sky) as SkyApi | undefined) ?? {\n setTime: () => {},\n getTime: () => 12,\n setAzimuth: () => {},\n getAzimuth: () => 0,\n };\n const mediaApi = (apis.get(CTX_SLOT.media) as MediaController | undefined) ?? {\n play: () => {},\n pause: () => {},\n stop: () => {},\n setVolume: () => {},\n playing: () => false,\n setMuted: () => {},\n muted: () => false,\n };\n\n // Per-element interaction-distance gate, derived from each pickable element's\n // `infoRadius`. Read structurally: `infoRadius` is a convention a plugin (the\n // artwork plugin) can put on its elements, not a kernel field. A later\n // interaction pass can move this onto the built object's userData entirely.\n type Gated = { id: string; infoRadius?: number };\n const radiiOf = (s: Scene): Map<string, number> => {\n const gated = (s as { artworks?: Gated[] }).artworks ?? [];\n return new Map(\n gated.filter((a) => a.infoRadius !== undefined).map((a) => [a.id, a.infoRadius as number]),\n );\n };\n\n // Picking is suppressed while the camera animates and while a piece stays\n // focused: once its info panel opens, hovering must not keep highlighting it.\n const interaction = new InteractionController(\n camera,\n canvas,\n threeScene,\n emit,\n () => !cameraApi.isAnimating() && !cameraApi.isFocused(),\n );\n interaction.setRadii(radiiOf(scene));\n interaction.attach();\n const clock = new Clock();\n let rafId: number | null = null;\n\n // Post-processing (MSAA antialias + SSAO + bloom). The whole chain is imported\n // on demand so it only ships when a gallery uses it; until it resolves (or when\n // everything is off) we render straight to the canvas. Antialiasing is MSAA on\n // the composer's render target (`samples`), so changing the sample count\n // rebuilds the composer; SSAO/bloom just flip `.enabled`.\n // Bloom spread/threshold are fixed; its strength is the live `render.bloom`.\n const BLOOM = { radius: 0.4, threshold: 0.85 };\n type PostClasses = {\n EffectComposer: typeof import(\"three/examples/jsm/postprocessing/EffectComposer.js\").EffectComposer;\n RenderPass: typeof import(\"three/examples/jsm/postprocessing/RenderPass.js\").RenderPass;\n SSAOPass: typeof import(\"three/examples/jsm/postprocessing/SSAOPass.js\").SSAOPass;\n UnrealBloomPass: typeof import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\").UnrealBloomPass;\n OutputPass: typeof import(\"three/examples/jsm/postprocessing/OutputPass.js\").OutputPass;\n };\n let postClasses: PostClasses | null = null;\n let composer: EffectComposer | null = null;\n let ssaoPass: (Pass & { kernelRadius: number }) | null = null;\n let bloomPass: (Pass & { setSize(w: number, h: number): void; strength: number }) | null = null;\n let composerLoading = false;\n\n const pixelRatio = Math.min(window.devicePixelRatio, 2);\n const needsComposer = (): boolean => render.antialias > 0 || render.ssao || render.bloom > 0;\n\n // (Re)build the composer from the loaded classes at the current sample count.\n const buildComposer = (): void => {\n if (!postClasses) return;\n composer?.dispose();\n const { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass } = postClasses;\n // Multisampled target → MSAA that survives the post chain. `samples` is\n // preserved across resizes (setSize only changes width/height). HalfFloat\n // matches the composer's own default and gives bloom its HDR range.\n const target = new WebGLRenderTarget(1, 1, { samples: render.antialias, type: HalfFloatType });\n const c = new EffectComposer(renderer, target);\n c.setPixelRatio(pixelRatio);\n c.setSize(width, height);\n c.addPass(new RenderPass(threeScene, camera));\n const ssao = new SSAOPass(threeScene, camera, width, height);\n ssao.kernelRadius = 16;\n ssao.enabled = render.ssao;\n c.addPass(ssao);\n const bloom = new UnrealBloomPass(new Vector2(width, height), render.bloom, BLOOM.radius, BLOOM.threshold);\n bloom.enabled = render.bloom > 0;\n c.addPass(bloom);\n // Restores tone mapping / sRGB that the composer would otherwise skip.\n c.addPass(new OutputPass());\n composer = c;\n ssaoPass = ssao;\n bloomPass = bloom;\n };\n\n const ensureComposer = (): void => {\n if (!needsComposer()) return;\n if (postClasses) {\n if (!composer) buildComposer();\n return;\n }\n if (composerLoading) return;\n composerLoading = true;\n void Promise.all([\n import(\"three/examples/jsm/postprocessing/EffectComposer.js\"),\n import(\"three/examples/jsm/postprocessing/RenderPass.js\"),\n import(\"three/examples/jsm/postprocessing/SSAOPass.js\"),\n import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\"),\n import(\"three/examples/jsm/postprocessing/OutputPass.js\"),\n ]).then(([{ EffectComposer }, { RenderPass }, { SSAOPass }, { UnrealBloomPass }, { OutputPass }]) => {\n postClasses = { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass };\n composerLoading = false;\n buildComposer();\n });\n };\n\n // Recompile materials so a shadowMap.enabled change takes effect.\n const refreshShadows = (): void => {\n renderer.shadowMap.enabled = render.shadows;\n threeScene.traverse((obj) => {\n const mat = (obj as { material?: unknown }).material;\n if (!mat) return;\n for (const m of Array.isArray(mat) ? mat : [mat]) (m as { needsUpdate: boolean }).needsUpdate = true;\n });\n };\n refreshShadows();\n ensureComposer();\n\n // Split the canvas into a left/right eye pair. Mirrors what StereoEffect does\n // internally, but stays in our loop and uses only setViewport/setScissor.\n const renderStereo = (): void => {\n camera.updateWorldMatrix(true, false);\n stereo.update(camera);\n renderer.getSize(stereoSize);\n const w = stereoSize.x;\n const h = stereoSize.y;\n if (renderer.autoClear) renderer.clear();\n renderer.setScissorTest(true);\n renderer.setScissor(0, 0, w / 2, h);\n renderer.setViewport(0, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraL);\n renderer.setScissor(w / 2, 0, w / 2, h);\n renderer.setViewport(w / 2, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraR);\n renderer.setScissorTest(false);\n renderer.setViewport(0, 0, w, h);\n };\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n const delta = clock.getDelta();\n const deltaMs = delta * 1000;\n // Systems drive the camera in registration order: the roaming camera first,\n // then the framing system, which may override it during a focus animation.\n for (const system of systems) system.update?.(deltaMs);\n interaction.update(deltaMs);\n manager.updateElements(deltaMs);\n for (const controller of controllers) controller.update?.(deltaMs);\n // Behavior runs after the camera moved this frame, before rendering. (The\n // cardboard system, last in `systems`, already had the final say on\n // orientation above.)\n for (const handler of frameHandlers) handler(deltaMs);\n // Auxiliary passes (e.g. the water's planar reflection) render into their\n // own targets from the current camera, before the main frame.\n manager.beforeRender(renderer, threeScene, camera);\n if (render.cardboard) renderStereo();\n else if (composer && needsComposer()) composer.render(delta);\n else renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n lights: lightsApi,\n media: mediaApi,\n controls: {\n setMoveInput: (forward, strafe) => input.setMove(forward, strafe),\n setLookInput: (x, y) => input.setLook(x, y),\n },\n viewControls: {\n get: () => controlSettings.get(),\n getMode: () => controlSettings.getMode(),\n setMode: (mode) => controlSettings.setMode(mode),\n setFlag: (key, value) => controlSettings.setFlag(key, value),\n subscribe: (fn) => controlSettings.subscribe(fn),\n },\n environment: {\n setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v),\n },\n sky: skyApi,\n camera: cameraApi,\n video: videoApi,\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n interaction.setRadii(radiiOf(next));\n environment.reconcile(next.environment);\n for (const system of systems) system.setScene?.(next);\n const fov = next.camera.fov ?? 55;\n if (camera.fov !== fov) {\n camera.fov = fov;\n camera.updateProjectionMatrix();\n }\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 for (const system of systems) system.dispose?.();\n for (const controller of controllers) controller.dispose?.();\n interaction.detach();\n environment.dispose();\n manager.dispose();\n composer?.dispose();\n renderer.dispose();\n listeners.clear();\n frameHandlers.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 emit(event, payload);\n },\n onFrame(handler) {\n frameHandlers.add(handler);\n return () => frameHandlers.delete(handler);\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n composer?.setSize(w, h);\n ssaoPass?.setSize(w, h);\n bloomPass?.setSize(w, h);\n },\n setRender(next) {\n if (next.shadows !== undefined && next.shadows !== render.shadows) {\n render.shadows = next.shadows;\n refreshShadows();\n }\n if (next.ssao !== undefined) render.ssao = next.ssao;\n if (next.bloom !== undefined) render.bloom = next.bloom;\n // The sample count lives on the render target, so a change rebuilds the\n // composer (cheap, and only on user toggle).\n const aaChanged = next.antialias !== undefined && next.antialias !== render.antialias;\n if (next.antialias !== undefined) render.antialias = next.antialias;\n if (next.cardboard !== undefined && next.cardboard !== render.cardboard) {\n // Legacy stereo toggle: route through the control state so mode/flags and\n // the render stay one source of truth (the subscriber applies gyro +\n // split; setEnabled thus still runs in this click gesture, which iOS needs\n // for the motion-permission prompt). Superseded by `ctx.viewControls` once\n // the mode picker replaces the cardboard button.\n controlSettings.setMode(next.cardboard ? \"cardboard\" : \"touch\");\n }\n if (next.eyeSeparation !== undefined) {\n render.eyeSeparation = next.eyeSeparation;\n stereo.eyeSep = next.eyeSeparation;\n }\n if (ssaoPass) ssaoPass.enabled = render.ssao;\n if (bloomPass) {\n bloomPass.strength = render.bloom;\n bloomPass.enabled = render.bloom > 0;\n }\n if (aaChanged && composer) buildComposer();\n ensureComposer(); // load/build the chain if an effect was just turned on\n },\n };\n}\n"],"mappings":"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"}
|