Download src/client/rendering/production-studio.mjs from xlt99/EmboDi: direct link, hf CLI and curl.
- Browser
- Download file 30.2 kB
-
https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/client/rendering/production-studio.mjs
- Command line
-
hf download hf://spaces/xlt99/EmboDi/src/client/rendering/production-studio.mjs
-
curl -L -o production-studio.mjs https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/client/rendering/production-studio.mjs
30.2 kB
| import {languagePattern} from "../../shared/language.mjs"; | |
| import { entityLabel } from "../../shared/entity-label.mjs"; | |
| import * as THREE from "three"; | |
| import { setApiSession } from "../api.mjs"; | |
| import { applyCinema, isCamera, isLight, isRig, objectQuaternion, editableControls, curvePoints, groundCreateCommand } from "../../shared/cinema.mjs"; | |
| import { createTriggerRegionLayer } from "./trigger-region-layer.mjs"; | |
| import { createCinemaLayer } from "./cinema-layer.mjs"; | |
| import { createVirtualScene, applyRecordedTransforms } from "./virtual-video.mjs"; | |
| import { createActorLayer } from "./actor-view.mjs"; | |
| import { productionSpecialist } from "../../shared/production-intent.mjs"; | |
| import { previewScope, isolatePreviewActors } from "../../shared/preview-scope.mjs"; | |
| import { uprightMonitorPose } from "../../shared/monitor-pose.mjs"; | |
| import { createTrackTriggerRuntime } from "../../shared/track-triggers.mjs"; | |
| import { cameraViewIntent } from "../../shared/camera-view-intent.mjs"; | |
| function createProductionStudio({ world, scene, renderer, api, getState, getSelection, getView, getSpatialKey, getMeshes, pickGround = () => null, canCreate = () => true, canMonitor = () => true, assets, onState, onPreview, onChange, onSelect = () => { | |
| }, onPlay, notice, hideMenu, pauseScene, transport }) { | |
| let editing = false, session = null, transitioning = false, editor = null, pending = null, saving = false, request = null, monitor = null, selectedCamera = null, playing = false, time = 0, lastMonitor = 0, definitionKey = "", speed = 0.5, neutral = false; | |
| const triggerRuntime = createTrackTriggerRuntime(); | |
| let triggerKey = "", triggerStatusKey = ""; | |
| const regionLayer = createTriggerRegionLayer(world), rig = createCinemaLayer(world), guides = new THREE.Group(); | |
| world.add(guides); | |
| let hasPlayed = false, focusIds = [], activeScope = null, modeTransition = null, previewRevision = null; | |
| const vec = (p) => new THREE.Vector3(...p), quat = (q) => new THREE.Quaternion(...q); | |
| const state = () => getState(), definition = () => pending?.scene || state().scene; | |
| const changed = () => onChange(); | |
| const focus = () => previewScope(state().scene, editing ? getSelection() : focusIds); | |
| const requestPlay = (options) => onPlay ? onPlay(options) : play(options); | |
| function clearGuides() { | |
| for (const o of [...guides.children]) { | |
| o.geometry?.dispose(); | |
| o.material?.dispose(); | |
| guides.remove(o); | |
| } | |
| } | |
| function dirty() { | |
| return !!editor || !!pending; | |
| } | |
| function requireEdit() { | |
| if (!editing || session?.mode !== "edit" || transitioning) throw Error("Enter edit mode first."); | |
| closeMonitor(); | |
| } | |
| function transformable() { | |
| return editor?.type === "poses"; | |
| } | |
| function savedCameras() { | |
| return state().scene.objects.filter(isCamera); | |
| } | |
| function closeMonitor() { | |
| if (!monitor) return; | |
| scene.remove(monitor.screen); | |
| monitor.screen.geometry.dispose(); | |
| monitor.screen.material.dispose(); | |
| monitor.target.dispose(); | |
| monitor.actors.dispose(); | |
| monitor.content.userData.cinema?.dispose(); | |
| monitor.flood?.dispose(); | |
| monitor.content.traverse((o) => { | |
| o.geometry?.dispose(); | |
| o.material?.dispose(); | |
| }); | |
| monitor = null; | |
| changed(); | |
| } | |
| async function syncMode(next) { | |
| transitioning = true; | |
| changed(); | |
| try { | |
| session = await api("/api/authoring/session", { ...session ? { id: session.id, epoch: session.epoch } : {}, mode: next }); | |
| setApiSession(session); | |
| } catch (error) { | |
| session = null; | |
| setApiSession(null); | |
| notice("Mode sync failed. Refresh and retry: " + error.message); | |
| throw error; | |
| } finally { | |
| transitioning = false; | |
| changed(); | |
| } | |
| } | |
| function cancel() { | |
| if (saving) { | |
| notice("Saving. Undo will be available when finished."); | |
| return; | |
| } | |
| const hadPreview = !!pending; | |
| if (request) api("/api/cinema/cancel", {}).catch(() => { | |
| }); | |
| request?.abort(); | |
| request = null; | |
| editor = null; | |
| pending = null; | |
| clearGuides(); | |
| if (hadPreview) onPreview(null); | |
| neutral = false; | |
| changed(); | |
| } | |
| function selectCamera(id) { | |
| const list = savedCameras(); | |
| if (!list.length) throw Error("Create and save a virtual camera first."); | |
| const selected = editing ? list.find((c) => getSelection().includes(c.id)) : null; | |
| const target = id ? list.find((c) => c.id === id) : selected || list.find((c) => c.id === selectedCamera) || list[0]; | |
| if (!target) throw Error("Select a saved scene camera."); | |
| selectedCamera = target.id; | |
| return target; | |
| } | |
| function openMonitor(id) { | |
| if (!session || !["edit", "explore"].includes(session.mode) || dirty() || transitioning || saving || !canMonitor()) throw Error("A Save or B Cancel the current edit before viewing a saved camera."); | |
| const camera = selectCamera(id); | |
| closeMonitor(); | |
| world.updateMatrixWorld(true); | |
| const content = createVirtualScene(state().scene, world.matrixWorld), actors = createActorLayer(content.userData.contentWorld, assets), flood = content.userData.flood; | |
| const target = new THREE.WebGLRenderTarget(768, 432), screen = new THREE.Mesh(new THREE.PlaneGeometry(1.08, 0.6075), new THREE.MeshBasicMaterial({ map: target.texture, toneMapped: false, side: THREE.DoubleSide, depthTest: false, depthWrite: false })); | |
| screen.renderOrder = 18; | |
| scene.add(screen); | |
| monitor = { content, actors, flood, target, screen, revision: state().revision, key: getSpatialKey() }; | |
| recenterMonitor(); | |
| hideMenu(); | |
| notice(`${entityLabel(camera)} · A Recenter · B Close · Save camera edits before reopening`); | |
| changed(); | |
| } | |
| function recenterMonitor() { | |
| if (!monitor) return false; | |
| const view = getView(), pose = uprightMonitorPose(view.getWorldPosition(new THREE.Vector3()).toArray(), view.getWorldQuaternion(new THREE.Quaternion()).toArray(), monitor.yaw || 0); | |
| monitor.screen.position.fromArray(pose.position); | |
| monitor.screen.quaternion.fromArray(pose.quaternion); | |
| monitor.yaw = pose.yaw; | |
| monitor.screen.updateMatrixWorld(true); | |
| changed(); | |
| return true; | |
| } | |
| function adjustCamera() { | |
| const id = selectedCamera; | |
| requireEdit(); | |
| if (!id) throw Error("Select a saved camera first."); | |
| onSelect(id); | |
| startPoses(); | |
| } | |
| function cameraStep(delta) { | |
| const list = savedCameras(); | |
| if (!list.length) return openMonitor(); | |
| const index = list.findIndex((c) => c.id === selectedCamera); | |
| selectedCamera = list[(index + delta + list.length) % list.length].id; | |
| if (!monitor) openMonitor(selectedCamera); | |
| else { | |
| notice("Current camera: " + entityLabel(list.find((c) => c.id === selectedCamera))); | |
| changed(); | |
| } | |
| } | |
| function startPoses() { | |
| requireEdit(); | |
| if (dirty() || saving || request) throw Error("Save or cancel the current operation first."); | |
| const targets = [...state().scene.objects, ...state().scene.actors || []].filter((o) => getSelection().includes(o.id)); | |
| if (!targets.length) throw Error("Select an object first."); | |
| if (targets.some((o) => o.editable !== true && (o.id === "ground" || o.category === "structure" || /^(wall|ceiling)(_|-|$)/.test(o.id)))) throw Error("Reference structure is locked."); | |
| pauseScene(); | |
| playing = false; | |
| const poses = targets.map((o) => ({ id: o.id, position: [...o.position], quaternion: objectQuaternion(o) })); | |
| const forward = getView().getWorldDirection(new THREE.Vector3()).transformDirection(world.matrixWorld.clone().invert()); | |
| forward.y = 0; | |
| forward.normalize(); | |
| editor = { type: "poses", poses, revision: state().revision, key: getSpatialKey(), forward, grab: null }; | |
| neutral = false; | |
| hideMenu(); | |
| changed(); | |
| notice("Transform: L stick Move · L grip + stick Height · R stick Rotate · R grip Grab · A Save · B Revert"); | |
| } | |
| function drawControls() { | |
| clearGuides(); | |
| if (editor?.type !== "curve") return; | |
| const points = curvePoints(editor.controls, true), path = new THREE.CurvePath(); | |
| for (let i = 1; i < points.length; i++) path.add(new THREE.LineCurve3(vec(points[i - 1]), vec(points[i]))); | |
| const line = new THREE.Mesh(new THREE.TubeGeometry(path, Math.min(256, points.length * 2), 0.022, 6, false), new THREE.MeshBasicMaterial({ color: "#ad95ff", toneMapped: false })); | |
| line.userData.curveSegment = true; | |
| guides.add(line); | |
| editor.controls.forEach((p, index) => { | |
| const m = new THREE.Mesh(new THREE.SphereGeometry(0.065, 12, 8), new THREE.MeshBasicMaterial({ color: editor.grab?.index === index ? "#ffffff" : "#ffcf70", depthTest: false, toneMapped: false })); | |
| m.position.fromArray(p); | |
| m.userData.controlIndex = index; | |
| m.renderOrder = 10; | |
| guides.add(m); | |
| }); | |
| } | |
| function editCurve(id) { | |
| requireEdit(); | |
| if (dirty()) throw Error("Save or cancel the current operation first."); | |
| const c = state().scene.curves?.find((c2) => c2.id === id); | |
| if (!c) throw Error("Save and select a curve first."); | |
| editor = { type: "curve", id, controls: editableControls(c), revision: state().revision, key: getSpatialKey(), grab: null }; | |
| drawControls(); | |
| hideMenu(); | |
| changed(); | |
| notice("Grip and pull control points · A Save smoothed curve · B Cancel"); | |
| } | |
| function localHand(controller) { | |
| world.updateMatrixWorld(true); | |
| controller.updateMatrixWorld(true); | |
| return world.matrixWorld.clone().invert().multiply(controller.matrixWorld); | |
| } | |
| function grab(controller, ray) { | |
| if (!editor || editor.type === "placement" || editor.grab || saving) return false; | |
| const hand = localHand(controller); | |
| if (editor.type === "poses") editor.grab = { controller, start: hand.clone().invert(), poses: structuredClone(editor.poses) }; | |
| else { | |
| const hit = ray.intersectObjects(guides.children, false)[0]; | |
| if (!hit) return false; | |
| let index = hit.object.userData.controlIndex; | |
| if (index === void 0) { | |
| if (editor.controls.length >= 64) throw Error("Up to 64 control points."); | |
| const point = world.worldToLocal(hit.point.clone()); | |
| let best = Infinity; | |
| index = 1; | |
| for (let i = 1; i < editor.controls.length; i++) { | |
| const near = new THREE.Line3(vec(editor.controls[i - 1]), vec(editor.controls[i])).closestPointToPoint(point, true, new THREE.Vector3()).distanceToSquared(point); | |
| if (near < best) { | |
| best = near; | |
| index = i; | |
| } | |
| } | |
| editor.controls.splice(index, 0, point.toArray()); | |
| } | |
| editor.grab = { controller, index, start: hand.clone().invert(), point: vec(editor.controls[index]) }; | |
| drawControls(); | |
| } | |
| neutral = false; | |
| changed(); | |
| return true; | |
| } | |
| function release() { | |
| if (editor?.grab) { | |
| editor.grab = null; | |
| neutral = false; | |
| changed(); | |
| } | |
| } | |
| function input(sources, dt) { | |
| if (!editor || saving) return; | |
| dt = Math.min(dt, 0.05); | |
| if (editor.revision !== state().revision || editor.key !== getSpatialKey()) { | |
| cancel(); | |
| notice("Scene or alignment changed. Edits reverted."); | |
| return; | |
| } | |
| if (editor.grab) { | |
| const g = editor.grab; | |
| if (!g.controller.visible) { | |
| cancel(); | |
| notice("Tracking interrupted. Edits cancelled."); | |
| return; | |
| } | |
| const delta2 = localHand(g.controller).multiply(g.start), alpha = 1 - Math.exp(-20 * dt); | |
| if (editor.type === "poses") for (let i = 0; i < editor.poses.length; i++) { | |
| const base = g.poses[i], matrix = delta2.clone().multiply(new THREE.Matrix4().compose(vec(base.position), quat(base.quaternion), new THREE.Vector3(1, 1, 1))), p = new THREE.Vector3(), q2 = new THREE.Quaternion(); | |
| matrix.decompose(p, q2, new THREE.Vector3()); | |
| editor.poses[i].position = vec(editor.poses[i].position).lerp(p, alpha).toArray().map((v) => THREE.MathUtils.clamp(v, -99, 99)); | |
| editor.poses[i].quaternion = quat(editor.poses[i].quaternion).slerp(q2, alpha).toArray(); | |
| } | |
| else { | |
| const previousY = editor.controls[g.index][1]; | |
| editor.controls[g.index] = g.point.clone().applyMatrix4(delta2).toArray().map((v) => THREE.MathUtils.clamp(v, -99, 99)); | |
| if (state().scene.curves.find((c) => c.id === editor.id)?.mode === "floor2d") editor.controls[g.index][1] = previousY; | |
| drawControls(); | |
| } | |
| return; | |
| } | |
| if (editor.type !== "poses") return; | |
| const pad = (hand) => sources.find((s) => s.handedness === hand)?.gamepad?.axes || [], left = pad("left"), right = pad("right"), axes = [left[2] || 0, left[3] || 0, right[2] || 0, right[3] || 0]; | |
| const grip = sources.find((s) => s.handedness === "left")?.gamepad?.buttons?.[1], vertical = !!(grip && (grip.pressed || grip.value > 0.65)); | |
| if (editor.vertical !== vertical) { | |
| editor.vertical = vertical; | |
| neutral = false; | |
| } | |
| if (!neutral) { | |
| neutral = axes.every((v) => Math.abs(v) < 0.18); | |
| return; | |
| } | |
| const [x, y, rx, ry] = axes.map((v) => Math.abs(v) > 0.18 ? v : 0); | |
| if (!x && !y && !rx && !ry) return; | |
| const forward = editor.forward, side = new THREE.Vector3(-forward.z, 0, forward.x), delta = vertical ? new THREE.Vector3(0, -y * speed * dt, 0) : side.multiplyScalar(x).addScaledVector(forward, -y).multiplyScalar(speed * dt); | |
| const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(-ry * dt * 0.8, -rx * dt * 0.8, 0, "YXZ")); | |
| for (const p of editor.poses) { | |
| p.position = vec(p.position).add(delta).toArray().map((v) => THREE.MathUtils.clamp(v, -99, 99)); | |
| p.quaternion = q.clone().multiply(quat(p.quaternion)).normalize().toArray(); | |
| } | |
| } | |
| function beginPlacement(command) { | |
| requireEdit(); | |
| if (dirty() || saving || request || !canCreate()) throw Error("Save or cancel the current operation first."); | |
| applyCinema(state().scene, groundCreateCommand(command, [0, 0, 0])); | |
| pauseScene(); | |
| clearGuides(); | |
| const cursor = new THREE.Mesh(new THREE.RingGeometry(0.18, 0.25, 48), new THREE.MeshBasicMaterial({ color: "#b79bea", side: THREE.DoubleSide, toneMapped: false })); | |
| cursor.rotation.x = -Math.PI / 2; | |
| cursor.visible = false; | |
| guides.add(cursor); | |
| editor = { type: "placement", command: structuredClone(command), point: null, cursor, revision: state().revision, key: getSpatialKey() }; | |
| hideMenu(); | |
| changed(); | |
| notice("Point at ground with your right hand · A Create · B Cancel"); | |
| } | |
| function updatePlacement(ray) { | |
| if (editor?.type !== "placement" || saving) return; | |
| if (editor.revision !== state().revision || editor.key !== getSpatialKey()) { | |
| cancel(); | |
| notice("Scene or alignment changed. Placement cancelled."); | |
| return; | |
| } | |
| const hit = ray && pickGround(ray), point = hit?.toArray?.() || hit || null; | |
| let valid = !!point; | |
| try { | |
| if (valid) groundCreateCommand(editor.command, point); | |
| } catch { | |
| valid = false; | |
| } | |
| const wasValid = !!editor.point; | |
| editor.point = valid ? [...point] : null; | |
| editor.cursor.visible = valid; | |
| if (valid) editor.cursor.position.fromArray(point).add(new THREE.Vector3(0, 0.015, 0)); | |
| if (wasValid !== valid) changed(); | |
| } | |
| function preview(command) { | |
| requireEdit(); | |
| if (dirty()) throw Error("Save or cancel the current operation first."); | |
| if (command.op === "create") return beginPlacement(command); | |
| pending = { command, scene: applyCinema(state().scene, command), revision: state().revision, key: getSpatialKey() }; | |
| onPreview(pending.scene); | |
| changed(); | |
| } | |
| async function save() { | |
| requireEdit(); | |
| if (saving) return; | |
| if (editor?.grab) throw Error("Release the grip before saving."); | |
| if (editor && (editor.key !== getSpatialKey() || editor.revision !== state().revision)) { | |
| cancel(); | |
| throw Error("Scene or alignment changed. Choose a new point or restart editing."); | |
| } | |
| const command = editor?.type === "placement" ? groundCreateCommand(editor.command, editor.point) : editor?.type === "poses" ? { op: "poses", poses: editor.poses } : editor?.type === "curve" ? { op: "curve", id: editor.id, controls: editor.controls, smooth: true } : pending?.command; | |
| if (!command) return; | |
| const revision = editor?.revision ?? pending.revision; | |
| if (revision !== state().revision) throw Error("Scene changed. Cancel and restart editing."); | |
| saving = true; | |
| changed(); | |
| try { | |
| const next = await api("/api/cinema/commit", { revision, command }); | |
| editor = null; | |
| pending = null; | |
| clearGuides(); | |
| onPreview(null); | |
| onState(next); | |
| notice(command.op === "create" ? "Created at your point · Use Transform to adjust position and height." : "Saved · Left grip or Interaction → Rehearse selection."); | |
| } finally { | |
| saving = false; | |
| changed(); | |
| } | |
| if (command.op === "create") onSelect(command.id); | |
| } | |
| function play({ restart = false, ids } = {}) { | |
| if (editing || session?.mode !== "explore" || transitioning || dirty() || saving) throw Error("Save edits, click the right stick to explore, then use left grip to rehearse."); | |
| const next = previewScope(state().scene, ids ?? focusIds); | |
| if (!next.ownerIds.length || next.missingIds.length) throw Error("Enter edit mode and select an object to rehearse."); | |
| if (!next.playable) throw Error("Bind a curve or save an interaction for this object first."); | |
| const same = activeScope && JSON.stringify(activeScope) === JSON.stringify(next) && previewRevision === state().revision; | |
| if (playing && !restart && same) return; | |
| const first = !hasPlayed || restart || !same; | |
| try { | |
| transport(first ? "start" : "resume", next); | |
| } catch (error) { | |
| activeScope = null; | |
| hasPlayed = false; | |
| playing = false; | |
| time = 0; | |
| pauseScene(); | |
| changed(); | |
| throw error; | |
| } | |
| if (first) { | |
| time = 0; | |
| triggerRuntime.sync(state().scene); | |
| triggerKey = JSON.stringify(state().scene); | |
| triggerRuntime.start(next.objectIds); | |
| } else triggerRuntime.resume(); | |
| focusIds = [...next.ownerIds]; | |
| activeScope = next; | |
| previewRevision = state().revision; | |
| hasPlayed = true; | |
| playing = true; | |
| hideMenu(); | |
| notice(`Rehearsing: ${next.label} · Left grip Pause / Resume · Right stick click Edit`); | |
| changed(); | |
| } | |
| function pause() { | |
| playing = false; | |
| triggerRuntime.pause(); | |
| transport("pause", activeScope); | |
| changed(); | |
| } | |
| async function handleText(text, context) { | |
| const viewAction = cameraViewIntent(text); | |
| if (viewAction === "recenter") { | |
| if (!monitor) return "Ask Camera Agent to open the monitor first."; | |
| recenterMonitor(); | |
| return "Monitor recentered upright. Camera pose and playback are preserved."; | |
| } | |
| if (viewAction === "close") { | |
| closeMonitor(); | |
| return "Camera monitor closed."; | |
| } | |
| if (viewAction) { | |
| if (context?.revision !== void 0 && context.revision !== state().revision) throw Error("Scene changed while speaking. Select the camera again."); | |
| if (dirty() || saving || transitioning || request) return "A Save or B Cancel the current edit before viewing a camera."; | |
| if (viewAction === "preview") { | |
| const ids = context?.targetIds || getSelection(), camera = state().scene.objects.find((o) => isCamera(o) && ids.includes(o.id)); | |
| openMonitor(camera?.id); | |
| } else cameraStep(viewAction === "next" ? 1 : -1); | |
| return `Viewing saved camera ${entityLabel(savedCameras().find((c) => c.id === selectedCamera))}. B Close; editing closes the monitor.`; | |
| } | |
| if (editor?.type === "poses") { | |
| if (languagePattern("transform.faster").test(text)) { | |
| speed = Math.min(4, speed * 2); | |
| return `Movement speed is now ${speed} m/s.`; | |
| } | |
| if (languagePattern("transform.slower").test(text)) { | |
| speed = Math.max(0.025, speed / 2); | |
| return `Movement speed is now ${speed} m/s.`; | |
| } | |
| if (languagePattern("transform.verticalRequest").test(text)) { | |
| const metres = languagePattern("transform.meters").test(text) && !languagePattern("transform.centimeters").test(text), amount = Number(text.match(/\d+(?:\.\d+)?/)?.[0] || 20) * (metres ? 1 : 0.01) * (languagePattern("transform.lower").test(text) ? -1 : 1); | |
| for (const p of editor.poses) p.position[1] = THREE.MathUtils.clamp(p.position[1] + amount, -99, 99); | |
| return "Height preview adjusted. A Save, B Revert."; | |
| } | |
| return "A Save or B Cancel this edit. You can also say faster, slower, or raise by 20 centimetres."; | |
| } | |
| if (dirty()) return "A Save or B Cancel the current preview first."; | |
| const words = text.trim().replace(languagePattern("punctuation.production-studio.1"), ""); | |
| if (languagePattern("preview.stop").test(words)) { | |
| pause(); | |
| return "Rehearsal paused. Left grip to resume."; | |
| } | |
| if (languagePattern("preview.play").test(words)) { | |
| await requestPlay({ restart: languagePattern("preview.restart").test(words) }); | |
| return `Rehearsing ${activeScope?.label || focus().label}. Left grip to pause / resume.`; | |
| } | |
| const targetIds = editing ? context?.targetIds || getSelection() : focusIds; | |
| const specialist = productionSpecialist(text, { scene: state().scene, ids: targetIds }); | |
| if (editing && !specialist) return null; | |
| if (request || transitioning) throw Error("The previous request is still processing."); | |
| const captured = session && { ...session }; | |
| if (context?.revision !== void 0 && context.revision !== state().revision) throw Error("Scene changed while speaking. Please retry."); | |
| if (context?.productionEpoch && context.productionEpoch !== captured?.epoch) throw Error("Mode changed while speaking. Please retry."); | |
| request = new AbortController(); | |
| const controller = request; | |
| changed(); | |
| try { | |
| const result = await api("/api/cinema/agent", { prompt: text, revision: state().revision, ids: targetIds, curveId: context?.curveId || null, specialist: specialist || "camera" }, { signal: controller.signal, timeoutMs: 125e3 }); | |
| if (controller.signal.aborted || session?.epoch !== captured?.epoch) return null; | |
| if (result.action === "edit") { | |
| if (result.command.op === "create") { | |
| request = null; | |
| beginPlacement(result.command); | |
| return "Placement ready. Point at ground with your right hand. A Create, B Cancel."; | |
| } | |
| preview(result.command); | |
| } else if (result.action === "editCurve") editCurve(result.id); | |
| else if (result.action === "preview") openMonitor(result.id); | |
| else if (result.action === "next") cameraStep(1); | |
| else if (result.action === "previous") cameraStep(-1); | |
| else if (result.action === "close") closeMonitor(); | |
| else if (result.action === "play") { | |
| request = null; | |
| changed(); | |
| await requestPlay(result.id ? { ids: [result.id] } : {}); | |
| } else if (result.action === "pause") pause(); | |
| return result.reply || "Done."; | |
| } finally { | |
| if (request === controller) request = null; | |
| changed(); | |
| } | |
| } | |
| function frame(dt, actors = [], transforms = [], flood = { states: [] }, tracking = {}) { | |
| const d = definition(), next = JSON.stringify(d); | |
| if (next !== definitionKey) { | |
| definitionKey = next; | |
| rig.sync(d); | |
| } | |
| if (activeScope && previewRevision !== state().revision) { | |
| activeScope = null; | |
| hasPlayed = false; | |
| playing = false; | |
| time = 0; | |
| pauseScene(); | |
| notice("Scene updated. Select an object again to rehearse."); | |
| changed(); | |
| } | |
| if (!editing && playing) time += Math.min(dt, 0.1); | |
| const targets = [...d.objects, ...(d.actors || []).map((o) => ({ ...o, position: actors.find((a) => a.id === o.id)?.position || o.position }))]; | |
| const savedKey = JSON.stringify(state().scene); | |
| if (savedKey !== triggerKey) { | |
| triggerKey = savedKey; | |
| triggerRuntime.sync(state().scene); | |
| } | |
| const triggerState = triggerRuntime.tick({ ...tracking, active: !!tracking.active && !editing && playing }); | |
| regionLayer.frame(d, { editing, states: triggerState.states }); | |
| const statusKey = triggerState.states.map((s) => s.id + ":" + (s.fired ? "fired" : s.inside ? Math.floor(s.dwell) : "waiting")).join("|"); | |
| if (statusKey !== triggerStatusKey) { | |
| triggerStatusKey = statusKey; | |
| const s = triggerState.states[0]; | |
| if (s) notice(s.fired ? "Region condition met. Starting this object's movement." : s.inside ? `Dwell in region: ${s.dwell.toFixed(1)} / ${s.seconds} s` : `Enter the floor region · Dwell continuously for ${s.seconds} s`); | |
| } | |
| const activeIds = activeScope?.objectIds || []; | |
| const frames = rig.frame(editing ? null : time, targets, [...transforms, ...editor?.type === "poses" ? editor.poses : []], activeIds, triggerState.clocks); | |
| for (const p of frames) { | |
| const mesh = getMeshes().get(p.id); | |
| if (!mesh) continue; | |
| if (isRig(mesh.userData.definition)) { | |
| mesh.visible = editing; | |
| if (!mesh.userData.cinemaGuide) { | |
| const arrow = new THREE.ArrowHelper(new THREE.Vector3(0, 0, -1), new THREE.Vector3(), 2.5, isCamera(mesh.userData.definition) ? 5554943 : 16767874, 0.6, 0.3); | |
| mesh.add(arrow); | |
| mesh.userData.cinemaGuide = arrow; | |
| } | |
| } | |
| if (editor?.poses?.some((o) => o.id === p.id) || mesh.userData.definition.track) { | |
| mesh.position.fromArray(p.position); | |
| mesh.quaternion.fromArray(p.quaternion); | |
| } | |
| } | |
| if (monitor) { | |
| if (dirty() || !canMonitor() || monitor.revision !== state().revision || monitor.key !== getSpatialKey()) { | |
| closeMonitor(); | |
| return frames; | |
| } | |
| const now = performance.now(); | |
| if (now - lastMonitor > 1e3 / 24) { | |
| lastMonitor = now; | |
| const w = monitor.content.userData.contentWorld; | |
| w.matrix.copy(world.matrixWorld); | |
| w.matrixWorldNeedsUpdate = true; | |
| monitor.actors.apply(actors, { clean: false }); | |
| applyRecordedTransforms(monitor.content, [...transforms, ...frames.filter((p) => d.objects.find((o) => o.id === p.id)?.track)]); | |
| monitor.flood?.frame(flood, { showRegions: false }); | |
| const cameraLayer = monitor.content.userData.cinema; | |
| cameraLayer?.frame(editing ? null : time, targets, [], activeIds, triggerState.clocks); | |
| const camera = cameraLayer?.cameras.get(selectedCamera); | |
| if (camera) { | |
| monitor.content.updateMatrixWorld(true); | |
| const previous = renderer.getRenderTarget(), xr = renderer.xr.enabled; | |
| try { | |
| renderer.xr.enabled = false; | |
| renderer.setRenderTarget(monitor.target); | |
| renderer.render(monitor.content, camera); | |
| } finally { | |
| renderer.setRenderTarget(previous); | |
| renderer.xr.enabled = xr; | |
| } | |
| } | |
| } | |
| } | |
| return frames; | |
| } | |
| return { | |
| initialize: () => modeTransition = syncMode("edit"), | |
| ready: () => modeTransition, | |
| setEditing(value, scope = value ? "edit" : "explore") { | |
| if (transitioning) return false; | |
| if (session?.mode === scope && editing === value) return true; | |
| if (dirty() || saving || request) { | |
| notice("Save or cancel the current operation first."); | |
| return false; | |
| } | |
| if (!value && scope === "explore") focusIds = [...getSelection()]; | |
| editing = value; | |
| closeMonitor(); | |
| triggerRuntime.stop(); | |
| playing = false; | |
| hasPlayed = false; | |
| activeScope = null; | |
| time = 0; | |
| pauseScene(); | |
| neutral = false; | |
| modeTransition = syncMode(scope); | |
| modeTransition.catch(() => { | |
| }); | |
| return true; | |
| }, | |
| cancel, | |
| save, | |
| startPoses, | |
| editCurve, | |
| grab, | |
| release, | |
| input, | |
| frame, | |
| handleText, | |
| preview, | |
| openMonitor, | |
| closeMonitor, | |
| recenterMonitor, | |
| adjustCamera, | |
| cameraStep, | |
| updatePlacement, | |
| create(kind) { | |
| beginPlacement({ op: "create", id: "rig-" + crypto.randomUUID(), kind }); | |
| }, | |
| async complete() { | |
| requireEdit(); | |
| if (dirty()) throw Error("Save the current tool edits first."); | |
| saving = true; | |
| changed(); | |
| try { | |
| onState(await api("/api/cinema/commit", { revision: state().revision, command: { op: "complete" } })); | |
| notice("Setup complete. Explore, rehearse and record."); | |
| } finally { | |
| saving = false; | |
| changed(); | |
| } | |
| }, | |
| play, | |
| pause, | |
| stopClock() { | |
| playing = false; | |
| triggerRuntime.stop(); | |
| }, | |
| isolateActors: (frames) => isolatePreviewActors(state().scene, frames, activeScope || { actorIds: frames.filter((f) => !f.preview).map((f) => f.id) }), | |
| allowsInteraction: (ids) => !activeScope || playing && ids.every((id) => activeScope.targetIds.includes(id)), | |
| cameraView: () => !editing && selectedCamera ? rig.cameras.get(selectedCamera) : null, | |
| livePoses: () => editor?.type === "poses" ? editor.poses.map((p) => ({ ...p, yaw: new THREE.Euler().setFromQuaternion(quat(p.quaternion), "YXZ").y })) : [], | |
| state: () => ({ editing, epoch: session?.epoch, tool: editor?.type === "poses" ? "transform" : editor?.type || null, vertical: !!editor?.vertical, placing: editor?.type === "placement", placement: editor?.type === "placement" ? { kind: editor.command.kind, point: editor.point ? [...editor.point] : null, visible: editor.cursor.visible } : null, dirty: dirty(), saving, requesting: !!request, transitioning, monitor: !!monitor, monitorPose: monitor ? { position: monitor.screen.position.toArray(), quaternion: monitor.screen.quaternion.toArray() } : null, triggers: triggerRuntime.snapshot(), selectedCamera, cameraName: entityLabel(savedCameras().find((c) => c.id === selectedCamera)), playing, time, speed, previewIds: focus().targetIds, previewLabel: focus().label, canPreview: !!focus().playable, activePreviewIds: activeScope?.targetIds || [], previewObjectIds: activeScope?.objectIds || [] }), | |
| busy: () => dirty() || saving || !!request || transitioning || !session, | |
| manipulating: () => !!editor && editor.type !== "placement" | |
| }; | |
| } | |
| export { | |
| createProductionStudio | |
| }; | |