cozyclay 3.1.5 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/bin/agent/agent-runner.mjs +9 -4
- package/bin/agent/openai-compatible-provider.mjs +24 -1
- package/dist/app/index.html +5 -5
- package/dist/assets/TaskBoard-BUjjuvm5.js +1 -0
- package/dist/assets/{app-CEP7jzXB.css → app-Dige7q3X.css} +1 -1
- package/dist/assets/app-DxoI941o.js +5144 -0
- package/dist/assets/compile-worker-DRWRa9pr.js +6 -0
- package/dist/assets/{motion3-compile-Du7UXrWn.js → motion3-compile-B4SX4TGx.js} +1 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +13 -0
- package/dist/assets/task-board-5PKn7ZaA.js +15 -0
- package/dist/assets/{workflow-DJwOUzqJ.js → workflow-Dg9jCKOq.js} +3 -3
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +1 -1
- package/dist/workflow/index.html +3 -3
- package/mcp/project-camera.mjs +43 -0
- package/package.json +3 -1
- package/src/App.jsx +5 -5
- package/src/commands/index.js +2 -1
- package/src/commands/physics.js +21 -0
- package/src/domains/motion.js +7 -9
- package/src/motion/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -0
- package/src/motion/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +9 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +28 -3
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- package/src/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/posestudio.jsx +1 -1
- package/tools/run-tests.mjs +9 -16
- package/tools/verify-package-tarball.mjs +29 -1
- package/dist/assets/TaskBoard-DwXhsJXz.js +0 -1
- package/dist/assets/app-CTKcyPM-.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Spring secondary motion for a carried or free object (spec.objects[id].spring = { length, stiffness, damping }): a damped
|
|
2
|
+
// pendulum hung on the object's anchor lags the anchor's horizontal acceleration, swings and settles. Computed once at
|
|
3
|
+
// compile time (object-pose.js rootPoseAt); playback, scrub and export read the baked track only.
|
|
4
|
+
//
|
|
5
|
+
// Determinism: a fixed step dt = 1 / fps from frame 0 on, semi-implicit Euler, and the solver update uses
|
|
6
|
+
// only + - * / and Math.sqrt, so every engine computes the same bits. Only the last step, the swing angles to a turn,
|
|
7
|
+
// goes through the quaternion helpers (quat.js), as every other baked rotation does.
|
|
8
|
+
//
|
|
9
|
+
// State: two swing angles th = [tx, tz] (rad), how far the object's far end swings toward world +X and +Z, and their
|
|
10
|
+
// rates w. Per frame f: a = (p[f] - 2 p[f-1] + p[f-2]) * fps^2 (0 on frames 0 and 1), then
|
|
11
|
+
// w = (w + (-k th - a_h / L) dt) / (1 + c dt) ; th += w dt ; c = 2 d sqrt(k)
|
|
12
|
+
// with k the stiffness (1/s^2), d the damping ratio (0..1, 1 critical) and L the pendulum length (m): spring.length, else
|
|
13
|
+
// half the object's longest side (springLength). Without a stiffness k = G / L, a real pendulum's sqrt(g / L). The damping is
|
|
14
|
+
// implicit, so no damping ratio makes it ring up (an explicit c dt > 1 diverged); the stiffness step stays stable while
|
|
15
|
+
// sqrt(k) dt < 2, and the schema caps k at SPRING.stiffness.max (500) so that sqrt(k) dt <= 1 from 24 fps up.
|
|
16
|
+
import { qexp, qmul, qnorm } from "./quat.js";
|
|
17
|
+
import { quatFromMat, matFromQuat } from "./math.js";
|
|
18
|
+
|
|
19
|
+
/** The schema's ranges and defaults (schema/types.js reads them; validate-objects.js checks them). No stiffness default: G / length. */
|
|
20
|
+
export const SPRING = { length: { min: 0.05, max: 2 }, stiffness: { min: 0, max: 500 }, damping: { min: 0, max: 1, default: 0.5 } };
|
|
21
|
+
/** Gravity, m/s^2 (qa/style.js and skills/impact.js use the same value). */
|
|
22
|
+
const G = 9.81;
|
|
23
|
+
/** The largest swing drawn, rad (60 degrees). */
|
|
24
|
+
const SPRING_MAX = Math.PI / 3;
|
|
25
|
+
/** The shortest pendulum, m: a tiny prop swings as if it were this long. */
|
|
26
|
+
const SPRING_MIN_LENGTH = 0.05;
|
|
27
|
+
|
|
28
|
+
const clampSwing = v => (v > SPRING_MAX ? SPRING_MAX : v < -SPRING_MAX ? -SPRING_MAX : v);
|
|
29
|
+
|
|
30
|
+
/** The resting state: no swing, no anchor history. */
|
|
31
|
+
export const springRest = () => ({ th: [0, 0], w: [0, 0], p1: null, p2: null });
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One frame: the state after anchor position p (world [x, y, z]) is fed to `state` (the state after the frame before).
|
|
35
|
+
* Pure: returns a new state. opts = { fps, stiffness, damping, length }.
|
|
36
|
+
*/
|
|
37
|
+
export function springStep(state, p, { fps, stiffness, damping, length }) {
|
|
38
|
+
const dt = 1 / fps, k = stiffness, c = 2 * damping * Math.sqrt(stiffness), L = length > SPRING_MIN_LENGTH ? length : SPRING_MIN_LENGTH;
|
|
39
|
+
let ax = 0, az = 0;
|
|
40
|
+
if (state.p1 && state.p2) {
|
|
41
|
+
const f2 = fps * fps;
|
|
42
|
+
ax = (p[0] - 2 * state.p1[0] + state.p2[0]) * f2;
|
|
43
|
+
az = (p[2] - 2 * state.p1[2] + state.p2[2]) * f2;
|
|
44
|
+
}
|
|
45
|
+
const wx = (state.w[0] + (-k * state.th[0] - ax / L) * dt) / (1 + c * dt);
|
|
46
|
+
const wz = (state.w[1] + (-k * state.th[1] - az / L) * dt) / (1 + c * dt);
|
|
47
|
+
return { th: [state.th[0] + wx * dt, state.th[1] + wz * dt], w: [wx, wz], p1: [p[0], p[1], p[2]], p2: state.p1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The solver options for spring entry `spring` ({ length?, stiffness?, damping? }) at `fps`: the length given, else
|
|
52
|
+
* `fallback` (springLength of the object); the stiffness given, else G / length; the damping given, else 0.5.
|
|
53
|
+
*/
|
|
54
|
+
export function springOpts(spring, fps, fallback = SPRING_MIN_LENGTH) {
|
|
55
|
+
const length = Math.max(SPRING_MIN_LENGTH, spring?.length ?? fallback);
|
|
56
|
+
return { fps, stiffness: spring?.stiffness ?? G / length, damping: spring?.damping ?? SPRING.damping.default, length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The swing of every frame for anchor positions `anchors` (one world [x, y, z] per frame from frame 0):
|
|
61
|
+
* [[tx, tz], ...] rad, each clamped to +-60 degrees. A still anchor gives 0 on every frame. Defaults as springOpts.
|
|
62
|
+
*/
|
|
63
|
+
export function springAngles({ anchors, fps, stiffness, damping, length }) {
|
|
64
|
+
const opts = springOpts({ stiffness, damping, length }, fps), out = [];
|
|
65
|
+
let state = springRest();
|
|
66
|
+
for (const p of anchors) {
|
|
67
|
+
state = springStep(state, p, opts);
|
|
68
|
+
out.push([clampSwing(state.th[0]), clampSwing(state.th[1])]);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The clamped swing of a state. */
|
|
74
|
+
export const swingOf = state => [clampSwing(state.th[0]), clampSwing(state.th[1])];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pose { pos, R } turned by swing th = [tx, tz] about its own base (position unchanged): +tx swings what hangs below
|
|
78
|
+
* toward +X (a turn about +Z), +tz toward +Z (a turn about -X). The turn is applied in world axes on top of R.
|
|
79
|
+
*/
|
|
80
|
+
export function swungPose(pose, th) {
|
|
81
|
+
if (th[0] === 0 && th[1] === 0) return pose;
|
|
82
|
+
const turn = qexp([-th[1], 0, th[0]]);
|
|
83
|
+
return { ...pose, pos: pose.pos, R: matFromQuat(qnorm(qmul(turn, quatFromMat(pose.R)))) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Pendulum length of a body or group from its boxes [{ pos, R, half }]'s corners: half the longest side of their bounding box, at least 5 cm. */
|
|
87
|
+
export function springLength(corners) {
|
|
88
|
+
if (!corners.length) return SPRING_MIN_LENGTH;
|
|
89
|
+
let longest = 0;
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
const vs = corners.map(c => c[i]);
|
|
92
|
+
longest = Math.max(longest, Math.max(...vs) - Math.min(...vs));
|
|
93
|
+
}
|
|
94
|
+
return Math.max(SPRING_MIN_LENGTH, longest / 2);
|
|
95
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// or from its release on. Fields: schema/fields.js OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS. See validate.js.
|
|
10
10
|
import { isVec3, fmt, didYouMean, GRASPS } from "./vocab.js";
|
|
11
11
|
import { objectOk } from "./validate-common.js";
|
|
12
|
-
import { OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS } from "../schema/fields.js";
|
|
12
|
+
import { OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS, SPRING_FIELDS } from "../schema/fields.js";
|
|
13
|
+
import { SPRING } from "./spring.js";
|
|
13
14
|
import { isSwitch, isFollow, movesRecord } from "../carried.js";
|
|
14
15
|
import { isFree } from "../free-motion.js";
|
|
15
16
|
import { checkRef } from "../refs.js";
|
|
@@ -99,6 +100,18 @@ function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/** objects.<id>.spring: { length?, stiffness?, damping? }, each a number in its SPRING range (a larger stiffness would blow the fixed-step solver up). */
|
|
104
|
+
function validateSpring(err, unknown, at, o) {
|
|
105
|
+
const sp = o.spring, SPR = "{ length?, stiffness?, damping? }";
|
|
106
|
+
if (!sp || typeof sp !== "object" || Array.isArray(sp)) return err(`${at}.spring must be ${SPR}, got ${fmt(sp)}`);
|
|
107
|
+
for (const k of Object.keys(sp)) if (!SPRING_FIELDS.includes(k)) unknown(`${at}.spring has unknown field "${k}"${didYouMean(k, SPRING_FIELDS)}; a spring is ${SPR}`);
|
|
108
|
+
const what = { length: "string or rod length, m", stiffness: "how fast it swings back, 1/s^2", damping: "how soon it settles, 1 = no overshoot" };
|
|
109
|
+
for (const k of SPRING_FIELDS) {
|
|
110
|
+
const v = sp[k], { min, max } = SPRING[k];
|
|
111
|
+
if (v !== undefined && !(Number.isFinite(v) && v >= min && v <= max)) err(`${at}.spring.${k} must be ${min}..${max} (${what[k]}), got ${fmt(v)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
/** A grasp that starts like the given one (4+ letters: "sphere" -> spherical), else the nearest by edit distance. */
|
|
103
116
|
const prefixLen = (a, b) => { let i = 0; while (i < a.length && a[i] === b[i]) i += 1; return i; };
|
|
104
117
|
function graspHint(given) {
|
|
@@ -122,7 +135,8 @@ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, un
|
|
|
122
135
|
const at = `objects.${id}`, kind = o?.prop !== undefined ? "carry" : isFollow(o) ? "follow" : isFree(o) ? "free" : isSwitch(o) ? "switch" : "hinge", fields = OBJECT_FIELDS[kind];
|
|
123
136
|
// a switch and a follow take any record (a light, an effect); the others a body or a group
|
|
124
137
|
if ((kind !== "switch" && kind !== "follow") || !scene?.others?.[id]) objectOk(err, scene, at, id, { groups: true });
|
|
125
|
-
|
|
138
|
+
if (kind === "hinge" && o?.spring !== undefined) err(`${at}.spring: spring is not for hinged objects; a hinge turns only by its channel (give spring to a carry or a free entry)`);
|
|
139
|
+
for (const f of Object.keys(o ?? {})) if (!fields.includes(f) && !(kind === "hinge" && (f === "keys" || f === "fall" || f === "spring"))) unknown(`${at} (${kind}) has unknown field "${f}"${didYouMean(f, fields)}; fields: ${fields.join(", ")}`);
|
|
126
140
|
massOk(err, at, o);
|
|
127
141
|
if (kind === "follow") {
|
|
128
142
|
validateFollow(spec, err, { sk, scene }, at, o);
|
|
@@ -137,6 +151,7 @@ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, un
|
|
|
137
151
|
if (kind === "free") {
|
|
138
152
|
if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames, true);
|
|
139
153
|
if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: false });
|
|
154
|
+
if (o.spring !== undefined) validateSpring(err, unknown, at, o);
|
|
140
155
|
channelsOk(err, at, o);
|
|
141
156
|
continue;
|
|
142
157
|
}
|
|
@@ -149,6 +164,7 @@ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, un
|
|
|
149
164
|
&& o.grab !== undefined && o.release !== undefined && !(o.grab < o.release)) err(`${at}: grab (${o.grab}) must come before release (${o.release})`);
|
|
150
165
|
if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames);
|
|
151
166
|
if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: true });
|
|
167
|
+
if (o.spring !== undefined) validateSpring(err, unknown, at, o);
|
|
152
168
|
continue;
|
|
153
169
|
}
|
|
154
170
|
if (o?.keys !== undefined || o?.fall !== undefined) err(`${at}: a hinge turns by its channel "${o.channel}", keyed in spec.keys; keys and fall move an object with no pivot, axis or channel (C17), or a carried one (prop)`);
|
|
@@ -114,6 +114,7 @@ function validateChecks(spec, err, { sk, scene, frames, unknown = err, past = ""
|
|
|
114
114
|
if (fix !== undefined && fix !== false && (typeof fix !== "object" || fix === null)) err(`spec.fix must be false or { ${FIX_KEYS.join(", ")} }`);
|
|
115
115
|
else for (const k of Object.keys(fix || {})) if (!FIX_KEYS.includes(k)) unknown(`spec.fix.${k} is not a fix${didYouMean(k, FIX_KEYS)} (fixes: ${FIX_KEYS.join(", ")})`);
|
|
116
116
|
if (fix && fix.grasp !== undefined && typeof fix.grasp !== "boolean") err(`spec.fix.grasp must be true (fingers close on what a hand holds, the default) or false (the preset grip, the object at its grab-frame offset), got ${fmt(fix.grasp)}`);
|
|
117
|
+
if (fix && fix.person !== undefined && typeof fix.person !== "boolean") err(`spec.fix.person must be true (arms kept out of the other characters, the default) or false, got ${fmt(fix.person)}`);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/** opts = { sk, scene, frames, lenient, unknown: err for an unknown field (a no-op when lenient), past: the
|
|
@@ -27,7 +27,7 @@ const RANGES = [
|
|
|
27
27
|
[/^wflex_[lr]$/, -70, 70, "flex about the knuckle line, deg"],
|
|
28
28
|
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch, deg: + lifts the heel (toes flat), - lifts the toes"],
|
|
29
29
|
// the whole-body pitch and roll have none: they turn in full (flips, cartwheels; pose.js tiltFrame)
|
|
30
|
-
[/^(spine_pitch|head_pitch)$/, -120, 120, "
|
|
30
|
+
[/^(spine_pitch|head_pitch)$/, -120, 120, "deg, + bends forward"],
|
|
31
31
|
[/^(spine_roll|head_roll)$/, -90, 90, "degrees"],
|
|
32
32
|
];
|
|
33
33
|
export const rangeOf = ch => RANGES.find(([re]) => re.test(ch)) ?? null;
|
package/src/motion3/fix/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the baked channels or the per-frame IK offsets and re-solves only the frames it touched, so QA
|
|
3
3
|
// still measures the final frames; every fix reports what it changed (counts, worst amount).
|
|
4
4
|
//
|
|
5
|
-
// spec.fix = { reach: true, slide: true, penetration: 0.02, self: true, retime: false } (false = all off)
|
|
5
|
+
// spec.fix = { reach: true, slide: true, penetration: 0.02, self: true, retime: false, person: true } (false = all off)
|
|
6
6
|
// reach wrist targets past the arm's reach go onto it through a soft knee (no snap);
|
|
7
7
|
// ankle targets past the leg's reach lower the hips just enough, eased in and out; a hand
|
|
8
8
|
// on a pinned target (a world or a scene object's anchor) that the straight arm misses brings
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// retime speed spikes get more frames, borrowed from the neighbouring keys
|
|
19
19
|
// stance a standIdle stance whose legs, feet or hips are in a scene body slides out of it, the least
|
|
20
20
|
// distance (skills/stance.js; applied when the skills expand, reported in observations.placed)
|
|
21
|
+
// person arms out of the other characters (scene.people, their installed takes; QA PERSON): the wrist
|
|
22
|
+
// target moves out by the depth, back the side the hand came from, eased in and out (person.js)
|
|
21
23
|
// grasp a holding hand's fingers close on what it holds and the object rests in the hand (#816);
|
|
22
24
|
// false keeps the preset grip and the grab-frame offset
|
|
23
25
|
import { lowerHips, armReach } from "./reach.js";
|
|
@@ -28,15 +30,16 @@ import { liftSoles } from "./ground.js";
|
|
|
28
30
|
import { pushOut } from "./push.js";
|
|
29
31
|
import { retime } from "./retime.js";
|
|
30
32
|
import { selfClear } from "./self.js";
|
|
33
|
+
import { personOut } from "./person.js";
|
|
31
34
|
|
|
32
|
-
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance", "grasp"];
|
|
35
|
+
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance", "grasp", "person"];
|
|
33
36
|
export { FIX_KEYS };
|
|
34
37
|
|
|
35
38
|
export function fixOptions(fix) {
|
|
36
|
-
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false, grasp: false };
|
|
39
|
+
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false, grasp: false, person: false };
|
|
37
40
|
const f = fix ?? {};
|
|
38
41
|
const pen = f.penetration === true ? 0.02 : Math.min(0.05, Math.max(0, Number(f.penetration) || 0));
|
|
39
|
-
return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true, stance: f.stance !== false, grasp: f.grasp !== false };
|
|
42
|
+
return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true, stance: f.stance !== false, grasp: f.grasp !== false, person: f.person !== false };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/** The slide report with the flat feet whose turn waited (turn.js): turns { frames, max deg }. */
|
|
@@ -55,6 +58,7 @@ export function applyFixes(out, fix, engine, { whole = true } = {}) {
|
|
|
55
58
|
if (fix.reach && fixed.slide?.pivots) put("reachLegs", mergeReach(fixed.reachLegs, lowerHips(out, engine)));
|
|
56
59
|
if (fix.penetration > 0) put("penetration", pushOut(out, engine, fix.penetration));
|
|
57
60
|
if (fix.self) put("self", selfClear(out, engine));
|
|
61
|
+
if (fix.person) put("person", personOut(out, engine));
|
|
58
62
|
if (fix.reach) put("reachArms", armReach(out));
|
|
59
63
|
return fixed;
|
|
60
64
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Person push-out (spec.fix.person, on by default): an arm capsule (upper arm, forearm, hand) inside another character
|
|
2
|
+
// (scene.people, their installed take; qa/people-checks.js PERSON) moves its wrist IK target out by the depth plus a margin,
|
|
3
|
+
// eased over neighbouring frames, the same way push.js takes a hand out of a box. No depth limit: a hand keyed deep into
|
|
4
|
+
// someone comes out too. The escape keeps the side the hand came from: a normal that points the opposite way from the
|
|
5
|
+
// previous frame's (the first frame of a pass: from their hips to mine, level), or away from my side of them (that level
|
|
6
|
+
// line), is replaced by the previous one, so a hand past their axis is pulled back the way it came instead of out through
|
|
7
|
+
// their back (the previous frame alone lets the normal turn a little each frame until it points through them). Fixed passes and fixed order (people by id, their
|
|
8
|
+
// capsules in CAPSULES order), no time or random cut-off: the same spec gives the same take. Frames where that arm is FK
|
|
9
|
+
// (armw 0) cannot be pushed and are counted as skipped. An intended touch (checks.allow { part, object: "person:<id>",
|
|
10
|
+
// depth }) may go that deep.
|
|
11
|
+
import { add, sub, scale, dot, len, clamp } from "../compiler/math.js";
|
|
12
|
+
import { QA_LIMITS, allowance } from "../qa/checks.js";
|
|
13
|
+
import { CAPSULES, segPoints } from "../qa/geometry.js";
|
|
14
|
+
import { personCapsules } from "../qa/people-checks.js";
|
|
15
|
+
import { round4 } from "./ease.js";
|
|
16
|
+
|
|
17
|
+
const EASE = 4, MARGIN = 0.004, PASSES = 6, APART = 2.2;
|
|
18
|
+
const ARM_PARTS = { l: CAPSULES.filter(c => /^(uarm|farm|hand)_l$/.test(c[0])), r: CAPSULES.filter(c => /^(uarm|farm|hand)_r$/.test(c[0])) };
|
|
19
|
+
|
|
20
|
+
/** The closest points of segment PQ (sampled as qa/geometry.js segDist does, so the depth is QA's) and segment CD. */
|
|
21
|
+
function closest(P, Q, C, D) {
|
|
22
|
+
const d2 = sub(D, C), dd = dot(d2, d2);
|
|
23
|
+
let best = null;
|
|
24
|
+
for (const a of segPoints(P, Q)) {
|
|
25
|
+
const u = dd < 1e-12 ? 0 : clamp(dot(sub(a, C), d2) / dd, 0, 1), b = add(C, scale(d2, u)), d = len(sub(a, b));
|
|
26
|
+
if (!best || d < best.d) best = { a, b, d };
|
|
27
|
+
}
|
|
28
|
+
return best;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Level unit vector from a to b (their hips to mine), or null when they are on top of each other. */
|
|
32
|
+
function level(a, b) {
|
|
33
|
+
const v = [b[0] - a[0], 0, b[2] - a[2]], l = len(v);
|
|
34
|
+
return l > 1e-6 ? scale(v, 1 / l) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The bodies to keep out of: [id, person] sorted by id, only those whose take has every capsule joint. */
|
|
38
|
+
function bodies(out) {
|
|
39
|
+
return Object.entries(out.ctx?.scene?.people ?? {}).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
40
|
+
.filter(([, p]) => CAPSULES.every(([, a, b]) => p.names.includes(a) && p.names.includes(b)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Per-frame push vectors for side k: the escape (side-kept normal * (depth past what is allowed + margin)) summed over people. */
|
|
44
|
+
function needs(out, k, people, lim, allow, stats) {
|
|
45
|
+
const { sk } = out.ctx, I = sk.index, n = out.frames.length, f0 = out.f0 ?? 0, v = Array.from({ length: n }, () => null);
|
|
46
|
+
const prev = new Map();
|
|
47
|
+
for (let i = 0; i < n; i++) {
|
|
48
|
+
const { s, c } = out.frames[i];
|
|
49
|
+
let sum = null;
|
|
50
|
+
for (const [id, p] of people) {
|
|
51
|
+
const theirs = personCapsules(p, i + f0), hips = theirs.find(t => t[0] === "hips")[1];
|
|
52
|
+
if (len(sub(s.P[I.Hips], hips)) > APART) continue;
|
|
53
|
+
let best = null;
|
|
54
|
+
for (const [part, a, b, r] of ARM_PARTS[k]) {
|
|
55
|
+
const meant = allowance(allow, part, `person:${id}`), limit = meant ?? lim, A = s.P[I[a]], B = s.P[I[b]];
|
|
56
|
+
for (const [, C, D, q] of theirs) {
|
|
57
|
+
const hit = closest(A, B, C, D), depth = r + q - hit.d;
|
|
58
|
+
if (depth > limit && (!best || depth - (meant ?? 0) > best.need)) best = { need: depth - (meant ?? 0), hit };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!best) continue;
|
|
62
|
+
const side = level(hips, s.P[I.Hips]) ?? [0, 0, 0], ref = prev.get(id) ?? side;
|
|
63
|
+
let nrm = best.hit.d > 1e-6 ? scale(sub(best.hit.a, best.hit.b), 1 / best.hit.d) : ref;
|
|
64
|
+
if (dot(nrm, ref) < 0 || dot(nrm, side) < 0) nrm = ref;
|
|
65
|
+
prev.set(id, nrm);
|
|
66
|
+
sum = add(sum ?? [0, 0, 0], scale(nrm, best.need + MARGIN));
|
|
67
|
+
}
|
|
68
|
+
if (!sum) continue;
|
|
69
|
+
if (!(c[`armw_${k}`] > 1e-4)) { stats.skipped++; continue; }
|
|
70
|
+
v[i] = sum;
|
|
71
|
+
}
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function personOut(out, engine) {
|
|
76
|
+
const people = bodies(out);
|
|
77
|
+
if (!people.length) return null;
|
|
78
|
+
const { checks = {} } = out.ctx.spec, lim = { ...QA_LIMITS, ...checks.limits }.allow, n = out.frames.length, stats = { skipped: 0 };
|
|
79
|
+
const hit = new Set();
|
|
80
|
+
let max = 0, passes = 0;
|
|
81
|
+
for (let pass = 0; pass < PASSES; pass++) {
|
|
82
|
+
stats.skipped = 0;
|
|
83
|
+
const touched = new Set();
|
|
84
|
+
for (const k of ["l", "r"]) {
|
|
85
|
+
const v = needs(out, k, people, lim, checks.allow, stats), push = out.ctx.push[k];
|
|
86
|
+
// eased dilation of the vectors: neighbours get the strongest nearby push, scaled down (push.js)
|
|
87
|
+
const eased = Array.from({ length: n }, () => null);
|
|
88
|
+
for (let i = 0; i < n; i++) {
|
|
89
|
+
if (!v[i]) continue;
|
|
90
|
+
hit.add(`${k}${i}`);
|
|
91
|
+
for (let j = Math.max(0, i - EASE - 1); j <= Math.min(n - 1, i + EASE + 1); j++) {
|
|
92
|
+
const t = 1 - Math.abs(i - j) / (EASE + 1), w = scale(v[i], t * t * (3 - 2 * t));
|
|
93
|
+
if (!eased[j] || len(w) > len(eased[j])) eased[j] = w;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (let i = 0; i < n; i++) {
|
|
97
|
+
if (!eased[i]) continue;
|
|
98
|
+
const f = out.f0 + i, next = add(push.get(f) ?? [0, 0, 0], eased[i]);
|
|
99
|
+
push.set(f, next);
|
|
100
|
+
max = Math.max(max, len(next));
|
|
101
|
+
touched.add(f);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!touched.size) break;
|
|
105
|
+
passes++;
|
|
106
|
+
engine.solve([...touched]);
|
|
107
|
+
}
|
|
108
|
+
if (!hit.size && !stats.skipped) return null;
|
|
109
|
+
return { frames: hit.size, max: round4(max), passes, skipped: stats.skipped, how: "hand pushed out of the other person, back the way it came" };
|
|
110
|
+
}
|
|
@@ -25,6 +25,7 @@ import { curveOf, keyedPose } from "./object-keys.js";
|
|
|
25
25
|
import { add, apply, sub, T, quatFromMat, I3 } from "./compiler/math.js";
|
|
26
26
|
import { shapeOf, airPath, settlePath, settledAt, spinOf, spinEnergy, TIP } from "./settle.js";
|
|
27
27
|
import { fallVelocity, moves, flightTime, drift, landingWith } from "./flight.js";
|
|
28
|
+
import { springRest, springStep, swingOf, swungPose, springLength, springOpts } from "./compiler/spring.js";
|
|
28
29
|
|
|
29
30
|
/** Gravity, m/s^2. */
|
|
30
31
|
export const G = 9.8;
|
|
@@ -138,24 +139,28 @@ function eventsOf(o, map, from = -Infinity, to = Infinity) {
|
|
|
138
139
|
return events;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
/**
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The pose events give at mapped frame f, starting from pose `start`, and whether f is on or past a fall's landing with no
|
|
144
|
+
* key lifting it since (landed: it rests and hangs from nothing): { pose, landed }. `keep` caches each fall's plan on its event.
|
|
145
|
+
*/
|
|
146
|
+
function walkLanded(events, f, start, plan, keep = true) {
|
|
147
|
+
let pose = start, landed = false;
|
|
144
148
|
for (const e of events) {
|
|
145
149
|
if (e.run) {
|
|
146
150
|
const { t } = e.curve;
|
|
147
|
-
if (f < t[0]) return pose;
|
|
148
|
-
if (f <= t[t.length - 1]) return keyedPose(e.curve, f);
|
|
149
|
-
pose = keyedPose(e.curve, t[t.length - 1]);
|
|
151
|
+
if (f < t[0]) return { pose, landed };
|
|
152
|
+
if (f <= t[t.length - 1]) return { pose: keyedPose(e.curve, f), landed: false };
|
|
153
|
+
pose = keyedPose(e.curve, t[t.length - 1]); landed = false;
|
|
150
154
|
} else {
|
|
151
|
-
if (f < e.fall.t) return pose;
|
|
155
|
+
if (f < e.fall.t) return { pose, landed };
|
|
152
156
|
const p = keep ? (e.plan ??= plan(pose, e.fall)) : plan(pose, e.fall);
|
|
153
|
-
if (f < p.end) return p.at(f);
|
|
154
|
-
pose = p.rest;
|
|
157
|
+
if (f < p.end) return { pose: p.at(f), landed: f >= p.land };
|
|
158
|
+
pose = p.rest; landed = true;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
|
-
return pose;
|
|
161
|
+
return { pose, landed };
|
|
158
162
|
}
|
|
163
|
+
const walk = (events, f, start, plan, keep = true) => walkLanded(events, f, start, plan, keep).pose;
|
|
159
164
|
|
|
160
165
|
const timelines = new WeakMap();
|
|
161
166
|
/** The events of entry o under time map `map`, cached per scene, entry and span (key curves built once). */
|
|
@@ -182,6 +187,34 @@ export function freePoseAt(ctx, id, f) {
|
|
|
182
187
|
return walk(timelineOf(ctx, id, o, ctx.timeMap, "all"), f, { pos: rest.pos, R: rest.R }, planner(ctx, id));
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
const swings = new WeakMap();
|
|
191
|
+
/**
|
|
192
|
+
* freePoseAt turned by the entry's spring (spec.objects[id].spring, compiler/spring.js), else freePoseAt: the pendulum
|
|
193
|
+
* walks frames 0..f of the unswung pose and stops dead on a fall's landing (landedAt), cached per spec, entry and time map. The one swing of a free entry: the take and
|
|
194
|
+
* refs (object-pose.js rootPoseAt) and the obstacle boxes QA and fixes read (compiler/compile.js objectTracks).
|
|
195
|
+
*/
|
|
196
|
+
export function freeSwungAt(ctx, id, f) {
|
|
197
|
+
const pose = freePoseAt(ctx, id, f), o = ctx.spec.objects?.[id];
|
|
198
|
+
if (!pose || !o?.spring) return pose;
|
|
199
|
+
if (!swings.has(ctx.spec)) swings.set(ctx.spec, new Map());
|
|
200
|
+
const cache = swings.get(ctx.spec);
|
|
201
|
+
let c = cache.get(id);
|
|
202
|
+
if (!c || c.map !== ctx.timeMap || c.o !== o) cache.set(id, c = { map: ctx.timeMap, o, states: [], length: springLength(partsIn(ctx.scene, id).flatMap(p => corners(ctx.scene.objects[p]))) });
|
|
203
|
+
const opts = springOpts(o.spring, ctx.spec.fps ?? 30, c.length);
|
|
204
|
+
for (let k = c.states.length; k <= f; k++) {
|
|
205
|
+
const s = springStep(k ? c.states[k - 1] : springRest(), (k === f ? pose : freePoseAt(ctx, id, k)).pos, opts);
|
|
206
|
+
// landed, it rests on its face and hangs from nothing: no swing, and none carried on (its corners stay on the surface)
|
|
207
|
+
c.states[k] = landedAt(ctx, id, k) ? { ...s, th: [0, 0], w: [0, 0] } : s;
|
|
208
|
+
}
|
|
209
|
+
return swungPose(pose, swingOf(c.states[f]));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** True when free entry `id` at absolute f is on or past a fall's landing and no key has lifted it since (walkLanded). */
|
|
213
|
+
function landedAt(ctx, id, f) {
|
|
214
|
+
const rest = restOf(ctx.scene, id);
|
|
215
|
+
return walkLanded(timelineOf(ctx, id, ctx.spec.objects[id], ctx.timeMap, "all"), f, { pos: rest.pos, R: rest.R }, planner(ctx, id)).landed;
|
|
216
|
+
}
|
|
217
|
+
|
|
185
218
|
/** Where carry `id` (entry o) is before its grab at absolute f: its scene pose, or where a fall before grab left it. */
|
|
186
219
|
export function beforeGrab(ctx, id, o, f, grab) {
|
|
187
220
|
const rest = restOf(ctx.scene, id);
|
|
@@ -194,10 +227,15 @@ export function beforeGrab(ctx, id, o, f, grab) {
|
|
|
194
227
|
* `beforePose` (a frame before the release) gives the spin it is let go with to a fall that starts right at the release.
|
|
195
228
|
*/
|
|
196
229
|
export function afterRelease(ctx, id, o, f, release, releasePose, beforePose = null) {
|
|
230
|
+
return releaseFall(ctx, id, o, f, release, releasePose, beforePose)?.pose ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** afterRelease and whether f is on or past a landing of those falls (walkLanded): { pose, landed }, or null as afterRelease. */
|
|
234
|
+
export function releaseFall(ctx, id, o, f, release, releasePose, beforePose = null) {
|
|
197
235
|
const events = fallsOf(o).length ? timelineOf({ ...ctx, release }, id, o, ctx.timeMap, "after") : [];
|
|
198
236
|
// the release pose comes from the solved body, which a rebake changes: plan these falls afresh
|
|
199
237
|
const plan = (start, fall) => fallPlan(ctx.spec, ctx.scene, id, start, fall.t, fall, ctx.spec.fps ?? 30, fall.t <= release + 1 ? beforePose : null);
|
|
200
|
-
return events.length ?
|
|
238
|
+
return events.length ? walkLanded(events, f, releasePose, plan, false) : null;
|
|
201
239
|
}
|
|
202
240
|
|
|
203
241
|
/** True when carry entry o falls at or after its release (absolute frame; afterRelease then moves it). */
|
|
@@ -32,8 +32,9 @@ import { groupsOf } from "./scene.js";
|
|
|
32
32
|
import { solveFrame } from "./compiler/compile.js";
|
|
33
33
|
import { channelsAt } from "./compiler/channels.js";
|
|
34
34
|
import { resolveBone } from "./bones.js";
|
|
35
|
-
import { isFree, freePoseAt, beforeGrab, afterRelease, fallsAfter, corners, topOf, under, TOUCH } from "./free-motion.js";
|
|
35
|
+
import { isFree, freePoseAt, freeSwungAt, beforeGrab, afterRelease, releaseFall, fallsAfter, corners, topOf, under, TOUCH } from "./free-motion.js";
|
|
36
36
|
import { add } from "./compiler/math.js";
|
|
37
|
+
import { springRest, springStep, swingOf, swungPose, springLength, springOpts } from "./compiler/spring.js";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Body ids that make up `id` in a motionScene: [id] for a body (plus any bodies parented under
|
|
@@ -161,12 +162,53 @@ function carriedAt(out, id, o, f) {
|
|
|
161
162
|
* solved frame, so the carrying hand must already be solved (compiler/anchors.js).
|
|
162
163
|
*/
|
|
163
164
|
export function rootPoseAt(out, id, f, now = null) {
|
|
165
|
+
const o = out.ctx.spec.objects?.[id];
|
|
166
|
+
if (o?.spring && isFree(o)) return freeSwungAt(freeCtx(out), id, f);
|
|
167
|
+
const pose = basePoseAt(out, id, f, now);
|
|
168
|
+
return o?.spring && pose && isCarried(o) ? swungPose(pose, springAt(out, id, o, f, now ? pose : null)) : pose;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const springs = new WeakMap();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The swing [tx, tz] of spring carry `id` at absolute f (compiler/spring.js): the solver walks frames 0..f of the
|
|
175
|
+
* record's unswung pose (its anchor). Each frame's state is cached with the solved frame it was read on, and reused
|
|
176
|
+
* while frames 0..k are still those frames (a fix that re-solves a frame starts the walk again there). `nowPose`: the
|
|
177
|
+
* unswung pose of frame f while it is being solved (not cached).
|
|
178
|
+
*/
|
|
179
|
+
function springAt(out, id, o, f, nowPose) {
|
|
180
|
+
if (!springs.has(out)) springs.set(out, new Map());
|
|
181
|
+
const cache = springs.get(out), f0 = out.f0 ?? 0, fps = out.fps ?? out.ctx.spec.fps ?? 30, { scene } = out.ctx;
|
|
182
|
+
if (!cache.has(id)) cache.set(id, { states: [], keys: [], length: springLength(partsOf(scene, id).flatMap(p => corners(scene.objects[p]))) });
|
|
183
|
+
const c = cache.get(id), opts = springOpts(o.spring, fps, c.length), release = spanOf(out, o)[1];
|
|
184
|
+
const end = nowPose ? f - 1 : f;
|
|
185
|
+
let k = 0;
|
|
186
|
+
while (k <= end && k < c.states.length && c.keys[k] === (out.frames[k - f0] ?? null)) k += 1;
|
|
187
|
+
c.states.length = k; c.keys.length = k;
|
|
188
|
+
// let go and fallen (free-motion.js releaseFall): from its landing it rests and hangs from nothing, no swing carried on
|
|
189
|
+
const step = (prev, j, pose) => {
|
|
190
|
+
const fell = j > release ? releaseFall(...releaseArgs(out, id, o, j, release)) : null, s = springStep(prev, (pose ?? fell?.pose ?? basePoseAt(out, id, j)).pos, opts);
|
|
191
|
+
return fell?.landed ? { ...s, th: [0, 0], w: [0, 0] } : s;
|
|
192
|
+
};
|
|
193
|
+
for (; k <= end; k++) {
|
|
194
|
+
c.states[k] = step(k ? c.states[k - 1] : springRest(), k, null);
|
|
195
|
+
c.keys[k] = out.frames[k - f0] ?? null;
|
|
196
|
+
}
|
|
197
|
+
const last = end >= 0 ? c.states[end] : springRest();
|
|
198
|
+
return swingOf(nowPose ? step(last, f, nowPose) : last);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The arguments of free-motion.js afterRelease / releaseFall for carry `id` (entry o) at f past its release frame. */
|
|
202
|
+
const releaseArgs = (out, id, o, f, release) => [freeCtx(out), id, o, f, release, carriedAt(out, id, o, release), release > 0 ? carriedAt(out, id, o, release - 1) : null];
|
|
203
|
+
|
|
204
|
+
/** rootPoseAt before a spring swings it: where the keys and the hand put it (qa/key-checks.js measures the keys on it). */
|
|
205
|
+
export function basePoseAt(out, id, f, now = null) {
|
|
164
206
|
const { spec, scene } = out.ctx, o = movesRecord(spec.objects?.[id]) ? spec.objects[id] : undefined, rest = restPose(scene, id);
|
|
165
207
|
if (isFollow(o)) return followPose(now ? { ...out, f0: f, frames: [now] } : out, o, f);
|
|
166
208
|
if (isCarried(o) && (rest || !keepsOffset(scene, id, o))) {
|
|
167
209
|
const [grab, release] = spanOf(out, o), g = Math.max(grab, Math.min(f, release));
|
|
168
210
|
if (f < grab && rest) return restingAt(out, id, f);
|
|
169
|
-
const after = f > release ? afterRelease(
|
|
211
|
+
const after = f > release ? afterRelease(...releaseArgs(out, id, o, f, release)) : null;
|
|
170
212
|
if (after) return after;
|
|
171
213
|
const by = f > release ? riderOf(out, id) : null;
|
|
172
214
|
if (by) { const at = carriedAt(out, id, o, release); return compose(rootPoseAt(out, by, f), relative(rootPoseAt(out, by, release), at)); }
|
package/src/motion3/people.js
CHANGED
|
@@ -8,6 +8,8 @@ import { skeletonFor } from "./rigs/index.js";
|
|
|
8
8
|
import { fk } from "./compiler/skeleton.js";
|
|
9
9
|
import { add, apply, ry } from "./compiler/math.js";
|
|
10
10
|
import { resolveBone } from "./bones.js";
|
|
11
|
+
import { CAPSULES } from "./qa/geometry.js";
|
|
12
|
+
import { APART } from "./qa/people-checks.js";
|
|
11
13
|
|
|
12
14
|
const sampled = new WeakMap();
|
|
13
15
|
// joints are kept to 1e-5 m: what a saved take keeps of them (keptPeople) is then exactly what its compile read
|
|
@@ -61,8 +63,10 @@ export const peopleList = scene => Object.entries(scene?.people ?? {}).map(([id,
|
|
|
61
63
|
* What a take keeps of the people so that its saved source compiles the same again without the others' takes
|
|
62
64
|
* (take-source.js): only the joints its person anchors and refs name (anywhere in the spec: anchors, a skill's
|
|
63
65
|
* define, goals, probes; a bare person:<id> in a skill's from is that person's Hips), as plain arrays (a saved project is JSON), or null.
|
|
66
|
+
* When fix.person moved an arm (out.fixed.person.frames), every capsule joint (qa/geometry.js CAPSULES) of each person whose
|
|
67
|
+
* hips came within APART of mine is kept too, so the saved source pushes the same arm out again (fix/person.js).
|
|
64
68
|
*/
|
|
65
|
-
export function keptPeople(scene, spec) {
|
|
69
|
+
export function keptPeople(scene, spec, out) {
|
|
66
70
|
const want = new Map(), keep = (id, b) => {
|
|
67
71
|
const p = scene?.people?.[id], bone = p && resolveBone(skeletonFor(p.rig), b).bone;
|
|
68
72
|
if (bone) want.set(id, new Set([...(want.get(id) ?? []), bone]));
|
|
@@ -73,6 +77,17 @@ export function keptPeople(scene, spec) {
|
|
|
73
77
|
else if (v && typeof v === "object") { if (v.kind === "person") keep(v.person, v.bone); for (const [k, x] of Object.entries(v)) walk(x, k); }
|
|
74
78
|
};
|
|
75
79
|
walk(spec);
|
|
80
|
+
if (out?.fixed?.person?.frames > 0) {
|
|
81
|
+
const H = out.ctx.sk.index.Hips, f0 = out.f0 ?? 0, joints = CAPSULES.flatMap(([, a, b]) => [a, b]);
|
|
82
|
+
for (const [id, p] of Object.entries(scene?.people ?? {})) {
|
|
83
|
+
const j = p.names.indexOf(CAPSULES.find(c => c[0] === "hips")[1]), J = p.names.length;
|
|
84
|
+
const near = j >= 0 && out.frames.some(({ s }, i) => {
|
|
85
|
+
const at = (Math.max(0, Math.min(p.frames - 1, i + f0)) * J + j) * 3, h = s.P[H];
|
|
86
|
+
return Math.hypot(h[0] - p.P[at], h[1] - p.P[at + 1], h[2] - p.P[at + 2]) <= APART;
|
|
87
|
+
});
|
|
88
|
+
if (near) for (const b of joints) if (p.names.includes(b)) keep(id, b);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
if (!want.size) return null;
|
|
77
92
|
return Object.fromEntries([...want].map(([id, bones]) => {
|
|
78
93
|
const p = scene.people[id], names = [...bones], J = p.names.length, P = [];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The frames QA measured, rebuilt from an installed take v3 (take.js bakeTake), for the physics overlay
|
|
2
|
+
// (physics-view.js). A take keeps local bone turns and the hips in the character's placement frame, plus world
|
|
3
|
+
// tracks of the objects it moves; QA read the compiler's `out`: world joint positions per frame, the skeleton and
|
|
4
|
+
// the obstacle boxes per frame. takeOut gives that shape back, so the overlay calls QA's own functions (qa/com.js,
|
|
5
|
+
// qa/ground.js, qa/style.js) on the pose the viewport plays, and its verdicts are QA's.
|
|
6
|
+
// frames[f].s { P, R }: fk of the take's frame f, turned by the placement yaw and moved to its position
|
|
7
|
+
// objectsAt(f) the authored records as motion boxes (scene.js), the way compile.js objectTracks lays them out:
|
|
8
|
+
// a record a carry or a follow moves is not an obstacle; one a free or hinge entry moves stands
|
|
9
|
+
// where the take's track puts its mover
|
|
10
|
+
// Pure: the take and the records in, numbers out; cached per take and records.
|
|
11
|
+
import { skeletonFor } from "./rigs/index.js";
|
|
12
|
+
import { fk } from "./compiler/skeleton.js";
|
|
13
|
+
import { add, apply, mul, ry, matFromQuat } from "./compiler/math.js";
|
|
14
|
+
import { motionScene, groupsOf } from "./scene.js";
|
|
15
|
+
import { isCarried, isFollow, movesRecord, compose, relative } from "./carried.js";
|
|
16
|
+
import { expandUse } from "./skills/index.js";
|
|
17
|
+
|
|
18
|
+
const cache = new WeakMap();
|
|
19
|
+
|
|
20
|
+
/** The take's spec with its skills expanded (the checks and objects QA read), or the spec as stored when it cannot expand. */
|
|
21
|
+
function expandedSpec(take, sk, scene) {
|
|
22
|
+
try { return expandUse(take.spec ?? {}, { sk, scene, lenient: true }).spec; } catch { return take.spec ?? {}; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** World pose { pos, R } of track `track` at frame f, or null when it has none there. */
|
|
26
|
+
export function trackPose(track, f) {
|
|
27
|
+
if (!track?.pos || !track.quat || f < 0 || (f + 1) * 3 > track.pos.length) return null;
|
|
28
|
+
const p = track.pos, q = track.quat;
|
|
29
|
+
return { pos: [p[f * 3], p[f * 3 + 1], p[f * 3 + 2]], R: matFromQuat([q[f * 4], q[f * 4 + 1], q[f * 4 + 2], q[f * 4 + 3]]) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The obstacle boxes of frame f (compile.js objectTracks obstacles, the movers read from the take's tracks). */
|
|
33
|
+
function obstaclesAt(take, spec, scene, f) {
|
|
34
|
+
const out = {}, groups = groupsOf(scene);
|
|
35
|
+
for (const [id, o] of Object.entries(scene.objects)) {
|
|
36
|
+
const mover = [id, ...(o.ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])), entry = spec.objects?.[mover];
|
|
37
|
+
if (!entry) { out[id] = o; continue; }
|
|
38
|
+
if (isCarried(entry) || isFollow(entry)) continue;
|
|
39
|
+
const pose = trackPose(take.objects?.[mover], f), rest = scene.objects[mover] ?? groups[mover];
|
|
40
|
+
out[id] = pose && rest ? { ...o, ...compose(pose, relative(rest, o)) } : o;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* takeOut(take, objects) -> { frames, ctx: { sk, spec, scene }, objectsAt, fps, f0, frameCount } | null: the `out`
|
|
47
|
+
* QA reads, from take v3 `take` over the authored records `objects`. null for another take or a body motion3 cannot
|
|
48
|
+
* drive. spec is the take's spec with its skills expanded.
|
|
49
|
+
*/
|
|
50
|
+
export function takeOut(take, objects = []) {
|
|
51
|
+
if (take?.schema !== "cozyclay.take.v3" || !(take.frameCount > 0)) return null;
|
|
52
|
+
const kept = cache.get(take);
|
|
53
|
+
if (kept && kept.objects === objects) return kept.out;
|
|
54
|
+
const sk = skeletonFor(take.rig);
|
|
55
|
+
if (!sk) return null;
|
|
56
|
+
const B = take.bones.length, slot = take.bones.map(name => sk.index[name]), yaw = ry(take.placement?.yaw ?? 0), at = take.placement?.position ?? [0, 0, 0];
|
|
57
|
+
const frames = [];
|
|
58
|
+
for (let f = 0; f < take.frameCount; f++) {
|
|
59
|
+
const quats = sk.names.map(() => [0, 0, 0, 1]);
|
|
60
|
+
for (let b = 0; b < B; b++) if (slot[b] !== undefined) quats[slot[b]] = Array.from(take.rot.subarray((f * B + b) * 4, (f * B + b) * 4 + 4));
|
|
61
|
+
const local = fk(sk, quats, Array.from(take.hips.subarray(f * 3, f * 3 + 3)));
|
|
62
|
+
frames.push({ s: { P: local.P.map(p => p && add(at, apply(yaw, p))), R: local.R.map(R => R && mul(yaw, R)) } });
|
|
63
|
+
}
|
|
64
|
+
const scene = motionScene(objects), spec = expandedSpec(take, sk, scene), boxes = new Map();
|
|
65
|
+
const objectsAt = f => {
|
|
66
|
+
if (!boxes.has(f)) boxes.set(f, obstaclesAt(take, spec, scene, Math.max(0, Math.min(take.frameCount - 1, f))));
|
|
67
|
+
return boxes.get(f);
|
|
68
|
+
};
|
|
69
|
+
const out = { frames, ctx: { sk, spec, scene }, objectsAt, fps: take.fps ?? 24, f0: 0, frameCount: take.frameCount };
|
|
70
|
+
cache.set(take, { objects, out });
|
|
71
|
+
return out;
|
|
72
|
+
}
|