@scenography/engine 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +874 -255
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +597 -52
- package/dist/index.d.mts +597 -52
- package/dist/index.mjs +861 -254
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Vector3","Quaternion","Vector3","Vector2","Quaternion","Vector3","Quaternion","Vector3","Mesh","PlaneGeometry","MeshStandardMaterial","DoubleSide","TextureLoader","Group","Quaternion","Vector3","Mesh","PlaneGeometry","MeshStandardMaterial","SRGBColorSpace","MeshBasicMaterial","WebGLRenderer","ThreeScene","PerspectiveCamera","AmbientLight","HemisphereLight","Clock"],"sources":["../src/controls/boundaries.ts","../src/controls/headBob.ts","../src/controls/keyboard.ts","../src/controls/mouse.ts","../src/controls/FreeRoamControls.ts","../src/builders/wall.ts","../src/builders/artwork.ts","../src/SceneManager.ts","../src/createGallery.ts"],"sourcesContent":["import { Vector3 } from \"three\";\n\n/**\n * Clamp a step on the XZ plane to a square `[-bound, bound]`. Axes are clamped\n * independently so sliding along a wall still works when one axis is blocked.\n */\nexport const clampToBounds = (current: Vector3, step: Vector3, bound: number): Vector3 => {\n const next = new Vector3().addVectors(current, step);\n if (next.x < -bound || next.x > bound) next.x = current.x;\n if (next.z < -bound || next.z > bound) next.z = current.z;\n return next;\n};\n","/** State for the subtle vertical bob applied while walking. */\nexport interface HeadBobState {\n active: boolean;\n timer: number;\n}\n\nexport const createHeadBobState = (): HeadBobState => ({ active: false, timer: 0 });\n\n/**\n * Advance the head-bob timer by `elapsed` seconds, snapping to half-wavelength\n * steps so the bob always settles back to neutral when movement stops.\n */\nexport const updateHeadBob = (state: HeadBobState, elapsed: number): void => {\n if (!state.active) return;\n\n const wavelength = Math.PI;\n const nextStep = 1 + Math.floor(((state.timer + 1e-6) * 10) / wavelength);\n const nextStepTime = (nextStep * wavelength) / 10;\n state.timer = Math.min(state.timer + elapsed, nextStepTime);\n\n if (state.timer === nextStepTime) state.active = false;\n};\n\n/** Vertical offset to add to the camera, in world units. */\nexport const headBobOffset = (state: HeadBobState): number => Math.sin(state.timer * 10) * 1.5;\n","import { Quaternion, Vector3 } from \"three\";\n\n/** Set of currently-pressed key codes. */\nexport type KeyboardState = Record<string, boolean>;\n\nexport const createKeyboardState = (): KeyboardState => ({});\n\nexport const onKeyDown = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = true;\n};\n\nexport const onKeyUp = (state: KeyboardState, event: KeyboardEvent): void => {\n state[event.code] = false;\n};\n\n/**\n * Compute the per-frame translation from WASD input, oriented by the current\n * yaw (`phi`). Holding Shift acts as a speed booster.\n */\nexport const movementFromKeyboard = (state: KeyboardState, phi: number, delta: number): Vector3 => {\n const has = (code: string): number => (state[code] ? 1 : 0);\n const forwardVelocity = has(\"KeyW\") - has(\"KeyS\");\n const strafeVelocity = has(\"KeyA\") - has(\"KeyD\");\n const boosted = state[\"ShiftLeft\"] || state[\"ShiftRight\"];\n const factor = boosted ? 30 : 10;\n\n const yaw = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), phi);\n\n const forward = new Vector3(0, 0, -10)\n .applyQuaternion(yaw)\n .multiplyScalar(forwardVelocity * delta * factor);\n const left = new Vector3(-10, 0, 0)\n .applyQuaternion(yaw)\n .multiplyScalar(strafeVelocity * delta * factor);\n\n return forward.add(left);\n};\n","import { Quaternion, Vector2, Vector3 } from \"three\";\n\n/** Mutable pointer state accumulated between frames. */\nexport interface MouseState {\n /** Whether the primary button is held (drag-to-look). */\n dragging: boolean;\n pos: Vector2;\n prevPos: Vector2;\n delta: Vector2;\n}\n\n/** Look angles driven by the pointer. */\nexport interface LookState {\n phi: number;\n theta: number;\n}\n\nconst PHI_SPEED = 8;\nconst THETA_SPEED = 5;\n\nexport const createMouseState = (): MouseState => ({\n dragging: false,\n pos: new Vector2(),\n prevPos: new Vector2(),\n delta: new Vector2(),\n});\n\nconst clamp = (x: number, a: number, b: number): number => Math.min(Math.max(x, a), b);\n\nexport const onMouseDown = (state: MouseState, event: MouseEvent): void => {\n state.dragging = event.button === 0;\n};\n\nexport const onMouseUp = (state: MouseState): void => {\n state.dragging = false;\n};\n\nexport const onMouseMove = (state: MouseState, event: MouseEvent): void => {\n state.pos.set(event.pageX - window.innerWidth / 2, event.pageY - window.innerHeight / 2);\n state.delta.subVectors(state.pos, state.prevPos);\n};\n\nexport const commitMouseFrame = (state: MouseState): void => {\n state.prevPos.copy(state.pos);\n state.delta.set(0, 0);\n};\n\n/**\n * Integrate the pointer delta into a look quaternion. Mutates `look` and\n * returns the resulting rotation. Yaw is unbounded; pitch is clamped to avoid\n * flipping over.\n */\nexport const updateLookFromMouse = (mouse: MouseState, look: LookState): Quaternion => {\n const xh = mouse.delta.x / window.innerWidth;\n const yh = mouse.delta.y / window.innerHeight;\n\n look.phi += -xh * PHI_SPEED;\n look.theta = clamp(look.theta + -yh * THETA_SPEED, -Math.PI / 3, Math.PI / 3);\n\n const qy = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), look.phi);\n const qx = new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), look.theta);\n return new Quaternion().multiply(qy).multiply(qx);\n};\n","import { Quaternion, Vector3, type PerspectiveCamera } from \"three\";\n\nimport { clampToBounds } from \"./boundaries\";\nimport { createHeadBobState, headBobOffset, updateHeadBob } from \"./headBob\";\nimport { createKeyboardState, movementFromKeyboard, onKeyDown, onKeyUp } from \"./keyboard\";\nimport {\n commitMouseFrame,\n createMouseState,\n onMouseDown,\n onMouseMove,\n onMouseUp,\n updateLookFromMouse,\n type LookState,\n} from \"./mouse\";\n\nexport interface FreeRoamOptions {\n /** Square half-extent that clamps XZ movement. */\n bounds?: number;\n /** Starting position. */\n position?: Vector3;\n /** Starting yaw in radians. */\n yaw?: number;\n}\n\n/**\n * First-person, free-roaming camera controls ported from the original Virtual\n * Gallery: drag-to-look with the mouse, WASD to walk, a subtle head-bob while\n * moving, and square boundary clamping. The cardboard / device-orientation path\n * was intentionally dropped.\n *\n * Framework-agnostic: construct it with a camera and the canvas element, call\n * `attach()`, then `update(delta)` once per frame.\n */\nexport class FreeRoamControls {\n bounds: number;\n\n private readonly camera: PerspectiveCamera;\n private readonly domElement: HTMLElement;\n private readonly mouse = createMouseState();\n private readonly keyboard = createKeyboardState();\n private readonly headBob = createHeadBobState();\n private readonly look: LookState = { phi: 0, theta: 0 };\n private rotation = new Quaternion();\n private translation = new Vector3(0, 170, 0);\n\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(camera: PerspectiveCamera, domElement: HTMLElement, options: FreeRoamOptions = {}) {\n this.camera = camera;\n this.domElement = domElement;\n this.bounds = options.bounds ?? 450;\n if (options.position) this.translation.copy(options.position);\n if (options.yaw !== undefined) this.look.phi = options.yaw;\n this.rotation.setFromAxisAngle(new Vector3(0, 1, 0), this.look.phi);\n }\n\n attach(): void {\n const bind = (target: EventTarget, type: string, listener: EventListener): void => {\n target.addEventListener(type, listener);\n this.handlers.push([target, type, listener]);\n };\n\n bind(this.domElement, \"mousedown\", (e) => onMouseDown(this.mouse, e as MouseEvent));\n bind(window, \"mousemove\", (e) => onMouseMove(this.mouse, e as MouseEvent));\n bind(window, \"mouseup\", () => onMouseUp(this.mouse));\n bind(window, \"keydown\", (e) => onKeyDown(this.keyboard, e as KeyboardEvent));\n bind(window, \"keyup\", (e) => onKeyUp(this.keyboard, e as KeyboardEvent));\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers)\n target.removeEventListener(type, listener);\n this.handlers.length = 0;\n }\n\n /** Advance the camera by `delta` seconds of input. */\n update(delta: number): void {\n if (this.mouse.dragging) this.rotation = updateLookFromMouse(this.mouse, this.look);\n\n const step = movementFromKeyboard(this.keyboard, this.look.phi, delta);\n this.translation = clampToBounds(this.translation, step, this.bounds);\n if (step.x !== 0 || step.z !== 0) this.headBob.active = true;\n updateHeadBob(this.headBob, delta);\n\n this.camera.quaternion.copy(this.rotation);\n this.camera.position.copy(this.translation);\n this.camera.position.y += headBobOffset(this.headBob);\n\n const target = new Vector3(0, 0, -1)\n .applyQuaternion(this.rotation)\n .multiplyScalar(100)\n .add(this.translation);\n this.camera.lookAt(target);\n\n commitMouseFrame(this.mouse);\n }\n}\n","import { DoubleSide, Mesh, MeshStandardMaterial, PlaneGeometry, type Object3D } from \"three\";\nimport type { Wall } from \"@scenography/core\";\n\n/** Build the three.js object that materializes a `Wall` definition. */\nexport function buildWall(wall: Wall): Object3D {\n const [width, height] = wall.size;\n const geometry = new PlaneGeometry(width, height);\n const material = new MeshStandardMaterial({ color: wall.color ?? \"#ffffff\", side: DoubleSide });\n\n const mesh = new Mesh(geometry, material);\n mesh.position.set(wall.position[0], wall.position[1], wall.position[2]);\n mesh.rotation.y = wall.rotation ?? 0;\n mesh.name = `wall:${wall.id}`;\n mesh.userData.scenographyId = wall.id;\n return mesh;\n}\n","import {\n Group,\n Mesh,\n MeshBasicMaterial,\n MeshStandardMaterial,\n PlaneGeometry,\n Quaternion,\n SRGBColorSpace,\n TextureLoader,\n Vector3,\n type Object3D,\n} from \"three\";\nimport type { Artwork, Wall } from \"@scenography/core\";\n\nconst loader = new TextureLoader();\nconst FRAME_MARGIN = 4;\nconst SURFACE_OFFSET = 1;\n\n/**\n * Build the three.js object for an `Artwork`, placing it on its wall. The piece\n * is positioned in the wall's local plane via `offset` and nudged forward along\n * the wall normal so it sits in front of the surface.\n */\nexport function buildArtwork(artwork: Artwork, wall: Wall): Object3D {\n const [width, height] = artwork.size;\n const [offsetX, offsetY] = artwork.offset ?? [0, 0];\n const yaw = wall.rotation ?? 0;\n\n const group = new Group();\n const wallQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), yaw);\n const right = new Vector3(1, 0, 0).applyQuaternion(wallQuat);\n const normal = new Vector3(0, 0, 1).applyQuaternion(wallQuat);\n\n const position = new Vector3(wall.position[0], wall.position[1], wall.position[2])\n .add(right.clone().multiplyScalar(offsetX))\n .add(new Vector3(0, offsetY, 0))\n .add(normal.clone().multiplyScalar(SURFACE_OFFSET));\n group.position.copy(position);\n group.rotation.y = yaw;\n\n // Frame: a slightly larger dark plane behind the image.\n const frame = new Mesh(\n new PlaneGeometry(width + FRAME_MARGIN, height + FRAME_MARGIN),\n new MeshStandardMaterial({ color: \"#1a1a1a\" }),\n );\n group.add(frame);\n\n // Image: textured plane, nudged forward so it reads above the frame.\n const texture = loader.load(artwork.src);\n texture.colorSpace = SRGBColorSpace;\n const image = new Mesh(new PlaneGeometry(width, height), new MeshBasicMaterial({ map: texture }));\n image.position.z = 0.1;\n group.add(image);\n\n group.name = `artwork:${artwork.id}`;\n group.userData.scenographyId = artwork.id;\n group.userData.wallId = artwork.wall;\n return group;\n}\n","import type { Object3D, Scene as ThreeScene } from \"three\";\nimport type { Artwork, Scene, Wall } from \"@scenography/core\";\n\nimport { buildArtwork, buildWall } from \"./builders\";\n\ninterface Entry {\n object: Object3D;\n /** JSON snapshot used to detect changes between reconciles. */\n hash: string;\n}\n\nconst disposeObject = (object: Object3D): void => {\n object.traverse((child) => {\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const material = mesh.material;\n const dispose = (m: unknown): void => (m as { dispose?: () => void })?.dispose?.();\n if (Array.isArray(material)) material.forEach(dispose);\n else dispose(material);\n });\n};\n\n/**\n * Owns the live three.js scene graph and keeps it in sync with a `Scene`\n * definition. `reconcile` diffs the incoming definition against what is already\n * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what\n * makes hot scene edits cheap: add a wing or drop one while moving and only the\n * touched objects are rebuilt.\n */\nexport class SceneManager {\n private readonly root: ThreeScene;\n private readonly walls = new Map<string, Entry>();\n private readonly artworks = new Map<string, Entry>();\n\n constructor(root: ThreeScene) {\n this.root = root;\n }\n\n reconcile(scene: Scene): void {\n const wallById = new Map(scene.walls.map((w) => [w.id, w] as const));\n this.reconcileWalls(scene.walls);\n this.reconcileArtworks(scene.artworks, wallById);\n }\n\n private reconcileWalls(walls: Wall[]): void {\n const next = new Set(walls.map((w) => w.id));\n for (const [id, entry] of this.walls) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.walls.delete(id);\n }\n }\n for (const wall of walls) {\n const hash = JSON.stringify(wall);\n const existing = this.walls.get(wall.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildWall(wall);\n this.root.add(object);\n this.walls.set(wall.id, { object, hash });\n }\n }\n\n private reconcileArtworks(artworks: Artwork[], wallById: Map<string, Wall>): void {\n const next = new Set(artworks.map((a) => a.id));\n for (const [id, entry] of this.artworks) {\n if (!next.has(id)) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n this.artworks.delete(id);\n }\n }\n for (const artwork of artworks) {\n const wall = wallById.get(artwork.wall);\n if (!wall) continue;\n // Rebuild when either the artwork or the wall it hangs on changed.\n const hash = JSON.stringify({ a: artwork, w: wall });\n const existing = this.artworks.get(artwork.id);\n if (existing?.hash === hash) continue;\n if (existing) {\n this.root.remove(existing.object);\n disposeObject(existing.object);\n }\n const object = buildArtwork(artwork, wall);\n this.root.add(object);\n this.artworks.set(artwork.id, { object, hash });\n }\n }\n\n /** Look up the live three.js object for a wall or artwork `id`. */\n get(id: string): Object3D | undefined {\n return this.walls.get(id)?.object ?? this.artworks.get(id)?.object;\n }\n\n dispose(): void {\n for (const entry of [...this.walls.values(), ...this.artworks.values()]) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n this.walls.clear();\n this.artworks.clear();\n }\n}\n","import {\n AmbientLight,\n Clock,\n HemisphereLight,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n type Object3D,\n} from \"three\";\nimport type { Scene } from \"@scenography/core\";\n\nimport { FreeRoamControls } from \"./controls\";\nimport { SceneManager } from \"./SceneManager\";\n\nexport interface GalleryOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n antialias?: boolean;\n}\n\n/**\n * High-level API handed to behavior code. Deliberately NOT raw three.js: this is\n * the supported surface. Reach into `three` only as a documented escape hatch.\n */\nexport interface GalleryContext {\n /** Resolve the live object for a wall/artwork `id`. */\n entity(id: string): Object3D | undefined;\n /** Escape hatch to the underlying three.js objects for advanced use. */\n readonly three: {\n scene: ThreeScene;\n camera: PerspectiveCamera;\n renderer: WebGLRenderer;\n };\n}\n\nexport interface Gallery {\n /** Apply a new scene definition, reconciling only what changed (hot-swap). */\n setScene(scene: Scene): void;\n /** Start the render loop. */\n start(): void;\n /** Stop the render loop. */\n stop(): void;\n /** Tear everything down and release GPU resources. */\n dispose(): void;\n /** Subscribe to a named event; returns an unsubscribe function. */\n on(event: string, handler: (payload?: unknown) => void): () => void;\n /** Emit a named event to subscribers. */\n emit(event: string, payload?: unknown): void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n readonly ctx: GalleryContext;\n}\n\n/**\n * Create and run a gallery from a scene definition. Sets up the three.js\n * renderer, camera, lights and free-roam controls, materializes the scene, and\n * starts the render loop. `setScene` later reconciles changes in place.\n */\nexport function createGallery(scene: Scene, options: GalleryOptions): Gallery {\n const { canvas } = options;\n const width = canvas.clientWidth || window.innerWidth;\n const height = canvas.clientHeight || window.innerHeight;\n\n const renderer = new WebGLRenderer({ canvas, antialias: options.antialias ?? true });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(75, width / height, 0.1, 5000);\n const { position, yaw } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n threeScene.add(new AmbientLight(0xffffff, 1.4));\n threeScene.add(new HemisphereLight(0xffffff, 0x444444, 1));\n\n const manager = new SceneManager(threeScene);\n manager.reconcile(scene);\n\n const controls = new FreeRoamControls(camera, canvas, {\n bounds: scene.camera.bounds,\n position: camera.position.clone(),\n yaw,\n });\n controls.attach();\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const clock = new Clock();\n let rafId: number | null = null;\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n controls.update(clock.getDelta());\n renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n if (next.camera.bounds !== undefined) controls.bounds = next.camera.bounds;\n },\n start() {\n if (rafId === null) {\n clock.start();\n renderFrame();\n }\n },\n stop() {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n },\n dispose() {\n this.stop();\n controls.detach();\n manager.dispose();\n renderer.dispose();\n listeners.clear();\n },\n on(event, handler) {\n const set = listeners.get(event) ?? new Set();\n set.add(handler);\n listeners.set(event, set);\n return () => set.delete(handler);\n },\n emit(event, payload) {\n listeners.get(event)?.forEach((handler) => handler(payload));\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["EquirectangularReflectionMapping","Raycaster","Vector2","Group","Vector3","MeshBasicMaterial","Mesh","BoxGeometry","Quaternion","FLAG_KEYS","WebGLRenderer","PCFShadowMap","StereoCamera","Vector2","ThreeScene","PerspectiveCamera","Clock","WebGLRenderTarget","HalfFloatType","TextureLoader","RepeatWrapping","SRGBColorSpace","NoColorSpace","Vector2"],"sources":["../src/environment/EnvironmentController.ts","../src/interaction/InteractionController.ts","../src/input/InputBus.ts","../src/input/controlSettings.ts","../src/input/controlStorage.ts","../src/plugin/CapabilityRegistry.ts","../src/plugin/systemPlugin.ts","../src/plugin/roamControl.ts","../src/plugin/terrain.ts","../src/plugin/colliders.ts","../src/plugin/edges.ts","../src/plugin/eyeHeight.ts","../src/SceneManager.ts","../src/createGallery.ts","../src/builders/texture.ts"],"sourcesContent":["import { EquirectangularReflectionMapping, type Scene as ThreeScene, type Texture } from \"three\";\nimport type { Environment } from \"@scenography/scene\";\n\n/** Load an equirectangular HDRI, picking the loader by extension. */\nconst loadHDRI = (src: string, onLoad: (tex: Texture) => void): void => {\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, onLoad);\n });\n } else {\n void import(\"three/examples/jsm/loaders/RGBELoader.js\").then(({ RGBELoader }) => {\n new RGBELoader().load(src, onLoad);\n });\n }\n};\n\n/**\n * Owns the scene's HDRI sky / image-based lighting and keeps it in sync with the\n * `Environment` definition. Loads the equirectangular map on demand (its loader\n * ships in its own chunk) and assigns it to `scene.background` and/or\n * `scene.environment`. Reconciles by a JSON hash so hot edits only reload when\n * the definition actually changes.\n */\nexport class EnvironmentController {\n private readonly scene: ThreeScene;\n private hash: string | null = null;\n private texture: Texture | null = null;\n\n constructor(scene: ThreeScene) {\n this.scene = scene;\n }\n\n reconcile(env: Environment | undefined): void {\n const hash = env ? JSON.stringify(env) : null;\n if (hash === this.hash) return;\n this.hash = hash;\n this.clear();\n if (!env) return;\n\n loadHDRI(env.src, (tex) => {\n // A newer reconcile superseded this load before it finished.\n if (this.hash !== hash) {\n tex.dispose();\n return;\n }\n tex.mapping = EquirectangularReflectionMapping;\n this.texture = tex;\n if (env.background ?? true) {\n this.scene.background = tex;\n this.scene.backgroundIntensity = env.intensity ?? 1;\n }\n if (env.lighting ?? true) this.scene.environment = tex;\n });\n }\n\n /** Set the visible sky background brightness (0 = black). Live, for behavior. */\n setBackgroundIntensity(value: number): void {\n this.scene.backgroundIntensity = value;\n }\n\n private clear(): void {\n this.scene.background = null;\n this.scene.environment = null;\n this.texture?.dispose();\n this.texture = null;\n }\n\n dispose(): void {\n this.clear();\n this.hash = null;\n }\n}\n","import {\n BoxGeometry,\n Group,\n Mesh,\n MeshBasicMaterial,\n Quaternion,\n Raycaster,\n Vector2,\n Vector3,\n type Object3D,\n type PerspectiveCamera,\n type Scene as ThreeScene,\n} from \"three\";\n\nconst HIGHLIGHT_COLOR = \"#ef4444\";\nconst HIGHLIGHT_THICKNESS = 1.5; // world units (cm)\nconst HIGHLIGHT_MARGIN = 1.5; // gap between the piece and the frame\nconst CLICK_SLOP = 4; // px of movement still counted as a click, not a drag\n\ninterface PickInfo {\n width: number;\n height: number;\n frontOffset: number;\n}\n\ninterface Pick {\n id: string;\n holder: Object3D;\n info: PickInfo;\n}\n\n/**\n * Pointer picking for artworks: hovering a piece's click target (its label, or\n * the piece itself when it has none) draws a pulsing red frame and shows a\n * pointer cursor; clicking it emits `artwork:select { id }`. Targets are marked\n * in the scene graph (`userData.pickTarget`/`pickInfo`) by the artwork builder,\n * so this just raycasts and walks up to find them. An optional per-id radius\n * gates interaction to when the camera is reasonably close.\n */\nexport class InteractionController {\n private readonly raycaster = new Raycaster();\n private readonly pointer = new Vector2();\n private over = false;\n private downPos: { x: number; y: number } | null = null;\n private radii = new Map<string, number>();\n private hovered: string | null = null;\n private time = 0;\n\n private readonly highlight = new Group();\n private highlightMat: MeshBasicMaterial | null = null;\n private readonly handlers: Array<[EventTarget, string, EventListener]> = [];\n\n constructor(\n private readonly camera: PerspectiveCamera,\n private readonly canvas: HTMLCanvasElement,\n private readonly scene: ThreeScene,\n private readonly emit: (event: string, payload?: unknown) => void,\n private readonly canInteract: () => boolean,\n ) {\n this.highlight.visible = false;\n this.scene.add(this.highlight);\n }\n\n /** Per-artwork max interaction distance (XZ). Absent id = no distance gate. */\n setRadii(radii: Map<string, number>): void {\n this.radii = radii;\n }\n\n attach(): void {\n const bind = (type: string, listener: EventListener): void => {\n this.canvas.addEventListener(type, listener);\n this.handlers.push([this.canvas, type, listener]);\n };\n bind(\"pointermove\", (e) => this.onMove(e as PointerEvent));\n bind(\"pointerdown\", (e) => this.onDown(e as PointerEvent));\n bind(\"pointerup\", (e) => this.onUp(e as PointerEvent));\n bind(\"pointerleave\", () => {\n this.over = false;\n });\n }\n\n detach(): void {\n for (const [target, type, listener] of this.handlers) target.removeEventListener(type, listener);\n this.handlers.length = 0;\n this.scene.remove(this.highlight);\n this.clearHighlight();\n }\n\n private setPointer(e: PointerEvent): void {\n const rect = this.canvas.getBoundingClientRect();\n this.pointer.set(\n ((e.clientX - rect.left) / rect.width) * 2 - 1,\n -((e.clientY - rect.top) / rect.height) * 2 + 1,\n );\n this.over = true;\n }\n\n private onMove(e: PointerEvent): void {\n this.setPointer(e);\n }\n\n private onDown(e: PointerEvent): void {\n this.downPos = { x: e.clientX, y: e.clientY };\n }\n\n private onUp(e: PointerEvent): void {\n const down = this.downPos;\n this.downPos = null;\n if (!down || !this.canInteract()) return;\n if (Math.hypot(e.clientX - down.x, e.clientY - down.y) > CLICK_SLOP) return; // a drag, not a click\n this.setPointer(e);\n const pick = this.raycast();\n if (pick) this.emit(\"artwork:select\", { id: pick.id });\n }\n\n /** Resolve the nearest pickable artwork under the pointer, honoring the gate. */\n private raycast(): Pick | null {\n this.raycaster.setFromCamera(this.pointer, this.camera);\n const hits = this.raycaster.intersectObjects(this.scene.children, true);\n for (const hit of hits) {\n let node: Object3D | null = hit.object;\n while (node && node.userData.pickTarget === undefined) node = node.parent;\n if (!node) continue;\n const id = node.userData.pickTarget as string;\n const radius = this.radii.get(id);\n if (radius !== undefined) {\n const p = node.getWorldPosition(new Vector3());\n if (Math.hypot(this.camera.position.x - p.x, this.camera.position.z - p.z) > radius) continue;\n }\n return { id, holder: node, info: node.userData.pickInfo as PickInfo };\n }\n return null;\n }\n\n /** Recompute hover state and animate the highlight. Call once per frame. */\n update(deltaMs: number): void {\n this.time += deltaMs;\n const pick = this.over && this.canInteract() ? this.raycast() : null;\n\n if ((pick?.id ?? null) !== this.hovered) {\n this.hovered = pick?.id ?? null;\n this.canvas.style.cursor = pick ? \"pointer\" : \"\";\n if (pick) this.buildHighlight(pick);\n else this.highlight.visible = false;\n }\n\n if (this.highlight.visible && this.highlightMat) {\n this.highlightMat.opacity = 0.45 + 0.45 * (0.5 + 0.5 * Math.sin(this.time / 320));\n }\n }\n\n private clearHighlight(): void {\n for (const child of [...this.highlight.children]) {\n this.highlight.remove(child);\n (child as Mesh).geometry?.dispose();\n }\n this.highlightMat?.dispose();\n this.highlightMat = null;\n }\n\n /** Position a fresh pulsing red frame over the picked target's front face. */\n private buildHighlight(pick: Pick): void {\n this.clearHighlight();\n const mat = new MeshBasicMaterial({ color: HIGHLIGHT_COLOR, transparent: true, depthTest: false });\n this.highlightMat = mat;\n\n const w = pick.info.width + 2 * HIGHLIGHT_MARGIN;\n const h = pick.info.height + 2 * HIGHLIGHT_MARGIN;\n const t = HIGHLIGHT_THICKNESS;\n // Four bars forming a border (horizontals span the corners; verticals fill in).\n const bars: [number, number, number, number][] = [\n [w + 2 * t, t, 0, h / 2 + t / 2],\n [w + 2 * t, t, 0, -(h / 2 + t / 2)],\n [t, h, -(w / 2 + t / 2), 0],\n [t, h, w / 2 + t / 2, 0],\n ];\n for (const [bw, bh, x, y] of bars) {\n const bar = new Mesh(new BoxGeometry(bw, bh, 0.5), mat);\n bar.position.set(x, y, 0);\n bar.renderOrder = 999;\n this.highlight.add(bar);\n }\n\n const pos = pick.holder.getWorldPosition(new Vector3());\n const quat = pick.holder.getWorldQuaternion(new Quaternion());\n const normal = new Vector3(0, 0, 1).applyQuaternion(quat);\n pos.addScaledVector(normal, pick.info.frontOffset);\n this.highlight.position.copy(pos);\n this.highlight.quaternion.copy(quat);\n this.highlight.visible = true;\n }\n}\n","/**\n * The shared analog-input channel: the current movement and look intent, each\n * axis in `-1..1` and held until a producer changes it. Producers (on-screen\n * joysticks in the overlay, a gamepad…) write to it; whatever camera system is\n * active reads it and interprets it its own way — free-roam walks with it, a\n * fixed camera might only use the look axes to glance around. This is what makes\n * the joysticks reusable across camera types. Pure state — no three.js.\n */\nexport class InputBus {\n /** Forward (`+1`) / backward (`-1`). */\n moveForward = 0;\n /** Strafe right (`+1`) / left (`-1`). */\n moveStrafe = 0;\n /** Look right (`+1`) / left (`-1`). */\n lookX = 0;\n /** Look up (`+1`) / down (`-1`). */\n lookY = 0;\n\n /** Set the movement intent (forward, strafe). */\n setMove(forward: number, strafe = 0): void {\n this.moveForward = forward;\n this.moveStrafe = strafe;\n }\n\n /** Set the look intent (turn, pitch). */\n setLook(x: number, y = 0): void {\n this.lookX = x;\n this.lookY = y;\n }\n}\n","/**\n * The visitor's camera-control preferences: a set of orthogonal capability flags\n * plus a derived, friendly \"mode\". Pure state — no three.js, no DOM. The engine\n * wires the flags to effects (gyroscope head-look, stereo split); consumers read\n * them (the analog look-invert axes); the overlay drives it from a mode picker\n * (the `ScanEye` button) and the settings panel. Persistence and device\n * feature-detection live in those callers, not here, so this stays trivially\n * unit-testable.\n */\n\n/**\n * Orthogonal control capabilities. `gyro` / `splitScreen` / `joysticks` are what a\n * mode fixes; the two `invertLook*` axes are independent look-feel preferences\n * (some people feel the right stick inverted), kept separate from the mode.\n */\nexport interface ControlFlags {\n /** Orient the camera from the device gyroscope (head-look). */\n gyro: boolean;\n /** Split the view into a stereo left/right eye pair (phone-in-a-headset). */\n splitScreen: boolean;\n /** Show the on-screen virtual joysticks. */\n joysticks: boolean;\n /** Invert the horizontal (turn) axis of analog look. */\n invertLookX: boolean;\n /** Invert the vertical (pitch) axis of analog look. */\n invertLookY: boolean;\n}\n\n/** A named preset over the mode-defining flags; `custom` when none matches. */\nexport type ControlMode = \"touch\" | \"vr\" | \"cardboard\" | \"custom\";\n\n/** The three flags a mode fixes (the look-invert axes stay a separate preference). */\ntype ModeFlags = Pick<ControlFlags, \"gyro\" | \"splitScreen\" | \"joysticks\">;\n\n/**\n * Mode presets. Touch = two joysticks, no gyro. VR = the gyro aims and a single\n * move joystick walks you, no split. Cardboard = gyro aims, stereo split, gaze\n * select (no joysticks). The move-only single stick in VR is derived downstream\n * from `gyro && !splitScreen`, so it needs no flag of its own.\n */\nexport const MODE_PRESETS: Record<Exclude<ControlMode, \"custom\">, ModeFlags> = {\n touch: { gyro: false, splitScreen: false, joysticks: true },\n vr: { gyro: true, splitScreen: false, joysticks: true },\n cardboard: { gyro: true, splitScreen: true, joysticks: false },\n};\n\n/** Default flags: plain touch, no look inversion. */\nexport const DEFAULT_CONTROL_FLAGS: ControlFlags = {\n ...MODE_PRESETS.touch,\n invertLookX: false,\n invertLookY: false,\n};\n\nconst FLAG_KEYS: readonly (keyof ControlFlags)[] = [\n \"gyro\",\n \"splitScreen\",\n \"joysticks\",\n \"invertLookX\",\n \"invertLookY\",\n];\n\nconst equalFlags = (a: ControlFlags, b: ControlFlags): boolean =>\n FLAG_KEYS.every((key) => a[key] === b[key]);\n\n/** The mode a flag set represents, or `custom` when no preset matches. */\nexport function modeOf(flags: ControlFlags): ControlMode {\n for (const mode of [\"touch\", \"vr\", \"cardboard\"] as const) {\n const preset = MODE_PRESETS[mode];\n if (\n flags.gyro === preset.gyro &&\n flags.splitScreen === preset.splitScreen &&\n flags.joysticks === preset.joysticks\n )\n return mode;\n }\n return \"custom\";\n}\n\n/** Apply a mode's preset onto `flags`, leaving the look-invert axes untouched. */\nexport function applyMode(\n flags: ControlFlags,\n mode: Exclude<ControlMode, \"custom\">,\n): ControlFlags {\n return { ...flags, ...MODE_PRESETS[mode] };\n}\n\n/**\n * Live control-preferences state: holds the flags, derives the mode, and notifies\n * subscribers on every real change. Callers apply the effects — the engine wires\n * gyro and split, the overlay redraws — so this object is just the shared source\n * of truth they all read and write.\n */\nexport class ControlSettings {\n private flags: ControlFlags;\n private readonly subscribers = new Set<(flags: ControlFlags) => void>();\n\n constructor(initial: Partial<ControlFlags> = {}) {\n this.flags = { ...DEFAULT_CONTROL_FLAGS, ...initial };\n }\n\n /** The current flags (a copy — mutate through `setMode` / `setFlag`). */\n get(): ControlFlags {\n return { ...this.flags };\n }\n\n /** The mode the current flags represent (`custom` when hand-tuned). */\n getMode(): ControlMode {\n return modeOf(this.flags);\n }\n\n /** Switch to a mode's preset (keeps the look-invert axes). No-op if unchanged. */\n setMode(mode: Exclude<ControlMode, \"custom\">): void {\n this.replace(applyMode(this.flags, mode));\n }\n\n /** Set one flag. Flipping a mode-defining flag can drop you into `custom`. */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void {\n this.replace({ ...this.flags, [key]: value });\n }\n\n /** Subscribe to changes; returns an unsubscribe. Fires on every real change. */\n subscribe(fn: (flags: ControlFlags) => void): () => void {\n this.subscribers.add(fn);\n return () => this.subscribers.delete(fn);\n }\n\n private replace(next: ControlFlags): void {\n if (equalFlags(this.flags, next)) return;\n this.flags = next;\n const snapshot = this.get();\n for (const fn of this.subscribers) fn(snapshot);\n }\n}\n","import { DEFAULT_CONTROL_FLAGS, type ControlFlags } from \"./controlSettings\";\n\n/**\n * localStorage persistence for the control flags, kept out of the pure\n * `ControlSettings` so that stays DOM-free and unit-testable. Both functions are\n * best-effort and guarded: no `localStorage` (SSR, privacy mode) or malformed\n * data just falls back to defaults, never throws.\n */\nconst STORAGE_KEY = \"scenography:controls\";\n\nconst FLAG_KEYS = Object.keys(DEFAULT_CONTROL_FLAGS) as (keyof ControlFlags)[];\n\n/** Read persisted control flags; `null` when absent, unavailable or malformed. */\nexport function loadControlFlags(): Partial<ControlFlags> | null {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n const flags: Partial<ControlFlags> = {};\n // Copy only known boolean keys, so stale/garbage fields can't leak in.\n for (const key of FLAG_KEYS) {\n if (typeof parsed[key] === \"boolean\") flags[key] = parsed[key] as boolean;\n }\n return flags;\n } catch {\n return null;\n }\n}\n\n/** Persist control flags (best-effort; ignores a full/blocked store). */\nexport function saveControlFlags(flags: ControlFlags): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(flags));\n } catch {\n /* storage unavailable or full — persistence is best-effort */\n }\n}\n","/**\n * Shared runtime contracts between plugins, keyed by name. A plugin (or the\n * kernel) *provides* a capability — e.g. the floor provides `terrain` — and\n * other plugins *consume* it if present, without depending on the provider's\n * package. Each name keeps a monotonic version counter so consumers can hash\n * their dependencies cheaply and rebuild when a provider changes.\n */\nexport class CapabilityRegistry {\n private readonly values = new Map<string, unknown>();\n private readonly versions = new Map<string, number>();\n /** Last change-token seen per name, to suppress redundant version bumps. */\n private readonly tokens = new Map<string, string>();\n\n /**\n * Publish (or replace) the capability under `name`. Pass a `token` (a cheap\n * hash of the inputs) to dedupe: re-providing with an unchanged token is a\n * no-op, so consumers that depend on this capability don't rebuild when the\n * provider recomputes an identical value.\n */\n provide<T>(name: string, value: T, token?: string): void {\n if (token !== undefined && this.values.has(name) && this.tokens.get(name) === token) return;\n this.values.set(name, value);\n if (token !== undefined) this.tokens.set(name, token);\n else this.tokens.delete(name);\n this.bump(name);\n }\n\n /** Remove the capability; consumers see `undefined` and a version bump. */\n revoke(name: string): void {\n if (this.values.delete(name)) {\n this.tokens.delete(name);\n this.bump(name);\n }\n }\n\n get<T>(name: string): T | undefined {\n return this.values.get(name) as T | undefined;\n }\n\n /** Monotonic change counter for `name` (`0` if never provided). */\n version(name: string): number {\n return this.versions.get(name) ?? 0;\n }\n\n private bump(name: string): void {\n this.versions.set(name, (this.versions.get(name) ?? 0) + 1);\n }\n}\n","import type {\n Object3D,\n PerspectiveCamera,\n Scene as ThreeScene,\n WebGLRenderer,\n} from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport type { CapabilityRegistry } from \"./CapabilityRegistry\";\nimport type { ControlFlags, InputBus } from \"../input\";\n\n/**\n * What the kernel hands a system on setup. A *system* plugin — unlike an element\n * plugin — produces no scene object; it drives shared runtime state (the camera,\n * input…) each frame. The camera is a kernel primitive (the renderer needs one),\n * so systems receive it and drive it, publishing/consuming capabilities to\n * cooperate (e.g. the camera plugin publishes `roamControl`).\n */\nexport interface SystemContext {\n /** The current scene (its data keys, start pose, boundary…); also `setScene` on hot-swap. */\n scene: Scene;\n /** The render camera the system may drive. */\n camera: PerspectiveCamera;\n /** The canvas, for pointer/keyboard listeners. */\n canvas: HTMLElement;\n /** The three.js scene graph, for systems that manage their own objects (sky…). */\n scene3d: ThreeScene;\n /** The renderer, for systems that need it (e.g. sky IBL generation). */\n renderer: WebGLRenderer;\n /** Shared analog input (joysticks feed it); systems interpret it. */\n input: InputBus;\n /** The visitor's current control flags (e.g. look-invert axes), read live. */\n controlFlags: () => ControlFlags;\n /** Capability registry for cross-plugin contracts (terrain, colliders…). */\n capabilities: CapabilityRegistry;\n /** Emit a gallery event. */\n emit: (event: string, payload?: unknown) => void;\n /** Resolve a live scene object by id (e.g. for a framing system). */\n resolve: (id: string) => Object3D | undefined;\n}\n\n/** A running system: its per-frame update, hot-swap and teardown hooks. */\nexport interface SystemInstance<Api = unknown> {\n /** Advance the system by the frame delta (ms). */\n update?(deltaMs: number): void;\n /** React to a hot-swapped scene definition. */\n setScene?(scene: Scene): void;\n /** Tear down (detach listeners, revoke capabilities). */\n dispose?(): void;\n /**\n * Public API the system exposes to userland/overlay. The kernel maps known\n * system names onto `ctx` (e.g. a `focus` system's api → `ctx.camera`).\n */\n api?: Api;\n}\n\n/**\n * A system plugin: sets up once against the kernel handles and returns its\n * running instance. Registered alongside element plugins in `plugins`; the\n * kernel tells them apart by shape (`setup` vs `build`).\n */\nexport interface SystemPlugin<Api = unknown> {\n name: string;\n setup(ctx: SystemContext): SystemInstance<Api>;\n}\n\n/** Narrow a mixed plugin to a system plugin (has `setup`, not `build`). */\nexport const isSystemPlugin = (plugin: unknown): plugin is SystemPlugin =>\n typeof plugin === \"object\" && plugin !== null && \"setup\" in plugin;\n","import type { Vector3 } from \"three\";\n\n/** Capability name under which a roaming camera system exposes pose control. */\nexport const ROAM_CONTROL = \"roamControl\";\n\n/**\n * Handle a camera plugin publishes so other camera systems can cooperate with\n * it — chiefly a framing/tour system that suspends free-roam while it animates\n * the camera, then reads/writes the pose to hand control back cleanly.\n */\nexport interface RoamControl {\n /** Enable/disable free-roam input (disabled while another system owns the camera). */\n setEnabled(enabled: boolean): void;\n /** Current pose: world position plus yaw (`phi`) and pitch (`theta`) in radians. */\n getPose(): { position: Vector3; phi: number; theta: number };\n /** Force the pose so free-roam resumes from it. */\n setPose(position: Vector3, phi: number, theta: number): void;\n /** Current eye height above the ground (world units). */\n getEyeHeight(): number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which the floor publishes its terrain. */\nexport const TERRAIN = \"terrain\";\n\n/**\n * The geometry a terrain consumer relies on — the fields needed to sample the\n * same ground a floor was displaced by (e.g. the water shader). A floor\n * definition satisfies this structurally; the kernel stays floor-agnostic.\n */\nexport interface TerrainDefinition {\n /** Extent on the ground plane, `[width (x), depth (z)]`. */\n size: Vec2;\n /** Center on the ground plane `[x, z]`. Defaults to `[0, 0]`. */\n center?: Vec2;\n /** Base vertical position (world Y). Defaults to `0`. */\n y?: number;\n /** Grayscale heightmap URL that displaces the ground. */\n heightmap?: string;\n /** Max displacement (world units) at white in the heightmap. */\n heightScale?: number;\n}\n\n/**\n * The walkable ground, published by the floor: its definition (for shaders that\n * sample the same heightmap), the live height function (heightmap-aware once the\n * image loads) and a promise that resolves once that sampling is in effect.\n */\nexport interface TerrainCapability {\n definition: TerrainDefinition;\n heightAt: (x: number, z: number) => number;\n /** Resolves once `heightAt` reflects the heightmap (immediately when there is none). */\n ready: Promise<void>;\n}\n","/** Capability name under which walkable-space obstacles are published. */\nexport const COLLIDERS = \"colliders\";\n\n/**\n * A collision obstacle as an oriented box on the XZ plane. The camera controls\n * consume these to keep the visitor out of walls (and any other plugin that\n * wants to block movement can provide more of them). Purely a movement contract\n * — no geometry, no three.js.\n */\nexport interface Collider {\n /** Box center `[x, z]`. */\n cx: number;\n cz: number;\n /** Unit direction along the box's length. */\n dx: number;\n dz: number;\n /** Half the box length. */\n halfLength: number;\n /** Half the box thickness. */\n halfThickness: number;\n}\n","import type { Vec2 } from \"@scenography/scene\";\n\n/** Capability name under which named wall edges are published for hanging. */\nexport const EDGES = \"edges\";\n\n/** A named anchor an artwork can hang on: a wall segment resolved to geometry. */\nexport interface EdgeAnchor {\n a: Vec2;\n b: Vec2;\n height: number;\n thickness: number;\n}\n\n/** The `name -> anchor` index a provider (e.g. the wall plugin) publishes. */\nexport type EdgeIndex = ReadonlyMap<string, EdgeAnchor>;\n","/**\n * Capability name under which the visitor's eye height (world units) is\n * published — the default vertical placement for wall-hung artworks. The kernel\n * provides it from the camera start; a camera plugin will own it later.\n */\nexport const EYE_HEIGHT = \"eyeHeight\";\n","import type { Object3D, PerspectiveCamera, Scene as ThreeScene, WebGLRenderer } from \"three\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport {\n CapabilityRegistry,\n EYE_HEIGHT,\n type BuiltElement,\n type EnginePlugin,\n} from \"./plugin\";\n\n/** A mounted plugin element: what the plugin built plus its content hash. */\ntype MountedElement = BuiltElement & { hash: string };\n\nconst disposeObject = (object: Object3D): void => {\n object.traverse((child) => {\n // Release any HTML video element backing a VideoTexture.\n const video = child.userData?.video as HTMLVideoElement | undefined;\n if (video) {\n video.pause();\n video.removeAttribute(\"src\");\n video.load();\n }\n const mesh = child as { geometry?: { dispose?: () => void }; material?: unknown };\n mesh.geometry?.dispose?.();\n const dispose = (m: unknown): void => {\n const material = m as { dispose?: () => void } & Record<string, unknown>;\n for (const slot of [\"map\", \"normalMap\", \"roughnessMap\", \"aoMap\", \"metalnessMap\"]) {\n (material?.[slot] as { dispose?: () => void } | undefined)?.dispose?.();\n }\n material?.dispose?.();\n };\n const material = mesh.material;\n if (Array.isArray(material)) material.forEach(dispose);\n else dispose(material);\n });\n};\n\n/**\n * Owns the live three.js scene graph and keeps it in sync with a `Scene`\n * definition. `reconcile` diffs the incoming definition against what is already\n * mounted — by `id` — and only adds/removes/rebuilds what changed. This is what\n * makes hot scene edits cheap: add a wing or drop one while moving and only the\n * touched objects are rebuilt.\n */\nexport class SceneManager {\n private readonly root: ThreeScene;\n private readonly plugins: EnginePlugin[];\n private readonly capabilities: CapabilityRegistry;\n /** Mounted plugin elements, per kind, keyed by element id (\"\" for singletons). */\n private readonly pluginElements = new Map<string, Map<string, MountedElement>>();\n\n constructor(\n root: ThreeScene,\n plugins: EnginePlugin[] = [],\n capabilities: CapabilityRegistry = new CapabilityRegistry(),\n ) {\n this.root = root;\n this.plugins = plugins;\n this.capabilities = capabilities;\n }\n\n reconcile(scene: Scene): void {\n // Eye height (default hang height for wall art) — a camera concern the\n // kernel owns for now; a camera plugin will publish it later.\n const eye = scene.camera.start.position[1];\n this.capabilities.provide(EYE_HEIGHT, eye, String(eye));\n // Plugins go last so kernel-provided capabilities (terrain, eyeHeight) are fresh.\n this.reconcilePlugins(scene);\n }\n\n /**\n * Reconcile every registered plugin's elements by content: an element\n * rebuilds when its definition changes or when a capability it depends on\n * was re-provided (the capability versions are part of the hash).\n */\n private reconcilePlugins(scene: Scene): void {\n for (const plugin of this.plugins) {\n const { kind, cardinality } = plugin.data;\n let mounted = this.pluginElements.get(kind);\n if (!mounted) {\n mounted = new Map();\n this.pluginElements.set(kind, mounted);\n }\n\n const raw = (scene as unknown as Record<string, unknown>)[kind];\n const items: Array<[string, unknown]> =\n cardinality === \"many\"\n ? Array.isArray(raw)\n ? raw.map((item, i) => [String((item as { id?: string }).id ?? i), item])\n : []\n : raw !== undefined\n ? [[\"\", raw]]\n : [];\n const deps = (plugin.dependsOn ?? []).map((name) => this.capabilities.version(name));\n\n const seen = new Set<string>();\n for (const [key, value] of items) {\n seen.add(key);\n const hash = JSON.stringify({ value, deps });\n const existing = mounted.get(key);\n if (existing?.hash === hash) continue;\n if (existing) this.removePluginElement(mounted, key, existing);\n const built = plugin.build(value as never, { capabilities: this.capabilities });\n if (built.object) this.root.add(built.object);\n mounted.set(key, { ...built, hash });\n }\n for (const [key, entry] of mounted)\n if (!seen.has(key)) this.removePluginElement(mounted, key, entry);\n\n // Publish this plugin's aggregate capabilities now that its elements are\n // in sync, so a later plugin that consumes them builds against fresh data.\n plugin.provide?.(\n items.map(([, value]) => value),\n { capabilities: this.capabilities },\n );\n }\n }\n\n private removePluginElement(\n mounted: Map<string, MountedElement>,\n key: string,\n entry: MountedElement,\n ): void {\n if (entry.object) {\n this.root.remove(entry.object);\n disposeObject(entry.object);\n }\n entry.dispose?.();\n mounted.delete(key);\n }\n\n\n /** Advance time-based plugin elements (e.g. water ripple). Call once per frame. */\n updateElements(deltaMs: number): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.update?.(deltaMs);\n }\n\n /** Run every element's auxiliary pre-render pass (e.g. water reflection). */\n beforeRender(renderer: WebGLRenderer, scene: ThreeScene, camera: PerspectiveCamera): void {\n for (const mounted of this.pluginElements.values())\n for (const entry of mounted.values()) entry.beforeRender?.(renderer, scene, camera);\n }\n\n\n /** Look up the live three.js object for any plugin element `id`. */\n get(id: string): Object3D | undefined {\n for (const mounted of this.pluginElements.values()) {\n const object = mounted.get(id)?.object;\n if (object) return object;\n }\n return undefined;\n }\n\n /** Live objects of one plugin kind, keyed by `id` — for that plugin's controller. */\n elementsOf(kind: string): ReadonlyMap<string, Object3D> {\n const out = new Map<string, Object3D>();\n const mounted = this.pluginElements.get(kind);\n if (mounted) for (const [id, entry] of mounted) if (entry.object) out.set(id, entry.object);\n return out;\n }\n\n dispose(): void {\n for (const [kind, mounted] of this.pluginElements) {\n for (const [key, entry] of mounted) this.removePluginElement(mounted, key, entry);\n this.pluginElements.delete(kind);\n }\n }\n}\n","import {\n Clock,\n HalfFloatType,\n PCFShadowMap,\n PerspectiveCamera,\n Scene as ThreeScene,\n StereoCamera,\n Vector2,\n WebGLRenderer,\n WebGLRenderTarget,\n type Object3D,\n} from \"three\";\nimport type { EffectComposer } from \"three/examples/jsm/postprocessing/EffectComposer.js\";\nimport type { Pass } from \"three/examples/jsm/postprocessing/Pass.js\";\nimport type { Scene } from \"@scenography/scene\";\n\nimport { EnvironmentController } from \"./environment\";\nimport type { CameraApi } from \"./camera\";\nimport type { LightsApi, MediaController, SkyApi, VideoApi } from \"./api\";\nimport { InteractionController } from \"./interaction\";\nimport {\n ControlSettings,\n InputBus,\n MODE_PRESETS,\n loadControlFlags,\n saveControlFlags,\n type ControlFlags,\n type ControlMode,\n} from \"./input\";\nimport {\n CapabilityRegistry,\n isSystemPlugin,\n type ElementController,\n type EnginePlugin,\n type SystemInstance,\n type SystemPlugin,\n} from \"./plugin\";\nimport { SceneManager } from \"./SceneManager\";\n\n/** Maps a plugin kind/name onto the `ctx` slot its api fills. */\nconst CTX_SLOT = {\n camera: \"focus\",\n lights: \"lights\",\n video: \"videos\",\n sky: \"sky\",\n media: \"audio\",\n} as const;\n/** The system the cardboard render toggle drives (gyroscope head-look). */\nconst CARDBOARD_SYSTEM = \"cardboard\";\n\n/** MSAA sample count for antialiasing. `0` disables it. */\nexport type Antialias = 0 | 2 | 4 | 8;\n\n/** Toggleable rendering features. All can be flipped live via `setRender`. */\nexport interface RenderOptions {\n /**\n * Antialiasing as MSAA sample count (`0` off, `2`/`4`/`8`). Applied to the\n * post-process render target so it survives SSAO/bloom and toggles live,\n * unlike the canvas's native MSAA. Defaults to `4`.\n */\n antialias?: Antialias;\n /** Screen-space ambient occlusion as a post-process. Defaults to `false`. */\n ssao?: boolean;\n /**\n * Bloom strength: a soft glow bleeding out of the brightest areas. `0` off;\n * gentle around `0.3`, strong around `1`. Defaults to `0`.\n */\n bloom?: number;\n /** Real-time shadow mapping (PCF). Defaults to `true`. */\n shadows?: boolean;\n /**\n * Cardboard / stereoscopic mode: split the view into a left/right eye pair for\n * a phone-in-a-headset viewer. Skips post-processing while active. Defaults to\n * `false`.\n */\n cardboard?: boolean;\n /**\n * Interocular distance for cardboard mode, in world units. The physical ≈6.4 cm\n * is too much parallax to fuse comfortably through cardboard lenses, so this\n * defaults to `3`; tune it live for the viewer. `0` flattens to mono.\n */\n eyeSeparation?: number;\n}\n\nexport interface GalleryOptions extends RenderOptions {\n /** Canvas the gallery renders into. */\n canvas: HTMLCanvasElement;\n /** Background color. Defaults to a soft gray. */\n background?: string;\n /**\n * Runtime plugins: element plugins materialize a scene key; system plugins\n * (e.g. a camera) drive shared runtime state. Mixed freely — the kernel tells\n * them apart by shape.\n */\n plugins?: (EnginePlugin | SystemPlugin)[];\n}\n\nconst DEFAULT_EYE_SEPARATION = 3;\n\n/**\n * Drive the free-roam camera from outside the engine — e.g. on-screen touch\n * controls in the overlay. Inputs are analog (`-1..1`) and held until changed.\n */\nexport interface ControlsApi {\n /** Walk forward (`+1`) / back (`-1`); optional strafe right (`+1`) / left (`-1`). */\n setMoveInput(forward: number, strafe?: number): void;\n /** Turn right (`+1`) / left (`-1`); optional look up (`+1`) / down (`-1`). */\n setLookInput(x: number, y?: number): void;\n}\n\n/**\n * The visitor's camera-control preferences (view mode + capability flags). The\n * engine applies the effects that touch rendering/input — gyroscope head-look and\n * the stereo split — as the flags change; the overlay drives it from the mode\n * picker and settings, and reads it to show the right joysticks. See\n * `ControlSettings`.\n */\nexport interface ViewControlsApi {\n /** Current capability flags (a copy). */\n get(): ControlFlags;\n /** The mode the flags represent (`custom` when hand-tuned in settings). */\n getMode(): ControlMode;\n /** Switch to a mode's preset (keeps the look-invert axes). */\n setMode(mode: Exclude<ControlMode, \"custom\">): void;\n /** Set one capability flag (may drop the mode into `custom`). */\n setFlag<K extends keyof ControlFlags>(key: K, value: ControlFlags[K]): void;\n /** Subscribe to changes; returns an unsubscribe. */\n subscribe(fn: (flags: ControlFlags) => void): () => void;\n}\n\n/** Control the HDRI sky / environment at runtime, for behavior code. */\nexport interface EnvironmentApi {\n /** Set the visible sky background brightness (`0` = black). */\n setBackgroundIntensity(value: number): void;\n}\n\n/**\n * High-level API handed to behavior code. Deliberately NOT raw three.js: this is\n * the supported surface. Reach into `three` only as a documented escape hatch.\n */\nexport interface GalleryContext {\n /** Resolve the live object for a wall/artwork/light `id`. */\n entity(id: string): Object3D | undefined;\n /** Control scene lights (dim, set intensity/color) by `id`. */\n readonly lights: LightsApi;\n /** Control background audio (play, pause, fade) by `id`. */\n readonly media: MediaController;\n /** Drive the camera externally (on-screen touch controls, etc.). */\n readonly controls: ControlsApi;\n /** The visitor's camera-control preferences (view mode + capability flags). */\n readonly viewControls: ViewControlsApi;\n /** Control the HDRI sky / environment at runtime. */\n readonly environment: EnvironmentApi;\n /** Drive the procedural sky's time of day. */\n readonly sky: SkyApi;\n /** Animate the camera to frame artworks (e.g. when an info panel opens/closes). */\n readonly camera: CameraApi;\n /** Control video screens (play, pause, show/hide) by `id`. */\n readonly video: VideoApi;\n /** Escape hatch to the underlying three.js objects for advanced use. */\n readonly three: {\n scene: ThreeScene;\n camera: PerspectiveCamera;\n renderer: WebGLRenderer;\n };\n}\n\nexport interface Gallery {\n /** Apply a new scene definition, reconciling only what changed (hot-swap). */\n setScene(scene: Scene): void;\n /** Start the render loop. */\n start(): void;\n /** Stop the render loop. */\n stop(): void;\n /** Tear everything down and release GPU resources. */\n dispose(): void;\n /** Subscribe to a named event; returns an unsubscribe function. */\n on(event: string, handler: (payload?: unknown) => void): () => void;\n /** Emit a named event to subscribers. */\n emit(event: string, payload?: unknown): void;\n /** Run a handler every frame with the frame delta in ms; returns unsubscribe. */\n onFrame(handler: (deltaMs: number) => void): () => void;\n /** Resize the renderer/camera to the given pixel size. */\n resize(width: number, height: number): void;\n /** Toggle rendering features (antialias, ssao, bloom, shadows) live. */\n setRender(options: RenderOptions): void;\n readonly ctx: GalleryContext;\n}\n\n/**\n * Create and run a gallery from a scene definition. Sets up the three.js\n * renderer, camera, lights and free-roam controls, materializes the scene, and\n * starts the render loop. `setScene` later reconciles changes in place.\n */\nexport function createGallery(scene: Scene, options: GalleryOptions): Gallery {\n const { canvas } = options;\n const width = canvas.clientWidth || window.innerWidth;\n const height = canvas.clientHeight || window.innerHeight;\n\n // Native MSAA is off: antialiasing is an SMAA post-pass so it survives SSAO\n // and toggles live. PCF shadow filtering set up once; shadows enabled below.\n const renderer = new WebGLRenderer({ canvas, antialias: false });\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n renderer.setSize(width, height, false);\n renderer.shadowMap.type = PCFShadowMap;\n\n // Live-toggleable render features.\n const render: Required<RenderOptions> = {\n antialias: options.antialias ?? 4,\n ssao: options.ssao ?? false,\n bloom: options.bloom ?? 0,\n shadows: options.shadows ?? true,\n cardboard: options.cardboard ?? false,\n eyeSeparation: options.eyeSeparation ?? DEFAULT_EYE_SEPARATION,\n };\n\n // Stereoscopic (cardboard) rendering. StereoCamera is core three (not the\n // WebGL-only StereoEffect addon); we render each eye with a scissored viewport,\n // which is portable to WebGPU. aspect 0.5 keeps each half undistorted.\n const stereo = new StereoCamera();\n stereo.aspect = 0.5;\n stereo.eyeSep = render.eyeSeparation;\n const stereoSize = new Vector2();\n\n const threeScene = new ThreeScene();\n\n const camera = new PerspectiveCamera(scene.camera.fov ?? 55, width / height, 0.1, 5000);\n const { position } = scene.camera.start;\n camera.position.set(position[0], position[1], position[2]);\n\n // Split element plugins (materialize a scene key) from system plugins (drive\n // shared state like the camera). Elements go to the manager; systems are set\n // up below, once elements have published their capabilities.\n const allPlugins = options.plugins ?? [];\n const systemPlugins = allPlugins.filter(isSystemPlugin);\n const elementPlugins = allPlugins.filter((p): p is EnginePlugin => !isSystemPlugin(p));\n\n const capabilities = new CapabilityRegistry();\n const manager = new SceneManager(threeScene, elementPlugins, capabilities);\n manager.reconcile(scene);\n\n const input = new InputBus();\n // The visitor's control preferences (view mode + flags). Created early so system\n // plugins can read the flags (e.g. fpc reads the look-invert axes); the effects\n // that need the head-look system / render are wired below, once they exist. A\n // persisted choice (localStorage) wins over the initial cardboard option so the\n // visitor's last mode is restored across reloads.\n const controlSettings = new ControlSettings(\n loadControlFlags() ?? (options.cardboard ? MODE_PRESETS.cardboard : {}),\n );\n const environment = new EnvironmentController(threeScene);\n environment.reconcile(scene.environment);\n\n const listeners = new Map<string, Set<(payload?: unknown) => void>>();\n const emit = (event: string, payload?: unknown): void => {\n listeners.get(event)?.forEach((handler) => handler(payload));\n };\n const frameHandlers = new Set<(deltaMs: number) => void>();\n\n // Plugins publish their public apis here, keyed by kind/name; the kernel maps\n // them onto `ctx` slots (see `CTX_SLOT`). Element-plugin controllers (lights,\n // video) and system apis (camera, sky, media) both land in this one map.\n const apis = new Map<string, unknown>();\n\n // Element-plugin controllers: an aggregate api over each plugin's live objects\n // (e.g. `ctx.lights`). They advance in the render loop and tear down on dispose.\n const controllers: ElementController[] = [];\n for (const plugin of elementPlugins) {\n const controller = plugin.controller?.(() => manager.elementsOf(plugin.data.kind));\n if (!controller) continue;\n if (controller.api !== undefined) apis.set(plugin.data.kind, controller.api);\n controllers.push(controller);\n }\n\n // System plugins (camera, sky, audio…): drive shared runtime state each frame\n // and may expose an api. They get the scene graph + renderer for subsystems\n // that manage their own objects (sky) or listen on the camera (audio).\n const systems: SystemInstance[] = systemPlugins.map((plugin) => {\n const instance = plugin.setup({\n scene,\n camera,\n canvas,\n scene3d: threeScene,\n renderer,\n input,\n controlFlags: () => controlSettings.get(),\n capabilities,\n emit,\n resolve: (id) => manager.get(id),\n });\n if (instance.api !== undefined) apis.set(plugin.name, instance.api);\n return instance;\n });\n\n // `ctx.camera` is filled by the framing system (\"focus\"). Without it, a no-op\n // keeps callers (the overlay) safe.\n const cameraApi: CameraApi = (apis.get(CTX_SLOT.camera) as CameraApi | undefined) ?? {\n focusArtwork: () => {},\n centerArtwork: () => {},\n release: () => {},\n reset: () => {},\n isAnimating: () => false,\n };\n\n // Cardboard render (stereo split) is a kernel feature; the gyroscope head-look\n // that rides with it is the `@scenography/plugin-cardboard` system. The render\n // toggle below drives its `setEnabled`.\n type HeadTracking = { setEnabled(enabled: boolean): void };\n const headTracking = apis.get(CARDBOARD_SYSTEM) as HeadTracking | undefined;\n // Apply the restored (or seeded) flags on load: enable gyro head-look and sync\n // the stereo render. (On iOS the gyro permission can't be granted outside a user\n // gesture, so a restored gyro mode only truly tracks once the visitor interacts —\n // enable() fails gracefully, nothing breaks.)\n const initialFlags = controlSettings.get();\n render.cardboard = initialFlags.splitScreen;\n if (initialFlags.gyro) headTracking?.setEnabled(true);\n\n // gyro and splitScreen are *orthogonal* here — unlike the legacy `cardboard`\n // render toggle that fused them — so a \"VR\" mode can aim with the gyro without\n // the stereo split. Apply those two effects (head-look system + stereo render)\n // as the flags change; the others are read by the overlay and input consumers.\n // render.cardboard / the initial setEnabled above already reflect the seed, so\n // the subscriber only handles subsequent changes.\n let prevFlags = controlSettings.get();\n controlSettings.subscribe((flags) => {\n if (flags.gyro !== prevFlags.gyro) headTracking?.setEnabled(flags.gyro);\n if (flags.splitScreen !== prevFlags.splitScreen) render.cardboard = flags.splitScreen;\n prevFlags = flags;\n saveControlFlags(flags); // remember the choice across reloads\n emit(\"controls:change\", { flags, mode: controlSettings.getMode() });\n });\n\n // No-op fallbacks so the overlay/behavior stay safe when a plugin is absent.\n const lightsApi = (apis.get(CTX_SLOT.lights) as LightsApi | undefined) ?? {\n set: () => {},\n dimAll: () => {},\n restore: () => {},\n };\n const videoApi = (apis.get(CTX_SLOT.video) as VideoApi | undefined) ?? {\n play: () => {},\n pause: () => {},\n setVisible: () => {},\n };\n const skyApi = (apis.get(CTX_SLOT.sky) as SkyApi | undefined) ?? {\n setTime: () => {},\n getTime: () => 12,\n setAzimuth: () => {},\n getAzimuth: () => 0,\n };\n const mediaApi = (apis.get(CTX_SLOT.media) as MediaController | undefined) ?? {\n play: () => {},\n pause: () => {},\n stop: () => {},\n setVolume: () => {},\n playing: () => false,\n setMuted: () => {},\n muted: () => false,\n };\n\n // Per-element interaction-distance gate, derived from each pickable element's\n // `infoRadius`. Read structurally: `infoRadius` is a convention a plugin (the\n // artwork plugin) can put on its elements, not a kernel field. A later\n // interaction pass can move this onto the built object's userData entirely.\n type Gated = { id: string; infoRadius?: number };\n const radiiOf = (s: Scene): Map<string, number> => {\n const gated = (s as { artworks?: Gated[] }).artworks ?? [];\n return new Map(\n gated.filter((a) => a.infoRadius !== undefined).map((a) => [a.id, a.infoRadius as number]),\n );\n };\n\n const interaction = new InteractionController(camera, canvas, threeScene, emit, () => !cameraApi.isAnimating());\n interaction.setRadii(radiiOf(scene));\n interaction.attach();\n const clock = new Clock();\n let rafId: number | null = null;\n\n // Post-processing (MSAA antialias + SSAO + bloom). The whole chain is imported\n // on demand so it only ships when a gallery uses it; until it resolves (or when\n // everything is off) we render straight to the canvas. Antialiasing is MSAA on\n // the composer's render target (`samples`), so changing the sample count\n // rebuilds the composer; SSAO/bloom just flip `.enabled`.\n // Bloom spread/threshold are fixed; its strength is the live `render.bloom`.\n const BLOOM = { radius: 0.4, threshold: 0.85 };\n type PostClasses = {\n EffectComposer: typeof import(\"three/examples/jsm/postprocessing/EffectComposer.js\").EffectComposer;\n RenderPass: typeof import(\"three/examples/jsm/postprocessing/RenderPass.js\").RenderPass;\n SSAOPass: typeof import(\"three/examples/jsm/postprocessing/SSAOPass.js\").SSAOPass;\n UnrealBloomPass: typeof import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\").UnrealBloomPass;\n OutputPass: typeof import(\"three/examples/jsm/postprocessing/OutputPass.js\").OutputPass;\n };\n let postClasses: PostClasses | null = null;\n let composer: EffectComposer | null = null;\n let ssaoPass: (Pass & { kernelRadius: number }) | null = null;\n let bloomPass: (Pass & { setSize(w: number, h: number): void; strength: number }) | null = null;\n let composerLoading = false;\n\n const pixelRatio = Math.min(window.devicePixelRatio, 2);\n const needsComposer = (): boolean => render.antialias > 0 || render.ssao || render.bloom > 0;\n\n // (Re)build the composer from the loaded classes at the current sample count.\n const buildComposer = (): void => {\n if (!postClasses) return;\n composer?.dispose();\n const { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass } = postClasses;\n // Multisampled target → MSAA that survives the post chain. `samples` is\n // preserved across resizes (setSize only changes width/height). HalfFloat\n // matches the composer's own default and gives bloom its HDR range.\n const target = new WebGLRenderTarget(1, 1, { samples: render.antialias, type: HalfFloatType });\n const c = new EffectComposer(renderer, target);\n c.setPixelRatio(pixelRatio);\n c.setSize(width, height);\n c.addPass(new RenderPass(threeScene, camera));\n const ssao = new SSAOPass(threeScene, camera, width, height);\n ssao.kernelRadius = 16;\n ssao.enabled = render.ssao;\n c.addPass(ssao);\n const bloom = new UnrealBloomPass(new Vector2(width, height), render.bloom, BLOOM.radius, BLOOM.threshold);\n bloom.enabled = render.bloom > 0;\n c.addPass(bloom);\n // Restores tone mapping / sRGB that the composer would otherwise skip.\n c.addPass(new OutputPass());\n composer = c;\n ssaoPass = ssao;\n bloomPass = bloom;\n };\n\n const ensureComposer = (): void => {\n if (!needsComposer()) return;\n if (postClasses) {\n if (!composer) buildComposer();\n return;\n }\n if (composerLoading) return;\n composerLoading = true;\n void Promise.all([\n import(\"three/examples/jsm/postprocessing/EffectComposer.js\"),\n import(\"three/examples/jsm/postprocessing/RenderPass.js\"),\n import(\"three/examples/jsm/postprocessing/SSAOPass.js\"),\n import(\"three/examples/jsm/postprocessing/UnrealBloomPass.js\"),\n import(\"three/examples/jsm/postprocessing/OutputPass.js\"),\n ]).then(([{ EffectComposer }, { RenderPass }, { SSAOPass }, { UnrealBloomPass }, { OutputPass }]) => {\n postClasses = { EffectComposer, RenderPass, SSAOPass, UnrealBloomPass, OutputPass };\n composerLoading = false;\n buildComposer();\n });\n };\n\n // Recompile materials so a shadowMap.enabled change takes effect.\n const refreshShadows = (): void => {\n renderer.shadowMap.enabled = render.shadows;\n threeScene.traverse((obj) => {\n const mat = (obj as { material?: unknown }).material;\n if (!mat) return;\n for (const m of Array.isArray(mat) ? mat : [mat]) (m as { needsUpdate: boolean }).needsUpdate = true;\n });\n };\n refreshShadows();\n ensureComposer();\n\n // Split the canvas into a left/right eye pair. Mirrors what StereoEffect does\n // internally, but stays in our loop and uses only setViewport/setScissor.\n const renderStereo = (): void => {\n camera.updateWorldMatrix(true, false);\n stereo.update(camera);\n renderer.getSize(stereoSize);\n const w = stereoSize.x;\n const h = stereoSize.y;\n if (renderer.autoClear) renderer.clear();\n renderer.setScissorTest(true);\n renderer.setScissor(0, 0, w / 2, h);\n renderer.setViewport(0, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraL);\n renderer.setScissor(w / 2, 0, w / 2, h);\n renderer.setViewport(w / 2, 0, w / 2, h);\n renderer.render(threeScene, stereo.cameraR);\n renderer.setScissorTest(false);\n renderer.setViewport(0, 0, w, h);\n };\n\n const renderFrame = (): void => {\n rafId = requestAnimationFrame(renderFrame);\n const delta = clock.getDelta();\n const deltaMs = delta * 1000;\n // Systems drive the camera in registration order: the roaming camera first,\n // then the framing system, which may override it during a focus animation.\n for (const system of systems) system.update?.(deltaMs);\n interaction.update(deltaMs);\n manager.updateElements(deltaMs);\n for (const controller of controllers) controller.update?.(deltaMs);\n // Behavior runs after the camera moved this frame, before rendering. (The\n // cardboard system, last in `systems`, already had the final say on\n // orientation above.)\n for (const handler of frameHandlers) handler(deltaMs);\n // Auxiliary passes (e.g. the water's planar reflection) render into their\n // own targets from the current camera, before the main frame.\n manager.beforeRender(renderer, threeScene, camera);\n if (render.cardboard) renderStereo();\n else if (composer && needsComposer()) composer.render(delta);\n else renderer.render(threeScene, camera);\n };\n\n const ctx: GalleryContext = {\n entity: (id) => manager.get(id),\n lights: lightsApi,\n media: mediaApi,\n controls: {\n setMoveInput: (forward, strafe) => input.setMove(forward, strafe),\n setLookInput: (x, y) => input.setLook(x, y),\n },\n viewControls: {\n get: () => controlSettings.get(),\n getMode: () => controlSettings.getMode(),\n setMode: (mode) => controlSettings.setMode(mode),\n setFlag: (key, value) => controlSettings.setFlag(key, value),\n subscribe: (fn) => controlSettings.subscribe(fn),\n },\n environment: {\n setBackgroundIntensity: (v) => environment.setBackgroundIntensity(v),\n },\n sky: skyApi,\n camera: cameraApi,\n video: videoApi,\n three: { scene: threeScene, camera, renderer },\n };\n\n return {\n ctx,\n setScene(next) {\n manager.reconcile(next);\n interaction.setRadii(radiiOf(next));\n environment.reconcile(next.environment);\n for (const system of systems) system.setScene?.(next);\n const fov = next.camera.fov ?? 55;\n if (camera.fov !== fov) {\n camera.fov = fov;\n camera.updateProjectionMatrix();\n }\n },\n start() {\n if (rafId === null) {\n clock.start();\n renderFrame();\n }\n },\n stop() {\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n },\n dispose() {\n this.stop();\n for (const system of systems) system.dispose?.();\n for (const controller of controllers) controller.dispose?.();\n interaction.detach();\n environment.dispose();\n manager.dispose();\n composer?.dispose();\n renderer.dispose();\n listeners.clear();\n frameHandlers.clear();\n },\n on(event, handler) {\n const set = listeners.get(event) ?? new Set();\n set.add(handler);\n listeners.set(event, set);\n return () => set.delete(handler);\n },\n emit(event, payload) {\n emit(event, payload);\n },\n onFrame(handler) {\n frameHandlers.add(handler);\n return () => frameHandlers.delete(handler);\n },\n resize(w, h) {\n camera.aspect = w / h;\n camera.updateProjectionMatrix();\n renderer.setSize(w, h, false);\n composer?.setSize(w, h);\n ssaoPass?.setSize(w, h);\n bloomPass?.setSize(w, h);\n },\n setRender(next) {\n if (next.shadows !== undefined && next.shadows !== render.shadows) {\n render.shadows = next.shadows;\n refreshShadows();\n }\n if (next.ssao !== undefined) render.ssao = next.ssao;\n if (next.bloom !== undefined) render.bloom = next.bloom;\n // The sample count lives on the render target, so a change rebuilds the\n // composer (cheap, and only on user toggle).\n const aaChanged = next.antialias !== undefined && next.antialias !== render.antialias;\n if (next.antialias !== undefined) render.antialias = next.antialias;\n if (next.cardboard !== undefined && next.cardboard !== render.cardboard) {\n // Legacy stereo toggle: route through the control state so mode/flags and\n // the render stay one source of truth (the subscriber applies gyro +\n // split; setEnabled thus still runs in this click gesture, which iOS needs\n // for the motion-permission prompt). Superseded by `ctx.viewControls` once\n // the mode picker replaces the cardboard button.\n controlSettings.setMode(next.cardboard ? \"cardboard\" : \"touch\");\n }\n if (next.eyeSeparation !== undefined) {\n render.eyeSeparation = next.eyeSeparation;\n stereo.eyeSep = next.eyeSeparation;\n }\n if (ssaoPass) ssaoPass.enabled = render.ssao;\n if (bloomPass) {\n bloomPass.strength = render.bloom;\n bloomPass.enabled = render.bloom > 0;\n }\n if (aaChanged && composer) buildComposer();\n ensureComposer(); // load/build the chain if an effect was just turned on\n },\n };\n}\n","import {\n MeshStandardMaterial,\n NoColorSpace,\n RepeatWrapping,\n SRGBColorSpace,\n type Texture,\n TextureLoader,\n Vector2,\n} from \"three\";\nimport type { SurfaceTexture, Vec2 } from \"@scenography/scene\";\n\nconst textureLoader = new TextureLoader();\n\ntype MapSlot = \"map\" | \"normalMap\" | \"roughnessMap\" | \"aoMap\";\n\n/** Configure a loaded texture: tiling + color space (sRGB only for color maps). */\nconst configure = (tex: Texture, repeat: Vec2, srgb: boolean): void => {\n tex.wrapS = RepeatWrapping;\n tex.wrapT = RepeatWrapping;\n tex.repeat.set(repeat[0], repeat[1]);\n tex.colorSpace = srgb ? SRGBColorSpace : NoColorSpace;\n tex.needsUpdate = true;\n};\n\n/**\n * Load one map and assign it to `material[slot]` once ready. `.exr` maps go\n * through EXRLoader, imported on demand so it only ships in the bundle when a\n * scene actually uses an EXR map; everything else uses the image loader.\n */\nconst loadMap = (\n material: MeshStandardMaterial,\n slot: MapSlot,\n src: string,\n repeat: Vec2,\n srgb: boolean,\n): void => {\n const assign = (tex: Texture): void => {\n configure(tex, repeat, srgb);\n material[slot] = tex;\n material.needsUpdate = true;\n };\n if (src.toLowerCase().endsWith(\".exr\")) {\n void import(\"three/examples/jsm/loaders/EXRLoader.js\").then(({ EXRLoader }) => {\n new EXRLoader().load(src, assign);\n });\n } else {\n textureLoader.load(src, assign);\n }\n};\n\n/**\n * Apply a `SurfaceTexture` to a standard material. Returns whether a second UV\n * set (`uv1`) is needed — three samples `aoMap` from it, so the geometry must\n * provide one.\n */\nexport const applyTexture = (\n material: MeshStandardMaterial,\n texture: SurfaceTexture,\n): boolean => {\n const repeat = texture.repeat ?? [1, 1];\n loadMap(material, \"map\", texture.src, repeat, true);\n if (texture.normal) {\n loadMap(material, \"normalMap\", texture.normal, repeat, false);\n const s = texture.normalScale ?? 1;\n material.normalScale = new Vector2(s, s);\n }\n if (texture.roughness) loadMap(material, \"roughnessMap\", texture.roughness, repeat, false);\n if (texture.ao) loadMap(material, \"aoMap\", texture.ao, repeat, false);\n return texture.ao !== undefined;\n};\n"],"mappings":"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"}
|