EmboDi / src /client /rendering /production-studio.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
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
};