Download src/client/rendering/director.mjs from xlt99/EmboDi: direct link, hf CLI and curl.
- Browser
- Download file 19 kB
-
https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/client/rendering/director.mjs
- Command line
-
hf download hf://spaces/xlt99/EmboDi/src/client/rendering/director.mjs
-
curl -L -o director.mjs https://huggingface.co/spaces/xlt99/EmboDi/resolve/main/src/client/rendering/director.mjs
19 kB
| import { UI_THEME as T } from "../../shared/ui-theme.mjs"; | |
| import { facingViewer } from "../../shared/cast-layout.mjs"; | |
| import * as THREE from "three"; | |
| import { actorMotionId, createPerformance, parseDirectorCommand } from "../../shared/actors.mjs"; | |
| import { actorMotionPresets, nextActorMotionPreset, STYLE_LABELS, changeCastStyle } from "../../shared/actor-presets.mjs"; | |
| import { createActorLayer } from "./actor-view.mjs"; | |
| import { CAST_SIZE as MAX_ACTORS, nextCastSlot } from "../../shared/actor-slots.mjs"; | |
| function createDirector({ authoring = false, world, assets, getState, getPhase, spatialKey, viewPosition, pickGround, occlusionDistance = () => Infinity, api, acceptState, notify, canEdit, canPlay = () => true, deferNewMotion = () => false, onSelect = () => { | |
| }, recordEvent = () => { | |
| }, mapFrames = (f) => f, pathCompleted = () => null }) { | |
| const performance = createPerformance(), layer = createActorLayer(world, assets); | |
| const cursor = new THREE.Mesh(new THREE.RingGeometry(0.18, 0.25, 32), new THREE.MeshBasicMaterial({ toneMapped: false, color: T.accent, side: THREE.DoubleSide, depthTest: false })); | |
| cursor.rotation.x = -Math.PI / 2; | |
| cursor.visible = false; | |
| cursor.renderOrder = 15; | |
| world.add(cursor); | |
| const facingArrow = new THREE.ArrowHelper(new THREE.Vector3(0, 0, 1), new THREE.Vector3(), 0.65, T.accent, 0.18, 0.12); | |
| facingArrow.line.material.toneMapped = facingArrow.cone.material.toneMapped = false; | |
| facingArrow.visible = false; | |
| facingArrow.line.material.depthTest = facingArrow.cone.material.depthTest = false; | |
| world.add(facingArrow); | |
| let lockedTarget = null; | |
| let selected = null, placing = false, placementDelay = 0, target = null, pending = false, frames = [], message = "Enter the room, then point at the floor to place an actor."; | |
| const emit = (text, status = true) => { | |
| message = text; | |
| notify(text, { status }); | |
| }; | |
| const selectedActor = () => getState()?.scene.actors?.find((a) => a.id === selected); | |
| function requireEdit() { | |
| if (getPhase() !== "explore") throw new Error("Enter the room first"); | |
| if (pending || !canEdit()) throw new Error("Finish the current request or preview first"); | |
| } | |
| function requirePlacement() { | |
| requireEdit(); | |
| if ((getState().scene.actors || []).length >= MAX_ACTORS) throw new Error(`${MAX_ACTORS} actors already placed. Remove one before adding another.`); | |
| } | |
| function validTarget(anchor) { | |
| if (!anchor || anchor.spatialKey !== spatialKey()) throw new Error("Point at the floor. Select a new location after alignment changes."); | |
| return anchor; | |
| } | |
| async function persist(command2) { | |
| requireEdit(); | |
| pending = true; | |
| try { | |
| const next = await api("/api/actors", { revision: getState().revision, command: command2 }); | |
| acceptState(next); | |
| recordEvent("actor-edit", { command: command2, revision: next.revision }); | |
| return next; | |
| } finally { | |
| pending = false; | |
| } | |
| } | |
| async function create(anchor, delay = 0) { | |
| requirePlacement(); | |
| const point = validTarget(anchor), actors = getState().scene.actors || [], preset = nextActorMotionPreset(actors, assets, getState().scene.actorStyle || "zombie"), asset = authoring && [...assets.values()].find((a) => a.defaultActor) || preset || [...assets.values()].find((a) => a.preferred) || assets.values().next().value; | |
| if (!asset) throw new Error("The default character asset has not loaded yet"); | |
| const delta = new THREE.Vector3().fromArray(point.viewer).sub(new THREE.Vector3().fromArray(point.position)); | |
| const actor = { id: "actor-" + crypto.randomUUID(), castSlot: nextCastSlot(actors), assetId: asset.id, motionId: deferNewMotion() || asset.bodyOnly ? null : preset?.id || null, name: `Actor ${nextCastSlot(actors)}`, position: [...point.position], yaw: Math.atan2(delta.x, delta.z), color: "#e7e9e5", trigger: "start", delay }; | |
| await persist({ type: "create", actor }); | |
| selected = actor.id; | |
| onSelect(selected); | |
| placing = false; | |
| cursor.visible = facingArrow.visible = false; | |
| emit(authoring ? "Mannequin placed. Select it and describe a basic motion; no motion import is needed." : actor.motionId ? `T-Pose actor placed · ${preset.name} assigned. Use the left grip to play or select another motion.` : "Mannequin placed. Select it to request a generated motion."); | |
| return message; | |
| } | |
| function select(id) { | |
| selected = id; | |
| onSelect(id); | |
| const actor = selectedActor(); | |
| if (actor) emit(authoring ? `${actor.name} selected. Describe a basic motion to the Agent, or draw a curve to guide this actor.` : `${actor.name} selected: ${assets.get(actorMotionId(actor))?.name || "No motion assigned"}. Choose a motion, move the actor, ask it to face you, or adjust its entrance delay. A style request changes the whole cast.`); | |
| } | |
| function updateRay(raycaster) { | |
| const point = pickGround(raycaster); | |
| target = lockedTarget ? structuredClone(lockedTarget) : point ? { position: point.toArray(), viewer: viewPosition().toArray(), spatialKey: spatialKey() } : null; | |
| cursor.visible = !!target && (authoring ? placing : placing || !!lockedTarget || deferNewMotion() && !getState().scene.actors?.length && canEdit()); | |
| facingArrow.visible = cursor.visible; | |
| if (target) { | |
| cursor.position.fromArray(target.position).add(new THREE.Vector3(0, 0.015, 0)); | |
| try { | |
| const yaw = facingViewer(target.position, target.viewer); | |
| facingArrow.position.copy(cursor.position); | |
| facingArrow.setDirection(new THREE.Vector3(Math.sin(yaw), 0, Math.cos(yaw))); | |
| } catch { | |
| cursor.visible = facingArrow.visible = false; | |
| } | |
| } | |
| } | |
| function beginPlacement(delay = 0) { | |
| requirePlacement(); | |
| placing = true; | |
| placementDelay = delay; | |
| target = null; | |
| cursor.visible = facingArrow.visible = false; | |
| emit(authoring ? "Point at the ground to place the actor · A Create · B Cancel" : "Point at the floor and press the right trigger to place an actor."); | |
| return message; | |
| } | |
| async function confirmPlacement(raycaster) { | |
| if (!placing) return false; | |
| updateRay(raycaster); | |
| if (!target) throw Error("Point at visible ground before confirming placement"); | |
| return create(structuredClone(target), placementDelay); | |
| } | |
| function interceptRay(raycaster) { | |
| if (getPhase() !== "explore") return false; | |
| if (placing) { | |
| if (authoring) { | |
| updateRay(raycaster); | |
| emit("A Create actor · B Cancel", false); | |
| } else confirmPlacement(raycaster).catch((e) => emit(e.message, false)); | |
| return true; | |
| } | |
| const hit = layer.pick(raycaster); | |
| if (hit && hit.distance <= occlusionDistance(raycaster)) { | |
| select(hit.id); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function playable(actorIds) { | |
| requireEdit(); | |
| if (!canPlay()) throw new Error("Wait until ready, then say Preview or select the Preview button."); | |
| const all = getState().scene.actors || [], actors = actorIds ? all.filter((a) => actorIds.includes(a.id)) : all; | |
| if (actorIds && actors.length !== new Set(actorIds).size) throw new Error("The door-triggered cast has changed"); | |
| if (!actors.length) throw new Error("Place actors first"); | |
| if (!actors.some((a) => a.motionPlan || actorMotionId(a))) throw new Error("Select an actor and assign a motion first"); | |
| if (actors.some((a) => !assets.has(a.assetId) || actorMotionId(a) && !assets.has(actorMotionId(a)))) throw new Error("Actor motions are not loaded. Playback is unavailable."); | |
| return actors; | |
| } | |
| function start({ actorIds } = {}) { | |
| playable(actorIds); | |
| placing = false; | |
| cursor.visible = false; | |
| performance.start(actorIds); | |
| recordEvent("performance-start", { ...performance.snapshot(), ...actorIds ? { actorIds: [...actorIds] } : {} }); | |
| emit("Playing · Left grip or Stop to pause · Unassigned actors remain in T-Pose"); | |
| } | |
| function arm(actorIds, { cue = "door" } = {}) { | |
| playable(actorIds); | |
| placing = false; | |
| cursor.visible = false; | |
| performance.arm(actorIds); | |
| recordEvent("performance-armed", { ...performance.snapshot(), actorIds: [...actorIds] }); | |
| emit(cue === "preview" ? "Actors are ready at the first frame. Press the left grip to preview." : "Actors are ready at the first frame. Open the door to start them together."); | |
| } | |
| function stop() { | |
| performance.stop(); | |
| recordEvent("performance-stop", performance.snapshot()); | |
| emit("Motion stopped. You can still move, speak, and edit."); | |
| } | |
| function resume() { | |
| requireEdit(); | |
| if (!canPlay()) throw new Error("Confirm Preview first."); | |
| performance.resume(); | |
| recordEvent("performance-resume", performance.snapshot()); | |
| emit("Playing · Left grip to pause"); | |
| } | |
| function toggleTransport() { | |
| const s = performance.snapshot(), done = pathCompleted(s) ?? performance.completed(assets); | |
| if (s.mode === "running" && !done) { | |
| stop(); | |
| return; | |
| } | |
| if (placing) throw new Error(authoring ? "Press A to place the actor or B to cancel placement" : "Place with the right trigger, or press A to cancel"); | |
| if (s.mode === "paused" && !done) resume(); | |
| else start(); | |
| } | |
| function edit() { | |
| performance.edit(); | |
| recordEvent("performance-edit", performance.snapshot()); | |
| emit("Back to T-Pose layout. Assigned motions are kept and restart on the next playback."); | |
| } | |
| async function change(changes, id = selected) { | |
| const actor = getState().scene.actors?.find((a) => a.id === id); | |
| if (!actor) throw new Error("Select an actor first"); | |
| await persist({ type: "update", id: actor.id, changes }); | |
| } | |
| async function assignMotion(motionId, id = selected) { | |
| const actor = getState().scene.actors?.find((a) => a.id === id); | |
| if (!actor) throw new Error("Select an actor first"); | |
| const asset = motionId && assets.get(motionId); | |
| if (motionId && !asset) throw new Error("This motion has not been imported"); | |
| if (asset?.bodyOnly) throw new Error("This is the default character asset. Describe a basic motion to the Agent."); | |
| await change({ motionId }, id); | |
| const nextRun = performance.snapshot().mode !== "editing"; | |
| emit((asset ? `Assigned “${asset.name}” to ${actor.name}.` : `Cleared the motion for ${actor.name}.`) + (nextRun ? "Takes effect on the next playback. Current timing is unchanged." : asset ? "Holding T-Pose. Use the left grip to play." : "Holding T-Pose.")); | |
| return message; | |
| } | |
| function assignPreferredMotion(id = selected) { | |
| const motions = [...assets.values()].filter((a) => !a.bodyOnly), preferred = motions.find((a) => a.preferred) || motions[0]; | |
| if (!preferred) throw new Error("No motion assets imported"); | |
| return assignMotion(preferred.id, id); | |
| } | |
| function nextMotion(id = selected) { | |
| const actor = getState().scene.actors?.find((a) => a.id === id); | |
| if (!actor) throw new Error("Select an actor first"); | |
| const style = actorMotionPresets(assets).find((p) => p.id === actorMotionId(actor))?.style || getState().scene.actorStyle || "zombie", presets = actorMotionPresets(assets, style); | |
| if (!presets.length) return assignPreferredMotion(id); | |
| const index = presets.findIndex((p) => p.id === actorMotionId(actor)); | |
| return assignMotion(presets[(index + 1) % presets.length].id, id); | |
| } | |
| async function setStyle(style, { playback = "play", context = {} } = {}) { | |
| requireEdit(); | |
| if (playback === "play" && !canPlay()) throw new Error("Confirm Preview or prepare the motions again."); | |
| const initial = getState(), space = spatialKey(); | |
| if (context.revision !== void 0 && context.revision !== initial.revision || context.spatialContext?.spatialKey && context.spatialContext.spatialKey !== space) throw new Error("Scene or alignment changed since you spoke. Submit the style request again."); | |
| changeCastStyle(initial.scene, style, [...assets.keys()]); | |
| const previous = performance.snapshot(), offsets = {}; | |
| for (const actor of initial.scene.actors) { | |
| const view = layer.views.get(actor.id); | |
| if (view?.visible && previous.mode !== "editing") { | |
| const root = view.worldToLocal(view.bones[0].getWorldPosition(new THREE.Vector3())); | |
| offsets[actor.id] = [root.x, 0, root.z]; | |
| } | |
| } | |
| performance.stop(); | |
| pending = true; | |
| try { | |
| const next = await api("/api/actor-style", { revision: initial.revision, style }); | |
| acceptState(next); | |
| performance.sync(next.scene.actors); | |
| placing = false; | |
| cursor.visible = false; | |
| if (spatialKey() !== space || getPhase() !== "explore") { | |
| performance.edit(); | |
| emit("Motion style saved. Alignment changed; return to creating before playing."); | |
| return message; | |
| } | |
| if (previous.mode === "running") performance.resume(); | |
| performance.switchMotions(offsets, playback); | |
| recordEvent("actor-style-switch", { style, playback, offsets, ...performance.snapshot(), actors: next.scene.actors.map((a) => ({ id: a.id, slot: a.castSlot, motionId: actorMotionId(a) })) }); | |
| emit(`Changed ${next.scene.actors.length} actors to ${STYLE_LABELS[style]} motions. ${playback === "assign" ? "Takes effect on the next playback." : playback === "preserve" && previous.mode !== "running" ? "Paused / layout state retained. Left grip to play." : "Playing from the start. Left grip to pause."}`); | |
| return message; | |
| } catch (error) { | |
| if (previous.mode === "running" && getPhase() === "explore" && spatialKey() === space) performance.resume(); | |
| throw error; | |
| } finally { | |
| pending = false; | |
| } | |
| } | |
| async function command(text, context = {}) { | |
| const intent = parseDirectorCommand(text); | |
| if (!intent) return null; | |
| if (intent.type === "style") return setStyle(intent.style, { context }); | |
| if (intent.type === "create") return authoring ? beginPlacement(intent.delay) : create(context.actorTarget, intent.delay); | |
| if (intent.type === "start") { | |
| start(); | |
| return message; | |
| } | |
| if (intent.type === "stop") { | |
| stop(); | |
| return message; | |
| } | |
| if (intent.type === "resume") { | |
| resume(); | |
| return message; | |
| } | |
| if (intent.type === "edit") { | |
| edit(); | |
| return message; | |
| } | |
| if (intent.type === "unavailable") return "Choose an available motion from the asset list. Additional imported motions are required for unsupported actions."; | |
| if (intent.type === "motion") { | |
| if (!context.actorId) throw new Error("Select an actor first"); | |
| if (intent.number) { | |
| const preset = actorMotionPresets(assets, getState().scene.actorStyle || "zombie").find((p) => p.number === intent.number); | |
| if (!preset) throw new Error(`Motion ${intent.number} has not loaded`); | |
| return assignMotion(preset.id, context.actorId); | |
| } | |
| return assignPreferredMotion(context.actorId); | |
| } | |
| if (intent.type === "clearMotion") return assignMotion(null, context.actorId); | |
| if (intent.type === "save") { | |
| await api("/api/save-scene", { revision: getState().revision }).then(acceptState); | |
| emit("Scene, actor positions, and entry timing saved."); | |
| return message; | |
| } | |
| if (intent.type === "delay") { | |
| await change({ delay: intent.delay }, context.actorId); | |
| emit(`Will appear ${intent.delay} seconds after the next playback starts.`); | |
| return message; | |
| } | |
| if (intent.type === "move") { | |
| await change({ position: [...validTarget(context.actorTarget).position] }, context.actorId); | |
| emit("Actor moved to the selected point. Motion timing is unchanged."); | |
| return message; | |
| } | |
| if (intent.type === "face") { | |
| const actor = getState().scene.actors?.find((a) => a.id === context.actorId); | |
| if (!actor) throw new Error("Select an actor first"); | |
| const p = context.actorViewer || viewPosition().toArray(); | |
| await change({ yaw: Math.atan2(p[0] - actor.position[0], p[2] - actor.position[2]) }, actor.id); | |
| emit("Actor now faces your current position."); | |
| return message; | |
| } | |
| if (intent.type === "remove") { | |
| if (!context.actorId) throw new Error("Select an actor first"); | |
| const name = getState().scene.actors?.find((a) => a.id === context.actorId)?.name || "Actor"; | |
| await persist({ type: "remove", id: context.actorId }); | |
| selected = null; | |
| emit(`${name} removed and saved · Left stick click to undo`); | |
| return message; | |
| } | |
| return null; | |
| } | |
| return { | |
| lockPlacement(anchor) { | |
| validTarget(anchor); | |
| lockedTarget = structuredClone(anchor); | |
| }, | |
| unlockPlacement() { | |
| lockedTarget = null; | |
| cursor.visible = facingArrow.visible = false; | |
| }, | |
| command, | |
| setStyle, | |
| start, | |
| arm, | |
| stop, | |
| resume, | |
| toggleTransport, | |
| edit, | |
| select, | |
| change, | |
| assignMotion, | |
| assignPreferredMotion, | |
| nextMotion, | |
| updateRay, | |
| interceptRay, | |
| beginPlacement, | |
| confirmPlacement, | |
| cancelPlacement() { | |
| placing = false; | |
| target = null; | |
| cursor.visible = facingArrow.visible = false; | |
| }, | |
| togglePlacement() { | |
| if (placing) { | |
| placing = false; | |
| target = null; | |
| cursor.visible = facingArrow.visible = false; | |
| emit("Placement closed. Click the right stick to toggle editing."); | |
| } else beginPlacement(); | |
| }, | |
| clearTarget() { | |
| target = null; | |
| cursor.visible = facingArrow.visible = false; | |
| }, | |
| captureContext() { | |
| return { actorTarget: target ? structuredClone(target) : null, actorViewer: viewPosition().toArray(), actorId: selected }; | |
| }, | |
| previewGenerated(id) { | |
| performance.start([id]); | |
| }, | |
| freeze(ids) { | |
| performance.arm(ids); | |
| }, | |
| sync(scene) { | |
| performance.sync(scene.actors || []); | |
| if (selected && !scene.actors?.some((a) => a.id === selected)) selected = null; | |
| }, | |
| frame(dt, opacity, { showSelection = true, selectedIds = null, overrides = [] } = {}) { | |
| performance.advance(dt); | |
| const changes = new Map(overrides.map((o) => [o.id, o])); | |
| frames = mapFrames(performance.frames(assets), performance.snapshot()).map((f) => changes.has(f.id) ? { ...f, ...changes.get(f.id) } : f); | |
| layer.apply(frames, { opacity, selected: showSelection ? selectedIds || selected : null }); | |
| return frames; | |
| }, | |
| snapshot() { | |
| return { placementPreview: { visible: cursor.visible, arrowVisible: facingArrow.visible, locked: !!lockedTarget, target: target ? structuredClone(target) : null }, style: getState()?.scene.actorStyle || "zombie", ...performance.snapshot(), completed: pathCompleted(performance.snapshot()) ?? performance.completed(assets), selected, placing, pending, message, actors: frames.map((f) => ({ id: f.id, assetId: f.assetId, motionId: f.motionId, pose: f.pose, castSlot: f.castSlot, motionOffset: f.motionOffset, position: f.position, yaw: f.yaw, delay: f.delay, clipTime: f.clipTime, visible: f.visible, preview: f.preview })), assetCount: assets.size }; | |
| }, | |
| frames: () => frames, | |
| layer | |
| }; | |
| } | |
| export { | |
| createDirector | |
| }; | |