EmboDi / src /shared /actors.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
10.3 kB
import {languagePattern, numberWords, termWords} from "./language.mjs";
import { validateMotionPlan } from "./authoring-motion.mjs";
import { CAST_SIZE, castSlots } from "./actor-slots.mjs";
import { clearEntityTargets } from "./entity-targets.mjs";
const finite = (x, min, max) => typeof x === "number" && Number.isFinite(x) && x >= min && x <= max;
const idOK = (id) => typeof id === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(id);
const MAX_ACTORS = 7;
const actorMotionId = (actor) => actor.motionId === void 0 ? actor.assetId : actor.motionId;
function validateActor(actor) {
if (!actor || !idOK(actor.id) || !idOK(actor.assetId) || typeof actor.name !== "string" || !actor.name.trim() || actor.name.length > 80) throw new Error("Invalid actor identifier");
if (!Array.isArray(actor.position) || actor.position.length !== 3 || !actor.position.every((x) => finite(x, -100, 100)) || !finite(actor.yaw, -Math.PI * 8, Math.PI * 8)) throw new Error("Invalid actor position or orientation");
if (!finite(actor.delay, 0, 60) || actor.trigger !== "start" || !/^#[0-9a-fA-F]{6}$/.test(actor.color)) throw new Error("Invalid actor entry settings");
if (actor.motionId !== void 0 && actor.motionId !== null && !idOK(actor.motionId)) throw new Error("Invalid actor motion identifier");
if (actor.castSlot !== void 0 && (!Number.isInteger(actor.castSlot) || actor.castSlot < 1 || actor.castSlot > CAST_SIZE)) throw new Error("Invalid actor motion slot");
if (actor.motionOffset !== void 0 && (!Array.isArray(actor.motionOffset) || actor.motionOffset.length !== 3 || !actor.motionOffset.every((x) => finite(x, -100, 100)))) throw new Error("Invalid motion transition offset");
if (actor.motionPlan != null) validateMotionPlan(actor.motionPlan);
return actor;
}
function validateActors(actors) {
if (!Array.isArray(actors) || actors.length > MAX_ACTORS) throw new Error(`A maximum of ${MAX_ACTORS} actors is supported`);
const ids = /* @__PURE__ */ new Set(), slots = /* @__PURE__ */ new Set();
for (const actor of actors) {
validateActor(actor);
if (ids.has(actor.id)) throw new Error("Duplicate actor ID");
ids.add(actor.id);
if (actor.castSlot !== void 0) {
if (slots.has(actor.castSlot)) throw new Error("Duplicate actor motion slot");
slots.add(actor.castSlot);
}
}
return actors;
}
function updateActors(scene, command, assetIds) {
const previous = structuredClone(scene.actors || []), actors = previous.length <= CAST_SIZE ? castSlots(previous) : previous, allowed = new Set(assetIds);
if (command.type === "create") {
if (actors.length >= CAST_SIZE) throw new Error(`This demo supports up to ${CAST_SIZE} actors`);
validateActor(command.actor);
if (!allowed.has(command.actor.assetId) || actorMotionId(command.actor) && !allowed.has(actorMotionId(command.actor))) throw new Error("Motion assets are not ready");
if (actors.some((a) => a.id === command.actor.id) || scene.objects.some((o) => o.id === command.actor.id)) throw new Error("Actor ID already exists");
actors.push(command.actor);
} else {
const index = actors.findIndex((a) => a.id === command.id);
if (index < 0) throw new Error("Select an actor first");
if (command.type === "remove") actors.splice(index, 1);
else if (command.type === "update") {
const permitted = ["position", "yaw", "delay", "color", "name", "assetId", "motionId", "motionPlan"];
if (!command.changes || Object.keys(command.changes).some((k) => !permitted.includes(k))) throw new Error("Invalid actor edit field");
if (command.changes.assetId !== void 0 && !allowed.has(command.changes.assetId)) throw new Error("Motion assets are not ready");
if (command.changes.motionId != null && !allowed.has(command.changes.motionId)) throw new Error("Motion assets are not ready");
actors[index] = { ...actors[index], ...command.changes, ...command.changes.motionId !== void 0 && command.changes.motionPlan === void 0 ? { motionPlan: null } : {} };
validateActor(actors[index]);
} else throw new Error("Invalid actor action");
}
const next = actors.length <= CAST_SIZE ? castSlots(actors) : actors;
validateActors(next);
const result = { ...scene, actors: next };
if (command.type === "remove") result.objects = clearEntityTargets(scene.objects, command.id);
if (scene.doorPerformance) {
const ids = scene.doorPerformance.actorIds.filter((id) => next.some((a) => a.id === id));
if (ids.length) result.doorPerformance = { ...scene.doorPerformance, actorIds: ids, anchorActorId: ids.includes(scene.doorPerformance.anchorActorId) ? scene.doorPerformance.anchorActorId : ids[0] };
else delete result.doorPerformance;
}
return result;
}
function createPerformance() {
let mode = "editing", time = 0, runId = 0, actors = [], schedule = /* @__PURE__ */ new Map();
const prepare = (ids, nextMode) => {
runId++;
time = 0;
mode = nextMode;
const chosen = ids ? new Set(ids) : null;
schedule = new Map(actors.filter((a) => !chosen || chosen.has(a.id)).map((a) => [a.id, { delay: a.delay, assetId: a.assetId, motionId: actorMotionId(a), motionPlan: a.motionPlan }]));
};
return {
sync(next) {
actors = structuredClone(next);
},
start(ids) {
prepare(ids, "running");
},
arm(ids) {
prepare(ids, "paused");
},
switchMotions(offsets = {}, playback = "play") {
if (playback === "assign") return;
const oldMode = mode, oldTime = time, oldSchedule = schedule;
if (playback === "preserve" && oldMode === "editing") return;
runId++;
time = 0;
mode = playback === "preserve" && oldMode === "paused" ? "paused" : "running";
schedule = new Map(actors.map((a) => [a.id, { delay: oldMode === "editing" ? a.delay : Math.max(0, (oldSchedule.get(a.id)?.delay ?? a.delay) - oldTime), assetId: a.assetId, motionId: actorMotionId(a), motionOffset: [...offsets[a.id] || [0, 0, 0]] }]));
},
stop() {
if (mode === "running") mode = "paused";
},
resume() {
if (mode === "paused") mode = "running";
},
edit() {
mode = "editing";
time = 0;
},
advance(dt) {
if (mode === "running" && Number.isFinite(dt) && dt > 0) time += dt;
},
snapshot() {
return { mode, time, runId };
},
completed(assets) {
const active = actors.map((a) => schedule.get(a.id)).filter((c) => c?.motionPlan || c?.motionId);
return mode !== "editing" && active.length > 0 && active.every((c) => time >= c.delay + (c.motionPlan?.duration ?? assets.get(c.motionId)?.duration ?? Infinity));
},
frames(assets) {
return actors.map((a) => {
const cue = schedule.get(a.id), preview = mode === "editing" || !cue;
const visible = preview || !!cue && time >= cue.delay;
const motionId = preview ? actorMotionId(a) : cue.motionId, assetId = motionId || (preview ? a.assetId : cue.assetId), rest = preview || !motionId;
const motionPlan = preview ? a.motionPlan : cue.motionPlan;
if (motionPlan) return { ...a, motionPlan, assetId: preview ? a.assetId : cue.assetId, motionId: null, pose: "generated", preview, visible, runId, clipTime: preview ? motionPlan.duration : Math.min(motionPlan.duration, Math.max(0, time - (cue?.delay || 0))) };
const duration = assets.get(motionId)?.duration || 0;
return { ...a, assetId, motionId, motionOffset: preview ? [0, 0, 0] : [...cue?.motionOffset || [0, 0, 0]], pose: rest ? "rest" : "motion", preview, visible, runId, clipTime: rest ? 0 : Math.min(duration, Math.max(0, time - (cue?.delay || 0))) };
});
}
};
}
function delayValue(text) {
const values = numberWords.actorDelays;
const match = text.match(languagePattern("actor.delaySeconds"));
return match ? Number(values[match[1]] ?? match[1]) : null;
}
function parseDirectorCommand(text) {
const t = text.trim().replace(languagePattern("punctuation.actors.1"), " ").trim();
const groupStyle = t.match(languagePattern("actor.groupStyle"));
if (groupStyle && !languagePattern("punctuation.actors.2").test(text)) return { type: "style", style: groupStyle[1] === termWords.cute ? "cute" : "zombie" };
if (languagePattern("actor.stop").test(t)) return { type: "stop" };
if (languagePattern("actor.start").test(t)) return { type: "start" };
if (languagePattern("actor.resume").test(t)) return { type: "resume" };
if (languagePattern("actor.edit").test(t)) return { type: "edit" };
if (languagePattern("actor.saveScene").test(t)) return { type: "save" };
const style = t.match(languagePattern("actor.style"));
if (style && !languagePattern("punctuation.actors.3").test(text)) return { type: "style", style: [termWords.cute, termWords.birthday].includes(style[1]) ? "cute" : "zombie" };
const actor = languagePattern("actor.subject").test(t), delay = delayValue(t);
if (languagePattern("actor.clearMotion").test(t)) return { type: "clearMotion" };
const numberedMotion = t.match(languagePattern("actor.numberedMotion"));
if (numberedMotion) return { type: "motion", number: Number(numberWords.actorMotionNumbers[numberedMotion[1]] ?? numberedMotion[1]) };
if (languagePattern("actor.motionSubject").test(t) && languagePattern("actor.generatedMotion").test(t)) return { type: "unavailable" };
if (languagePattern("actor.spatialSubject").test(t) && languagePattern("actor.moveVerb").test(t) && languagePattern("actor.here").test(t)) return { type: "move" };
if (actor && languagePattern("actor.createVerb").test(t)) return { type: "create", delay: delay ?? 0 };
if (languagePattern("legacy.lungeMotion").test(t) && languagePattern("legacy.motionAssignmentSubject").test(t) || languagePattern("legacy.assignLungeMotion").test(t)) return { type: "motion" };
if (delay !== null && languagePattern("actor.delayVerb").test(t)) return { type: "delay", delay };
if (languagePattern("actor.spatialSubject").test(t) && languagePattern("actor.faceViewer").test(t)) return { type: "face" };
if (languagePattern("actor.spatialSubject").test(t) && languagePattern("actor.removeVerb").test(t)) return { type: "remove" };
return null;
}
export {
MAX_ACTORS,
actorMotionId,
createPerformance,
parseDirectorCommand,
updateActors,
validateActor,
validateActors
};