@scenography/engine 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -65,6 +65,7 @@ const HIGHLIGHT_COLOR = "#ef4444";
65
65
  const HIGHLIGHT_THICKNESS = 1.5;
66
66
  const HIGHLIGHT_MARGIN = 1.5;
67
67
  const CLICK_SLOP = 4;
68
+ const PICK_INTERVAL = 60;
68
69
  /**
69
70
  * Pointer picking for artworks: hovering a piece's click target (its label, or
70
71
  * the piece itself when it has none) draws a pulsing red frame and shows a
@@ -86,6 +87,8 @@ var InteractionController = class {
86
87
  radii = /* @__PURE__ */ new Map();
87
88
  hovered = null;
88
89
  time = 0;
90
+ pickElapsed = 0;
91
+ pointerMoved = false;
89
92
  highlight = new three.Group();
90
93
  highlightMat = null;
91
94
  handlers = [];
@@ -128,6 +131,7 @@ var InteractionController = class {
128
131
  const rect = this.canvas.getBoundingClientRect();
129
132
  this.pointer.set((e.clientX - rect.left) / rect.width * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
130
133
  this.over = true;
134
+ this.pointerMoved = true;
131
135
  }
132
136
  onMove(e) {
133
137
  this.setPointer(e);
@@ -169,18 +173,38 @@ var InteractionController = class {
169
173
  }
170
174
  return null;
171
175
  }
172
- /** Recompute hover state and animate the highlight. Call once per frame. */
176
+ /**
177
+ * Recompute hover state and animate the highlight. Call once per frame.
178
+ * Hover picking is throttled: a full-scene raycast runs at most once every
179
+ * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can
180
+ * move without the pointer, so the interval keeps hovering correct while
181
+ * walking.
182
+ */
173
183
  update(deltaMs) {
174
184
  this.time += deltaMs;
175
- const pick = this.over && this.canInteract() ? this.raycast() : null;
176
- if ((pick?.id ?? null) !== this.hovered) {
177
- this.hovered = pick?.id ?? null;
178
- this.canvas.style.cursor = pick ? "pointer" : "";
179
- if (pick) this.buildHighlight(pick);
180
- else this.highlight.visible = false;
185
+ if (!this.over || !this.canInteract()) {
186
+ this.pickElapsed = PICK_INTERVAL;
187
+ this.pointerMoved = false;
188
+ this.applyPick(null);
189
+ } else {
190
+ this.pickElapsed += deltaMs;
191
+ if (this.pointerMoved) this.pickElapsed = PICK_INTERVAL;
192
+ this.pointerMoved = false;
193
+ if (this.pickElapsed >= PICK_INTERVAL) {
194
+ this.pickElapsed = 0;
195
+ this.applyPick(this.raycast());
196
+ }
181
197
  }
182
198
  if (this.highlight.visible && this.highlightMat) this.highlightMat.opacity = .45 + .45 * (.5 + .5 * Math.sin(this.time / 320));
183
199
  }
200
+ /** Swap the hovered piece, touching the canvas and the highlight only on change. */
201
+ applyPick(pick) {
202
+ if ((pick?.id ?? null) === this.hovered) return;
203
+ this.hovered = pick?.id ?? null;
204
+ this.canvas.style.cursor = pick ? "pointer" : "";
205
+ if (pick) this.buildHighlight(pick);
206
+ else this.highlight.visible = false;
207
+ }
184
208
  clearHighlight() {
185
209
  for (const child of [...this.highlight.children]) {
186
210
  this.highlight.remove(child);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["EquirectangularReflectionMapping","Raycaster","Vector2","Group","Vector3","MeshBasicMaterial","Mesh","BoxGeometry","Quaternion","FLAG_KEYS","RepeatWrapping","SRGBColorSpace","NoColorSpace","TextureLoader","Vector2","WebGLRenderer","PCFShadowMap","StereoCamera","Vector2","ThreeScene","PerspectiveCamera","Clock","WebGLRenderTarget","HalfFloatType"],"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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"}
1
+ {"version":3,"file":"index.cjs","names":["EquirectangularReflectionMapping","Raycaster","Vector2","Group","Vector3","MeshBasicMaterial","Mesh","BoxGeometry","Quaternion","FLAG_KEYS","RepeatWrapping","SRGBColorSpace","NoColorSpace","TextureLoader","Vector2","WebGLRenderer","PCFShadowMap","StereoCamera","Vector2","ThreeScene","PerspectiveCamera","Clock","WebGLRenderTarget","HalfFloatType"],"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\nconst PICK_INTERVAL = 60; // ms between hover raycasts while the pointer is over the canvas\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 private pickElapsed = 0;\n private pointerMoved = false;\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 this.pointerMoved = 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 /**\n * Recompute hover state and animate the highlight. Call once per frame.\n * Hover picking is throttled: a full-scene raycast runs at most once every\n * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can\n * move without the pointer, so the interval keeps hovering correct while\n * walking.\n */\n update(deltaMs: number): void {\n this.time += deltaMs;\n\n if (!this.over || !this.canInteract()) {\n // No picking while the visitor cannot interact; pick at once when they can.\n this.pickElapsed = PICK_INTERVAL;\n this.pointerMoved = false;\n this.applyPick(null);\n } else {\n this.pickElapsed += deltaMs;\n if (this.pointerMoved) this.pickElapsed = PICK_INTERVAL;\n this.pointerMoved = false;\n if (this.pickElapsed >= PICK_INTERVAL) {\n this.pickElapsed = 0;\n this.applyPick(this.raycast());\n }\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 /** Swap the hovered piece, touching the canvas and the highlight only on change. */\n private applyPick(pick: Pick | null): void {\n if ((pick?.id ?? null) === this.hovered) return;\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 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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"}
package/dist/index.d.cts CHANGED
@@ -646,6 +646,8 @@ declare class InteractionController {
646
646
  private radii;
647
647
  private hovered;
648
648
  private time;
649
+ private pickElapsed;
650
+ private pointerMoved;
649
651
  private readonly highlight;
650
652
  private highlightMat;
651
653
  private readonly handlers;
@@ -660,8 +662,16 @@ declare class InteractionController {
660
662
  private onUp;
661
663
  /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */
662
664
  private raycast;
663
- /** Recompute hover state and animate the highlight. Call once per frame. */
665
+ /**
666
+ * Recompute hover state and animate the highlight. Call once per frame.
667
+ * Hover picking is throttled: a full-scene raycast runs at most once every
668
+ * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can
669
+ * move without the pointer, so the interval keeps hovering correct while
670
+ * walking.
671
+ */
664
672
  update(deltaMs: number): void;
673
+ /** Swap the hovered piece, touching the canvas and the highlight only on change. */
674
+ private applyPick;
665
675
  private clearHighlight;
666
676
  /** Position a fresh pulsing red frame over the picked target's front face. */
667
677
  private buildHighlight;
package/dist/index.d.mts CHANGED
@@ -646,6 +646,8 @@ declare class InteractionController {
646
646
  private radii;
647
647
  private hovered;
648
648
  private time;
649
+ private pickElapsed;
650
+ private pointerMoved;
649
651
  private readonly highlight;
650
652
  private highlightMat;
651
653
  private readonly handlers;
@@ -660,8 +662,16 @@ declare class InteractionController {
660
662
  private onUp;
661
663
  /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */
662
664
  private raycast;
663
- /** Recompute hover state and animate the highlight. Call once per frame. */
665
+ /**
666
+ * Recompute hover state and animate the highlight. Call once per frame.
667
+ * Hover picking is throttled: a full-scene raycast runs at most once every
668
+ * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can
669
+ * move without the pointer, so the interval keeps hovering correct while
670
+ * walking.
671
+ */
664
672
  update(deltaMs: number): void;
673
+ /** Swap the hovered piece, touching the canvas and the highlight only on change. */
674
+ private applyPick;
665
675
  private clearHighlight;
666
676
  /** Position a fresh pulsing red frame over the picked target's front face. */
667
677
  private buildHighlight;
package/dist/index.mjs CHANGED
@@ -64,6 +64,7 @@ const HIGHLIGHT_COLOR = "#ef4444";
64
64
  const HIGHLIGHT_THICKNESS = 1.5;
65
65
  const HIGHLIGHT_MARGIN = 1.5;
66
66
  const CLICK_SLOP = 4;
67
+ const PICK_INTERVAL = 60;
67
68
  /**
68
69
  * Pointer picking for artworks: hovering a piece's click target (its label, or
69
70
  * the piece itself when it has none) draws a pulsing red frame and shows a
@@ -85,6 +86,8 @@ var InteractionController = class {
85
86
  radii = /* @__PURE__ */ new Map();
86
87
  hovered = null;
87
88
  time = 0;
89
+ pickElapsed = 0;
90
+ pointerMoved = false;
88
91
  highlight = new Group();
89
92
  highlightMat = null;
90
93
  handlers = [];
@@ -127,6 +130,7 @@ var InteractionController = class {
127
130
  const rect = this.canvas.getBoundingClientRect();
128
131
  this.pointer.set((e.clientX - rect.left) / rect.width * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
129
132
  this.over = true;
133
+ this.pointerMoved = true;
130
134
  }
131
135
  onMove(e) {
132
136
  this.setPointer(e);
@@ -168,18 +172,38 @@ var InteractionController = class {
168
172
  }
169
173
  return null;
170
174
  }
171
- /** Recompute hover state and animate the highlight. Call once per frame. */
175
+ /**
176
+ * Recompute hover state and animate the highlight. Call once per frame.
177
+ * Hover picking is throttled: a full-scene raycast runs at most once every
178
+ * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can
179
+ * move without the pointer, so the interval keeps hovering correct while
180
+ * walking.
181
+ */
172
182
  update(deltaMs) {
173
183
  this.time += deltaMs;
174
- const pick = this.over && this.canInteract() ? this.raycast() : null;
175
- if ((pick?.id ?? null) !== this.hovered) {
176
- this.hovered = pick?.id ?? null;
177
- this.canvas.style.cursor = pick ? "pointer" : "";
178
- if (pick) this.buildHighlight(pick);
179
- else this.highlight.visible = false;
184
+ if (!this.over || !this.canInteract()) {
185
+ this.pickElapsed = PICK_INTERVAL;
186
+ this.pointerMoved = false;
187
+ this.applyPick(null);
188
+ } else {
189
+ this.pickElapsed += deltaMs;
190
+ if (this.pointerMoved) this.pickElapsed = PICK_INTERVAL;
191
+ this.pointerMoved = false;
192
+ if (this.pickElapsed >= PICK_INTERVAL) {
193
+ this.pickElapsed = 0;
194
+ this.applyPick(this.raycast());
195
+ }
180
196
  }
181
197
  if (this.highlight.visible && this.highlightMat) this.highlightMat.opacity = .45 + .45 * (.5 + .5 * Math.sin(this.time / 320));
182
198
  }
199
+ /** Swap the hovered piece, touching the canvas and the highlight only on change. */
200
+ applyPick(pick) {
201
+ if ((pick?.id ?? null) === this.hovered) return;
202
+ this.hovered = pick?.id ?? null;
203
+ this.canvas.style.cursor = pick ? "pointer" : "";
204
+ if (pick) this.buildHighlight(pick);
205
+ else this.highlight.visible = false;
206
+ }
183
207
  clearHighlight() {
184
208
  for (const child of [...this.highlight.children]) {
185
209
  this.highlight.remove(child);
@@ -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/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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"}
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\nconst PICK_INTERVAL = 60; // ms between hover raycasts while the pointer is over the canvas\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 private pickElapsed = 0;\n private pointerMoved = false;\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 this.pointerMoved = 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 /**\n * Recompute hover state and animate the highlight. Call once per frame.\n * Hover picking is throttled: a full-scene raycast runs at most once every\n * `PICK_INTERVAL` ms, or immediately after the pointer moves. The camera can\n * move without the pointer, so the interval keeps hovering correct while\n * walking.\n */\n update(deltaMs: number): void {\n this.time += deltaMs;\n\n if (!this.over || !this.canInteract()) {\n // No picking while the visitor cannot interact; pick at once when they can.\n this.pickElapsed = PICK_INTERVAL;\n this.pointerMoved = false;\n this.applyPick(null);\n } else {\n this.pickElapsed += deltaMs;\n if (this.pointerMoved) this.pickElapsed = PICK_INTERVAL;\n this.pointerMoved = false;\n if (this.pickElapsed >= PICK_INTERVAL) {\n this.pickElapsed = 0;\n this.applyPick(this.raycast());\n }\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 /** Swap the hovered piece, touching the canvas and the highlight only on change. */\n private applyPick(pick: Pick | null): void {\n if ((pick?.id ?? null) === this.hovered) return;\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 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenography/engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The scenography runtime: materializes a scene definition with three.js and runs it. Reconciles by id for hot scene changes.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -23,7 +23,7 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "@scenography/scene": "0.4.0"
26
+ "@scenography/scene": "0.4.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/three": "^0.160.0",