EmboDi / src /shared /group-path.mjs
xlt99's picture
Upload 180 files
970fc3e verified
Raw History Blame Contribute Delete
5.21 kB
import { actorMotionId } from "./actors.mjs";
import { floorAt } from "./cast-layout.mjs";
import { pathLength } from "./draft.mjs";
const point = (p) => Array.isArray(p) && p.length === 3 && p.every((n) => Number.isFinite(n) && Math.abs(n) <= 100);
const turn = (p, y) => [Math.cos(y) * p[0] + Math.sin(y) * p[2], p[1], -Math.sin(y) * p[0] + Math.cos(y) * p[2]];
const angle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
const actorDesign = (actors) => actors.map((a) => ({ id: a.id, position: [...a.position], yaw: a.yaw, motionId: actorMotionId(a) }));
function makeGroupPath(scene, { actorIds, points, speed = 0.6, id }) {
if (!Array.isArray(actorIds) || !actorIds.length || new Set(actorIds).size !== actorIds.length) throw Error("Select a group of actors");
const actors = actorIds.map((id2) => scene.actors?.find((a) => a.id === id2));
if (actors.some((a) => !a || !actorMotionId(a))) throw Error("Prepare motions for the selected actors first");
if (!Array.isArray(points) || points.length < 2 || points.length > 512 || !points.every(point)) throw Error("Invalid path point");
const lead = actors[0], rawPoints = structuredClone(points), clean = [];
for (const p of points) {
const y = floorAt(scene, p);
if (!Number.isFinite(y) || Math.abs(p[1] - y) > 0.2 || Math.abs(y - lead.position[1]) > 0.2) throw Error("The path must stay on valid floor at one level");
const q = [p[0], lead.position[1], p[2]];
if (!clean.length || Math.hypot(q[0] - clean.at(-1)[0], q[2] - clean.at(-1)[2]) >= 0.025) clean.push(q);
}
if (clean.length < 2 || pathLength(clean) < 0.15 || pathLength(clean) > 30) throw Error("Draw a continuous path between 0.15 and 30 meters");
if (Math.hypot(clean[0][0] - lead.position[0], clean[0][2] - lead.position[2]) > 0.025) clean.unshift([...lead.position]);
else clean[0] = [...lead.position];
if (pathLength(clean) > 35) throw Error("Path starts too far from the group. Start drawing near the actors.");
const path = { id, groupId: "group-" + lead.id, actorIds: [...actorIds], rawPoints, points: clean, speed, initialYaw: lead.yaw, design: actorDesign(actors), offsets: actors.map((a) => ({ id: a.id, position: turn(a.position.map((v, i) => v - lead.position[i]), -lead.yaw), yaw: angle(a.yaw - lead.yaw) })), length: pathLength(clean) };
validateGroupPath(path, scene);
return path;
}
function validateGroupPath(p, scene) {
if (!p || typeof p.id !== "string" || !/^path-[\w-]{1,90}$/.test(p.id) || typeof p.groupId !== "string" || !Number.isFinite(p.speed) || p.speed < 0.1 || p.speed > 2 || !Number.isFinite(p.initialYaw) || !Array.isArray(p.points) || p.points.length < 2 || p.points.length > 513 || !p.points.every(point) || !Array.isArray(p.rawPoints) || p.rawPoints.length > 512 || !p.rawPoints.every(point) || !Number.isFinite(p.length) || Math.abs(pathLength(p.points) - p.length) > 1e-3 || p.length < 0.15 || p.length > 35) throw Error("Invalid group path configuration");
if (!Array.isArray(p.actorIds) || p.actorIds.length < 1 || p.actorIds.length > 5 || new Set(p.actorIds).size !== p.actorIds.length || p.actorIds.some((id) => !scene.actors?.some((a) => a.id === id && actorMotionId(a))) || !Array.isArray(p.offsets) || p.offsets.length !== p.actorIds.length || p.offsets.some((o, i) => o.id !== p.actorIds[i] || !point(o.position) || !Number.isFinite(o.yaw))) throw Error("Path references an invalid actor");
if (!Array.isArray(p.design) || p.design.length !== p.actorIds.length || p.design.some((a, i) => a.id !== p.actorIds[i] || !point(a.position) || !Number.isFinite(a.yaw) || typeof a.motionId !== "string")) throw Error("Invalid initial path position");
}
function at(points, d) {
for (let i = 1; i < points.length; i++) {
const a = points[i - 1], b = points[i], length = Math.hypot(b[0] - a[0], b[2] - a[2]);
if (d <= length || i === points.length - 1) {
const t = Math.min(1, Math.max(0, d / Math.max(length, 1e-5)));
return a.map((v, j) => v + (b[j] - v) * t);
}
d -= length;
}
return points[0];
}
function groupPathPose(path, time) {
const distance = Math.min(path.length, Math.max(0, time) * path.speed), p = at(path.points, distance), a = at(path.points, Math.max(0, distance - 0.25)), b = at(path.points, Math.min(path.length, distance + 0.25));
const target = Math.atan2(b[0] - a[0], b[2] - a[2]), blend = Math.min(1, distance / 0.5), yaw = path.initialYaw + angle(target - path.initialYaw) * blend;
return { position: p, yaw, complete: distance >= path.length };
}
function pathFrames(path, frames, time, assets) {
const pose = groupPathPose(path, time), byId = new Map(path.offsets.map((o) => [o.id, o])), elapsed = Math.min(time, path.length / path.speed);
return frames.map((f) => {
const offset = byId.get(f.id);
if (!offset) return f;
const duration = assets.get(actorMotionId(f))?.duration || 1;
return { ...f, position: turn(offset.position, pose.yaw).map((v, i) => v + pose.position[i]), yaw: pose.yaw + offset.yaw, pose: "motion", preview: false, visible: true, clipTime: elapsed % duration, inPlace: true, motionOffset: [0, 0, 0] };
});
}
export {
actorDesign,
groupPathPose,
makeGroupPath,
pathFrames,
validateGroupPath
};