cozyclay 3.0.0 → 3.0.2
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 +57 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/agent/agent-routes.mjs +3 -3
- package/bin/agent/agent-runner.mjs +5 -5
- package/bin/agent/pi-tools.mjs +12 -1
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +99 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-DbKG0NgD.js +6 -0
- package/dist/assets/app-34i-iUy1.js +5088 -0
- package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
- package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
- package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
- package/dist/assets/{landing-DjHD8u7k.js → landing-mq5bnqFE.js} +1 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
- package/dist/assets/workflow-dUhurYUc.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/workflow/index.html +3 -3
- package/package.json +1 -1
- package/src/App.jsx +1 -1
- package/src/agent-progress.js +3 -2
- package/src/agent-turn-mark.js +33 -0
- package/src/app-context.js +7 -2
- package/src/command-bus.js +10 -11
- package/src/commands/elements/character.js +2 -1
- package/src/commands/elements/motion.js +7 -1
- package/src/commands/elements/scene.js +3 -0
- package/src/commands/elements/stage.js +2 -1
- package/src/commands/elements/take-read.js +31 -0
- package/src/commands/elements.js +16 -2
- package/src/commands/motion3.js +15 -6
- package/src/commands/objects.js +2 -2
- package/src/commands/shot.js +6 -2
- package/src/domains/motion/take-source.js +4 -2
- package/src/draft-ghost.js +3 -2
- package/src/motion/beat-board-model.js +1 -1
- package/src/motion/beat-board.jsx +2 -1
- package/src/motion3/compile-job.js +4 -0
- package/src/motion3/compiler/anchors.js +68 -4
- package/src/motion3/compiler/compile.js +5 -1
- package/src/motion3/compiler/grip.js +8 -2
- package/src/motion3/compiler/limbs.js +35 -8
- package/src/motion3/compiler/pose.js +17 -2
- package/src/motion3/compiler/socket-grip.js +107 -0
- package/src/motion3/compiler/validate-objects.js +3 -2
- package/src/motion3/compiler/validate-parts.js +9 -2
- package/src/motion3/compiler/validate.js +3 -3
- package/src/motion3/compiler/vocab.js +23 -16
- package/src/motion3/describe.js +5 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/free-motion.js +54 -15
- package/src/motion3/object-keys.js +36 -5
- package/src/motion3/object-pose.js +39 -6
- package/src/motion3/people.js +82 -0
- package/src/motion3/qa/checks.js +6 -2
- package/src/motion3/qa/hints-contact.js +17 -1
- package/src/motion3/qa/index.js +6 -4
- package/src/motion3/qa/people-checks.js +37 -0
- package/src/motion3/qa/style.js +14 -4
- package/src/motion3/refs.js +10 -1
- package/src/motion3/scene.js +4 -3
- package/src/motion3/schema/fields.js +4 -4
- package/src/motion3/schema/topics.js +9 -6
- package/src/motion3/schema/types.js +11 -9
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +10 -3
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +3 -1
- package/src/motion3/skills/info.js +19 -4
- package/src/motion3/sockets.js +104 -0
- package/src/motion3/take-edit.js +1 -1
- package/src/motion3/take.js +6 -4
- package/src/object-catalog.jsx +3 -1
- package/src/object-grasp.js +38 -0
- package/src/object-look.js +2 -0
- package/src/object-travel.js +6 -3
- package/src/panels/ObjectLookPanel.jsx +24 -3
- package/src/props.jsx +5 -5
- package/src/qa-label.js +25 -0
- package/src/scene-objects.js +19 -11
- package/src/shell/details-selection.js +2 -1
- package/src/studio-actions.js +4 -0
- package/src/studio-agent-protocol.js +11 -12
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +3 -1
- package/src/studio-error-codes.js +8 -1
- package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
- package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
- package/src/text/text-object.jsx +106 -0
- package/src/text/text-record.js +122 -0
- package/src/workflow/AgentProgressCard.jsx +6 -4
- package/tools/run-tests.mjs +2 -2
- package/dist/assets/analytics-C9sUnndd.js +0 -1
- package/dist/assets/app-CCh--1Hc.js +0 -5087
- package/dist/assets/compile-worker-CkxiD6lH.js +0 -5
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +0 -20
- package/dist/assets/workflow-C4yKH1p4.js +0 -179
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// origin) at frame f, on the curve held keys use (object-keys.js keyedPose, C14). aim turns the
|
|
5
5
|
// scene pose's +Y onto aim, then roll degrees about it. Before the first key of a run it stays
|
|
6
6
|
// where it was (its scene pose, or where it landed), after the last it holds the last key.
|
|
7
|
-
// fall { at, onto?, bounce? } or a list from where it is at frame `at` it
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the
|
|
7
|
+
// fall { at, onto?, bounce?, tumble? } or a list from where it is at frame `at` it falls under gravity
|
|
8
|
+
// (G m/s^2) onto the top of the highest still body under the centre of its parts (onto: that object's
|
|
9
|
+
// top; nothing below: the floor, y 0) and comes to rest on a stable face (settle.js): it lands on its
|
|
10
|
+
// lowest point, turning at the spin it was let go with, rolls flat and over an edge onto a lower face
|
|
11
|
+
// while the drop has the energy (a phone falls flat, a can lies down). tumble: false drops it straight
|
|
12
|
+
// down as it was, bouncing at most once (bounce: the restitution 0..0.6, default 0.25, never higher
|
|
13
|
+
// than BOUNCE_MAX m); one that lands on a stable face as it falls bounces the same way, and one that
|
|
14
|
+
// rolls spends its landing on the roll and does not bounce (bounce is not read for it).
|
|
11
15
|
// Keys and falls chain in frame order: a fall starts from the pose the keys before it leave (a key on the
|
|
12
16
|
// fall's own frame comes first), keys after a fall take over at their first frame.
|
|
13
17
|
// A carry (C3) takes fall too: before its grab (the hand finds it where it landed) or from its release on.
|
|
@@ -18,13 +22,16 @@
|
|
|
18
22
|
import { isCarried, compose, relative, movesRecord } from "./carried.js";
|
|
19
23
|
import { groupsOf } from "./scene.js";
|
|
20
24
|
import { curveOf, keyedPose } from "./object-keys.js";
|
|
21
|
-
import { add, apply, sub, T, quatFromMat } from "./compiler/math.js";
|
|
25
|
+
import { add, apply, sub, T, quatFromMat, I3 } from "./compiler/math.js";
|
|
26
|
+
import { shapeOf, airPath, settlePath, settledAt, spinOf, spinEnergy, TIP } from "./settle.js";
|
|
22
27
|
|
|
23
28
|
/** Gravity, m/s^2. */
|
|
24
29
|
export const G = 9.8;
|
|
25
30
|
/** The highest a bounce goes, metres. */
|
|
26
31
|
export const BOUNCE_MAX = 0.03;
|
|
27
|
-
const BOUNCE = 0.25
|
|
32
|
+
const BOUNCE = 0.25;
|
|
33
|
+
/** A support's top may sit this far above the lowest point of what rests on it (m). */
|
|
34
|
+
export const TOUCH = 0.005;
|
|
28
35
|
const HELD_OR_HINGED = ["prop", "follow", "pivot", "axis", "channel"];
|
|
29
36
|
|
|
30
37
|
/** A free entry (C17): no prop, follow or hinge field, and keys or fall. */
|
|
@@ -36,14 +43,14 @@ const restOf = (scene, id) => scene?.objects?.[id] ?? groupsOf(scene)[id] ?? nul
|
|
|
36
43
|
const partsIn = (scene, id) => Object.keys(scene?.objects ?? {}).filter(k => k === id || scene.objects[k].ancestors?.includes(id));
|
|
37
44
|
|
|
38
45
|
/** The 8 corners of a base-pivot box { pos, R, half }. */
|
|
39
|
-
function corners(box) {
|
|
46
|
+
export function corners(box) {
|
|
40
47
|
const out = [];
|
|
41
48
|
for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) out.push(add(box.pos, apply(box.R, [x * box.half[0], y * box.half[1], z * box.half[2]])));
|
|
42
49
|
return out;
|
|
43
50
|
}
|
|
44
|
-
const topOf = box => Math.max(...corners(box).map(p => p[1]));
|
|
51
|
+
export const topOf = box => Math.max(...corners(box).map(p => p[1]));
|
|
45
52
|
/** True when the vertical through x/z point p crosses the footprint of box. */
|
|
46
|
-
const under = (box, p) => { const q = apply(T(box.R), sub([p[0], box.pos[1] + box.half[1], p[2]], box.pos)); return Math.abs(q[0]) <= box.half[0] && Math.abs(q[2]) <= box.half[2]; };
|
|
53
|
+
export const under = (box, p) => { const q = apply(T(box.R), sub([p[0], box.pos[1] + box.half[1], p[2]], box.pos)); return Math.abs(q[0]) <= box.half[0] && Math.abs(q[2]) <= box.half[2]; };
|
|
47
54
|
|
|
48
55
|
/** Bodies a spec.objects entry moves (itself or a group above it): they hold nothing up. */
|
|
49
56
|
function moversIn(spec, scene) {
|
|
@@ -76,9 +83,31 @@ export function landingOf(spec, scene, id, start, onto = null) {
|
|
|
76
83
|
return { ...best, h: Math.max(0, low - best.top) };
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
const shapes = new WeakMap();
|
|
87
|
+
/** The shape of `id` in its root frame (settle.js), once per scene. */
|
|
88
|
+
function shapeIn(scene, id) {
|
|
89
|
+
if (!shapes.has(scene)) shapes.set(scene, new Map());
|
|
90
|
+
const cache = shapes.get(scene);
|
|
91
|
+
if (!cache.has(id)) cache.set(id, shapeOf(boxesAtRoot(scene, id, { pos: [0, 0, 0], R: I3() })));
|
|
92
|
+
return cache.get(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A fall from root pose `start` at (mapped) frame t0: its timing, its pose at a frame, where it rests. `before` is its pose
|
|
97
|
+
* a frame earlier (a release from the hand), for the spin it is let go with.
|
|
98
|
+
*/
|
|
99
|
+
function fallPlan(spec, scene, id, start, t0, fall, fps, before = null) {
|
|
100
|
+
const land = landingOf(spec, scene, id, start, fall.onto ?? null), shape = fall.tumble === false ? null : shapeIn(scene, id);
|
|
101
|
+
if (shape) {
|
|
102
|
+
const spin = before ? spinOf(before.R, start.R, fps) : [0, 0, 0], air = airPath(shape, start, spin, land.top, fps);
|
|
103
|
+
// a spin tips it the way its top moves (spin x up); a spin about the vertical tips nothing
|
|
104
|
+
const path = settlePath(shape, air.at(air.tl), land.top, TIP * land.h + spinEnergy(shape, spin), [-spin[2], 0, spin[0]]);
|
|
105
|
+
if (path.steps.length || spin.some(v => v)) {
|
|
106
|
+
const at = f => { const t = (f - t0) / fps; return t <= 0 ? start : t <= air.tl ? air.at(t) : settledAt(path, t - air.tl); };
|
|
107
|
+
return { t0, land: t0 + air.tl * fps, end: t0 + (air.tl + path.time) * fps, h: land.h, support: land.support, top: land.top, at, rest: path.rest };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const h = land.h, tl = Math.sqrt(2 * h / G);
|
|
82
111
|
const e = Math.min(0.6, Math.max(0, fall.bounce ?? BOUNCE)), vb = Math.min(e * G * tl, Math.sqrt(2 * G * BOUNCE_MAX)), tb = 2 * vb / G;
|
|
83
112
|
const rest = { pos: [start.pos[0], start.pos[1] - h, start.pos[2]], R: start.R };
|
|
84
113
|
const at = f => {
|
|
@@ -157,11 +186,21 @@ export function beforeGrab(ctx, id, o, f, grab) {
|
|
|
157
186
|
return walk(timelineOf({ ...ctx, grab }, id, o, ctx.timeMap, "before"), f, { pos: rest.pos, R: rest.R }, planner(ctx, id));
|
|
158
187
|
}
|
|
159
188
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Where carry `id` (entry o) is at absolute f after its release, from its release pose; null without a fall from the release on.
|
|
191
|
+
* `beforePose` (a frame before the release) gives the spin it is let go with to a fall that starts right at the release.
|
|
192
|
+
*/
|
|
193
|
+
export function afterRelease(ctx, id, o, f, release, releasePose, beforePose = null) {
|
|
162
194
|
const events = fallsOf(o).length ? timelineOf({ ...ctx, release }, id, o, ctx.timeMap, "after") : [];
|
|
163
195
|
// the release pose comes from the solved body, which a rebake changes: plan these falls afresh
|
|
164
|
-
|
|
196
|
+
const plan = (start, fall) => fallPlan(ctx.spec, ctx.scene, id, start, fall.t, fall, ctx.spec.fps ?? 30, fall.t <= release + 1 ? beforePose : null);
|
|
197
|
+
return events.length ? walk(events, f, releasePose, plan, false) : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** True when carry entry o falls at or after its release (absolute frame; afterRelease then moves it). */
|
|
201
|
+
export function fallsAfter(ctx, o, release) {
|
|
202
|
+
const map = ctx.timeMap ?? (f => f);
|
|
203
|
+
return fallsOf(o).some(fl => map(fl.at) >= release);
|
|
165
204
|
}
|
|
166
205
|
|
|
167
206
|
/** The falls of entry `id` as planned under the current time map, for readers (timeline, tests): [{ t0, land, end, h, support, top }]. */
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// between keys a cubic Hermite on pos and the same in rotation-vector space on rot (squad
|
|
5
5
|
// like), Catmull-Rom tangents through inner keys, zero at the first and last key
|
|
6
6
|
// around the span the hand blends from the plain carry into the keyed span over KEY_BLEND frames
|
|
7
|
-
// (inside grab..release; smoothstep), so nothing jumps
|
|
7
|
+
// (inside grab..release; smoothstep), so nothing jumps; keys that start within KEY_BLEND frames
|
|
8
|
+
// of the grab with a grip off where the hand is turn the hand onto it before (holdHook approach)
|
|
8
9
|
// the object leads the hand: compile.js asks holdHook for the palm frame the keys put the grip
|
|
9
10
|
// at, palm = key o attach^-1, and pose.js solves the hand to it (compiler/hold.js)
|
|
10
11
|
// The object itself is still drawn in the hand (object-pose.js): the residual is what the arm
|
|
@@ -20,11 +21,14 @@ import { polePlan, smoothPath, smoothed } from "./hold-plan.js";
|
|
|
20
21
|
import { secondGrip, secondHold, gripGate } from "./second-hand.js";
|
|
21
22
|
import { SIDES } from "./compiler/pose.js";
|
|
22
23
|
import { chooseGrip, between, thinBody } from "./grip-choice.js";
|
|
24
|
+
import { graspedBy } from "./compiler/socket-grip.js";
|
|
23
25
|
|
|
24
26
|
/** Frames over which the hand blends into and out of the keyed span. */
|
|
25
27
|
export const KEY_BLEND = 8;
|
|
26
28
|
// the frames a hand takes to turn into a chosen grip before its grab and out of it after its release (grip-choice.js)
|
|
27
29
|
const GRAB_EASE = 12;
|
|
30
|
+
// how far (m, deg) the grip keys put the hand on at the grab may be from where the reach brought it before the hand turns onto it first
|
|
31
|
+
const OFF_GRIP = { m: 0.01, deg: 5 };
|
|
28
32
|
|
|
29
33
|
const smooth = x => x * x * (3 - 2 * x);
|
|
30
34
|
/** [grab, release] in absolute frames of `out` (object-pose.js spanOf, kept here to stay out of its import cycle). */
|
|
@@ -108,8 +112,8 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
108
112
|
if (!held.length) return null;
|
|
109
113
|
const palmOf = (o, s) => carriedPose(sk, spec, s, { prop: o.prop }), attached = new WeakMap(), attaching = new Set();
|
|
110
114
|
const restAt = (id, f) => ctx.restingAt?.(id, f) ?? scene?.objects?.[id] ?? groupsOf(scene)[id];
|
|
111
|
-
// a keyed carry of a thin body (a cup, a can, a bar) whose grip is free (no at/along, no orient: false, no
|
|
112
|
-
// the grab) keeps the grip its keyed path holds most comfortably (grip-choice.js), solved on the plain frames with the
|
|
115
|
+
// a keyed carry of a thin body (a cup, a can, a bar) whose grip is free (no at/along, no orient: false, no grasp fit, no keyed
|
|
116
|
+
// wrist at the grab) keeps the grip its keyed path holds most comfortably (grip-choice.js), solved on the plain frames with the
|
|
113
117
|
// body read at rest; a box keeps the grip grip.js gives it at the grab
|
|
114
118
|
const choices = new WeakMap(), mapped = f => (out.timeMap ? Math.round(out.timeMap(f)) : f);
|
|
115
119
|
// chosen once per compile (the first bake): the fixes' rebakes move the body a little, the carry's keys not at all
|
|
@@ -118,7 +122,7 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
118
122
|
const cache = choices.get(out), o = spec.objects[id], k = spec.props?.[o.prop]?.hand, grab = spanIn(out, o)[0], b = out.baked ?? {};
|
|
119
123
|
if (cache.has(id)) return cache.get(id);
|
|
120
124
|
cache.set(id, null);
|
|
121
|
-
const free = k && o.at === undefined && o.along === undefined && (o.grab !== undefined || !scene?.objects?.[id]) && thinBody(scene, id) && !(spec.fits ?? []).some(fit => fit.side === k && fit.orient === false)
|
|
125
|
+
const free = k && o.at === undefined && o.along === undefined && (o.grab !== undefined || !scene?.objects?.[id]) && thinBody(scene, id) && !(spec.fits ?? []).some(fit => fit.side === k && fit.orient === false) && !graspedBy(spec, scene, id, k)
|
|
122
126
|
&& !["wtwist", "wflex"].some(ch => Math.abs(b[`${ch}_${k}`]?.[grab] ?? 0) > 1e-3)
|
|
123
127
|
// keys that start after the grab: the body rides the hand from where it rests, which only today's grip meets exactly
|
|
124
128
|
&& mapped(Math.min(...o.keys.map(key => key.f))) <= grab;
|
|
@@ -203,6 +207,33 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
203
207
|
const h = heldAt(id, o, f);
|
|
204
208
|
return { w: 1, palm: h ? { pos: add(scale(grip.pos, 1 - h.w), scale(h.palm.pos, h.w)), R: between(grip.R, h.palm.R, h.w) } : grip, measure, pole };
|
|
205
209
|
};
|
|
210
|
+
// keys that lead from the grab frame, or from fewer than KEY_BLEND frames after it, put the hand on their grip at once (or
|
|
211
|
+
// within those frames: palm = first key o attach^-1). Where that grip is not where the hand would be at the first key (an
|
|
212
|
+
// at/along offset of the agent's choosing: bench session cb2503b5's cup, 13.6 cm in the grab frame; keys two frames after
|
|
213
|
+
// the grab: 7146050f's box), the hand turns onto it over the GRAB_EASE frames up to the first key instead, as onto a chosen
|
|
214
|
+
// grip (gripAround): before the grab with the object at rest, after it carrying the object there. A grip within OFF_GRIP
|
|
215
|
+
// of where the hand would be changes nothing.
|
|
216
|
+
const offGrips = new WeakMap();
|
|
217
|
+
const offGrip = (id, o, grip) => {
|
|
218
|
+
if (!offGrips.has(out.baked)) offGrips.set(out.baked, new Map());
|
|
219
|
+
const cache = offGrips.get(out.baked), had = cache.get(id);
|
|
220
|
+
if (had && had.map === out.timeMap) return had.off;
|
|
221
|
+
attaching.add(id);
|
|
222
|
+
let palm;
|
|
223
|
+
try { palm = palmOf(o, solvePlain(Math.max(spanIn(out, o)[0], Math.round(curveIn(out, id).t[0]))).s); } finally { attaching.delete(id); }
|
|
224
|
+
const turn = relative(palm, grip).R, deg = Math.acos(Math.min(1, Math.max(-1, (turn[0][0] + turn[1][1] + turn[2][2] - 1) / 2))) * 180 / Math.PI;
|
|
225
|
+
const off = Math.hypot(...sub(grip.pos, palm.pos)) > OFF_GRIP.m || deg > OFF_GRIP.deg;
|
|
226
|
+
cache.set(id, { map: out.timeMap, off });
|
|
227
|
+
return off;
|
|
228
|
+
};
|
|
229
|
+
const approach = (id, o, f) => {
|
|
230
|
+
const curve = curveIn(out, id), [grab, release] = spanIn(out, o), t0 = Math.max(grab, Math.round(curve.t[0]));
|
|
231
|
+
if (!(f < t0 && f >= t0 - GRAB_EASE) || t0 - grab >= KEY_BLEND || t0 > release) return null;
|
|
232
|
+
const grip = compose(keyedPose(curve, t0), invert(attach(id)));
|
|
233
|
+
if (!offGrip(id, o, grip)) return null;
|
|
234
|
+
const w = smooth(1 - (t0 - f) / GRAB_EASE);
|
|
235
|
+
return w > 1e-3 ? { w, palm: grip, measure: s => palmOf(o, s), pole: planOf(id, o).get(t0) } : null;
|
|
236
|
+
};
|
|
206
237
|
// a release within KEY_BLEND frames of the last key: the hands finish easing out of the keys after it (weight from the last
|
|
207
238
|
// key), the holding hand from the palm they gave it at the release (the object stays where it was let go), each hand with
|
|
208
239
|
// the elbow pole (and the second hand the roll) planned for the release frame
|
|
@@ -217,7 +248,7 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
217
248
|
// the hand that holds a keyed carry follows its keys
|
|
218
249
|
const lead = (k, f, s, c) => {
|
|
219
250
|
for (const [id, o] of held) {
|
|
220
|
-
const pre = spec.props?.[o.prop]?.hand === k ? gripAround(id, o, f) ?? easeOut(id, o, f) : null;
|
|
251
|
+
const pre = spec.props?.[o.prop]?.hand === k ? gripAround(id, o, f) ?? approach(id, o, f) ?? easeOut(id, o, f) : null;
|
|
221
252
|
if (pre) return pre;
|
|
222
253
|
const h = spec.props?.[o.prop]?.hand === k ? heldAt(id, o, f) : null;
|
|
223
254
|
if (h) return { ...h, pole: planOf(id, o).get(f) };
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// f < grab the scene pose
|
|
15
15
|
// grab <= f <= release follows the prop's hand; without at/along it keeps the transform it had
|
|
16
16
|
// against the palm axes at grab (no jump); at/along pin an offset (a group: its root, C15)
|
|
17
|
-
// f > release frozen at the release pose
|
|
17
|
+
// f > release frozen at the release pose; let go over a body that moves on its own (a seat of a truck the spec
|
|
18
|
+
// drives with keys, a hinged lid), it rides that body's mover from the release on, keeping its world
|
|
19
|
+
// pose at the release (riderOf): a lighter put down in the cab leaves with the truck
|
|
18
20
|
// no grab: in hand from frame 0; no release: in hand to the end (PR #14 behaviour)
|
|
19
21
|
// keys (C14, object-keys.js): the object leads and the hand follows; the object is still drawn
|
|
20
22
|
// in the hand, so what the hand cannot reach shows as a residual (QA)
|
|
@@ -29,7 +31,8 @@ import { groupsOf } from "./scene.js";
|
|
|
29
31
|
import { solveFrame } from "./compiler/compile.js";
|
|
30
32
|
import { channelsAt } from "./compiler/channels.js";
|
|
31
33
|
import { resolveBone } from "./bones.js";
|
|
32
|
-
import { isFree, freePoseAt, beforeGrab, afterRelease } from "./free-motion.js";
|
|
34
|
+
import { isFree, freePoseAt, beforeGrab, afterRelease, fallsAfter, corners, topOf, under, TOUCH } from "./free-motion.js";
|
|
35
|
+
import { add } from "./compiler/math.js";
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Body ids that make up `id` in a motionScene: [id] for a body (plus any bodies parented under
|
|
@@ -68,7 +71,7 @@ export function spanOf(out, o) {
|
|
|
68
71
|
/** Without at/along a grabbed body, and a group, keeps its grab-frame palm offset; at/along pin one (a kit root, C15). */
|
|
69
72
|
const keepsOffset = (scene, id, o) => o.at === undefined && o.along === undefined && ((!scene?.objects?.[id] && !!groupsOf(scene)[id]) || o.grab !== undefined);
|
|
70
73
|
|
|
71
|
-
const solved = new WeakMap(), poses = new WeakMap();
|
|
74
|
+
const solved = new WeakMap(), poses = new WeakMap(), riders = new WeakMap();
|
|
72
75
|
|
|
73
76
|
/** What free-motion.js reads of a compile result: the spec, the scene and the time map. */
|
|
74
77
|
const freeCtx = out => ({ spec: out.ctx.spec, scene: out.ctx.scene, timeMap: out.timeMap });
|
|
@@ -84,8 +87,36 @@ export function restingAt(out, id, f) {
|
|
|
84
87
|
return root && (mover === id ? root : compose(root, relative(restPose(scene, mover), restPose(scene, id))));
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The spec.objects key a carry `id` (entry o) rides after its release: the key that moves the highest body under the
|
|
92
|
+
* record's centre at the release frame, when that key moves on its own (keys, a fall or a hinge; never a hand or a follow)
|
|
93
|
+
* and is not the record itself. null when the carry falls after its release, is never let go, or nothing under it moves.
|
|
94
|
+
*/
|
|
95
|
+
export function riderOf(out, id) {
|
|
96
|
+
if (!riders.has(out)) riders.set(out, new Map());
|
|
97
|
+
const cache = riders.get(out);
|
|
98
|
+
if (cache.has(id)) return cache.get(id);
|
|
99
|
+
const { spec, scene } = out.ctx, o = spec.objects?.[id], rest = restPose(scene, id), parts = partsOf(scene, id);
|
|
100
|
+
let best = null;
|
|
101
|
+
if (isCarried(o) && o.release !== undefined && rest && parts.length) {
|
|
102
|
+
const release = spanOf(out, o)[1];
|
|
103
|
+
if (release < out.frameCount - 1 && !fallsAfter(freeCtx(out), o, release)) {
|
|
104
|
+
const at = carriedAt(out, id, o, release), pts = parts.flatMap(p => corners({ ...compose(at, relative(rest, scene.objects[p])), half: scene.objects[p].half }));
|
|
105
|
+
const low = Math.min(...pts.map(p => p[1])), c = pts.reduce((a, p) => add(a, p), [0, 0, 0]).map(v => v / pts.length);
|
|
106
|
+
for (const [k, body] of Object.entries(scene.objects)) {
|
|
107
|
+
const mover = parts.includes(k) ? null : moverOf(spec, scene, k), entry = spec.objects?.[mover];
|
|
108
|
+
if (!mover || mover === id || isCarried(entry) || isFollow(entry)) continue;
|
|
109
|
+
const box = { ...objectPoseAt(out, k, release), half: body.half }, top = topOf(box);
|
|
110
|
+
if (under(box, c) && top <= low + TOUCH && (!best || top > best.top)) best = { mover, top };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
cache.set(id, best?.mover ?? null);
|
|
115
|
+
return best?.mover ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The solved frame at absolute f: out.frames, or solved once on demand outside a window (or ahead of the frame being solved). */
|
|
119
|
+
export function frameAt(out, f) {
|
|
89
120
|
const own = out.frames[f - (out.f0 ?? 0)];
|
|
90
121
|
if (own) return own;
|
|
91
122
|
if (!solved.has(out)) solved.set(out, new Map());
|
|
@@ -127,8 +158,10 @@ export function rootPoseAt(out, id, f, now = null) {
|
|
|
127
158
|
if (isCarried(o) && (rest || !keepsOffset(scene, id, o))) {
|
|
128
159
|
const [grab, release] = spanOf(out, o), g = Math.max(grab, Math.min(f, release));
|
|
129
160
|
if (f < grab && rest) return restingAt(out, id, f);
|
|
130
|
-
const after = f > release ? afterRelease(freeCtx(out), id, o, f, release, carriedAt(out, id, o, release)) : null;
|
|
161
|
+
const after = f > release ? afterRelease(freeCtx(out), id, o, f, release, carriedAt(out, id, o, release), release > 0 ? carriedAt(out, id, o, release - 1) : null) : null;
|
|
131
162
|
if (after) return after;
|
|
163
|
+
const by = f > release ? riderOf(out, id) : null;
|
|
164
|
+
if (by) { const at = carriedAt(out, id, o, release); return compose(rootPoseAt(out, by, f), relative(rootPoseAt(out, by, release), at)); }
|
|
132
165
|
return now && g === f ? carriedOn(out, id, o, f, now.s) : carriedAt(out, id, o, g);
|
|
133
166
|
}
|
|
134
167
|
if (!rest) return null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The other people in a compile: every other character with an installed take v3, sampled once per take frame into
|
|
2
|
+
// world joint positions (the take's local rotations by FK, at the character's stage placement, as playback draws it).
|
|
3
|
+
// scene.people[id] = { name, rig, frames, names, P: Float64Array(frames * names.length * 3) } (scene.js motionScene
|
|
4
|
+
// leaves it out; commands/motion3.js adds it). QA measures the compiled body against theirs (qa/people-checks.js,
|
|
5
|
+
// PERSON), an anchor can sit on one of their bones ({ kind: "person" }, compiler/anchors.js) and a ref can name one
|
|
6
|
+
// (person:<id>.<Bone>, refs.js). Frames are timeline frames; past a take's end its last frame holds.
|
|
7
|
+
import { skeletonFor } from "./rigs/index.js";
|
|
8
|
+
import { fk } from "./compiler/skeleton.js";
|
|
9
|
+
import { add, apply, ry } from "./compiler/math.js";
|
|
10
|
+
import { resolveBone } from "./bones.js";
|
|
11
|
+
|
|
12
|
+
const sampled = new WeakMap();
|
|
13
|
+
// joints are kept to 1e-5 m: what a saved take keeps of them (keptPeople) is then exactly what its compile read
|
|
14
|
+
const keep = v => Math.round(v * 1e5) / 1e5;
|
|
15
|
+
|
|
16
|
+
/** World joints of an installed take at a placement { position, yaw }, every frame (cached per take and placement). */
|
|
17
|
+
function sampleTake(take, placement) {
|
|
18
|
+
const key = JSON.stringify(placement), hit = sampled.get(take);
|
|
19
|
+
if (hit?.key === key) return hit.value;
|
|
20
|
+
const sk = skeletonFor(take.rig), B = take.bones.length, n = take.frameCount, names = sk.names, J = names.length;
|
|
21
|
+
const order = names.map(name => take.bones.indexOf(name)), R = ry(placement.yaw), P = new Float64Array(n * J * 3);
|
|
22
|
+
for (let f = 0; f < n; f++) {
|
|
23
|
+
const quats = order.map(b => (b < 0 ? [0, 0, 0, 1] : Array.from(take.rot.subarray((f * B + b) * 4, (f * B + b) * 4 + 4))));
|
|
24
|
+
const s = fk(sk, quats, Array.from(take.hips.subarray(f * 3, f * 3 + 3)));
|
|
25
|
+
for (let j = 0; j < J; j++) P.set(add(placement.position, apply(R, s.P[j])).map(keep), (f * J + j) * 3);
|
|
26
|
+
}
|
|
27
|
+
const value = { rig: take.rig, frames: n, names, P };
|
|
28
|
+
sampled.set(take, { key, value });
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* scene.people for a compile of character `selfId`: characters = the cast ({ id, subject, model, x, y, z, rot, hidden }),
|
|
34
|
+
* takeOf(id) = its installed take v3 or nothing. Hidden characters and ones without a take are left out.
|
|
35
|
+
*/
|
|
36
|
+
export function peopleOf(characters, selfId, takeOf) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const c of characters ?? []) {
|
|
39
|
+
if (!c || c.id === selfId || c.hidden) continue;
|
|
40
|
+
const take = takeOf(c.id);
|
|
41
|
+
if (take?.schema !== "cozyclay.take.v3" || !take.frameCount || !skeletonFor(take.rig)) continue;
|
|
42
|
+
out[c.id] = { name: c.subject || c.name || c.id, ...sampleTake(take, { position: [c.x ?? 0, c.y ?? 0, c.z ?? 0], yaw: c.rot ?? 0 }) };
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The person's joint `bone` at timeline frame f as [x, y, z], or null (unknown person or bone). */
|
|
48
|
+
export function personJoint(scene, id, bone, f) {
|
|
49
|
+
const p = scene?.people?.[id];
|
|
50
|
+
if (!p) return null;
|
|
51
|
+
const j = p.names.indexOf(resolveBone(skeletonFor(p.rig), bone).bone ?? "");
|
|
52
|
+
if (j < 0) return null;
|
|
53
|
+
const at = (Math.max(0, Math.min(p.frames - 1, Math.round(f))) * p.names.length + j) * 3;
|
|
54
|
+
return [p.P[at], p.P[at + 1], p.P[at + 2]];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The ids and names a person ref or anchor may name, for an error. */
|
|
58
|
+
export const peopleList = scene => Object.entries(scene?.people ?? {}).map(([id, p]) => `${id} (${p.name})`).join(", ") || "none: no other character has an installed take";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What a take keeps of the people so that its saved source compiles the same again without the others' takes
|
|
62
|
+
* (take-source.js): only the joints its person anchors and refs name (anywhere in the spec: anchors, a skill's
|
|
63
|
+
* 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.
|
|
64
|
+
*/
|
|
65
|
+
export function keptPeople(scene, spec) {
|
|
66
|
+
const want = new Map(), keep = (id, b) => {
|
|
67
|
+
const p = scene?.people?.[id], bone = p && resolveBone(skeletonFor(p.rig), b).bone;
|
|
68
|
+
if (bone) want.set(id, new Set([...(want.get(id) ?? []), bone]));
|
|
69
|
+
};
|
|
70
|
+
const walk = (v, key) => {
|
|
71
|
+
if (typeof v === "string") { const m = /^person:([^.]+)(?:\.(.+))?$/.exec(v); if (m && (m[2] || key === "from")) keep(m[1], m[2] ?? "Hips"); }
|
|
72
|
+
else if (Array.isArray(v)) v.forEach(x => walk(x));
|
|
73
|
+
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
|
+
};
|
|
75
|
+
walk(spec);
|
|
76
|
+
if (!want.size) return null;
|
|
77
|
+
return Object.fromEntries([...want].map(([id, bones]) => {
|
|
78
|
+
const p = scene.people[id], names = [...bones], J = p.names.length, P = [];
|
|
79
|
+
for (let f = 0; f < p.frames; f++) for (const b of names) { const i = (f * J + p.names.indexOf(b)) * 3; P.push(p.P[i], p.P[i + 1], p.P[i + 2]); }
|
|
80
|
+
return [id, { name: p.name, rig: p.rig, frames: p.frames, names, P }];
|
|
81
|
+
}));
|
|
82
|
+
}
|
package/src/motion3/qa/checks.js
CHANGED
|
@@ -58,11 +58,15 @@ export function generalChecks(sk, out, opts = {}) {
|
|
|
58
58
|
for (const [nm, p, r] of probes) { const d = headDepth(sk, s, p) + r; if (d > L.headAllow) push("FACE", `${nm} into head`, f, d, L.headAllow); }
|
|
59
59
|
if (segDist(s.P[I.LeftLeg], s.P[I.LeftFoot], s.P[I.RightLeg], s.P[I.RightFoot]) < 0.1 + L.self) push("SELF", "shins touch", f, 0.1, 0.1);
|
|
60
60
|
if (segDist(s.P[I.LeftUpLeg], s.P[I.LeftLeg], s.P[I.RightUpLeg], s.P[I.RightLeg]) < 0.135 + L.self) push("SELF", "thighs overlap", f, 0.135, 0.135);
|
|
61
|
+
const tilted = norm(sub(s.P[I.Spine], s.P[I.Hips]))[1] < 0.5;
|
|
61
62
|
for (const S of ["Left", "Right"]) {
|
|
62
63
|
const th = s.P[I[`${S}UpLeg`]], kn = s.P[I[`${S}Leg`]], an = s.P[I[`${S}Foot`]];
|
|
63
64
|
const bend = angle(sub(kn, th), sub(an, kn));
|
|
64
|
-
// the foot's heading on the ground; the toe sits below the ankle, which must not count
|
|
65
|
-
|
|
65
|
+
// the foot's heading on the ground; the toe sits below the ankle, which must not count. With the pelvis turned past
|
|
66
|
+
// 60 deg from upright (a flip, a cartwheel, lying) the ground says nothing about the leg: the foot's front is the toe
|
|
67
|
+
// across the shin, which an upright leg reads the same way
|
|
68
|
+
let toeDir = sub(s.P[I[`${S}ToeBase`]], an);
|
|
69
|
+
if (tilted) { const shin = norm(sub(an, kn)); toeDir = sub(toeDir, scale(shin, dot(toeDir, shin))); } else toeDir[1] = 0;
|
|
66
70
|
const mid = sub(kn, [(th[0] + an[0]) / 2, (th[1] + an[1]) / 2, (th[2] + an[2]) / 2]);
|
|
67
71
|
if (bend > 3 && dot(mid, toeDir) < -0.002) push("KNEE", `${S} knee bends backward`, f, bend, 0);
|
|
68
72
|
// a knee folded past its stop puts the shin in the thigh (the solver stops at KNEE_STOP)
|
|
@@ -8,6 +8,7 @@ import { kneeFoldHint, kneeWhy } from "./hints-knee.js";
|
|
|
8
8
|
import { objectFrame } from "../compiler/anchors.js";
|
|
9
9
|
import { bodyFor, bodyNormal, limbDepth } from "../body.js";
|
|
10
10
|
import { footProbes, footDepths } from "./foot-checks.js";
|
|
11
|
+
import { personCapsules } from "./people-checks.js";
|
|
11
12
|
|
|
12
13
|
const MARGIN = 0.003;
|
|
13
14
|
/** The most a foot hint lifts a foot over a body it went into through a side (a stair riser); higher, it backs out. */
|
|
@@ -19,7 +20,7 @@ function anchorFix(X, name, f, delta, k) {
|
|
|
19
20
|
const a = X.spec.anchors?.[name], path = field => `/anchors/${name}/${field}`;
|
|
20
21
|
if (a?.kind === "world") return { path: path("p"), delta };
|
|
21
22
|
if (a?.kind === "body") return { path: path("p"), delta: apply(ry(-(X.fr(f).c.yaw ?? 0)), delta) };
|
|
22
|
-
if (["bone", "segment", "prop"].includes(a?.kind)) return { path: path("offset"), delta };
|
|
23
|
+
if (["bone", "segment", "prop", "person"].includes(a?.kind)) return { path: path("offset"), delta };
|
|
23
24
|
if (a?.kind !== "object") return undefined;
|
|
24
25
|
// local is in the object's axes where that hand's target read it (anchors.js objectFrame)
|
|
25
26
|
const fr = X.fr(f), o = objectFrame(X.out.ctx, a, name, { ...fr, diag: { ...fr.diag, frame: f } }, k);
|
|
@@ -105,6 +106,21 @@ export const contactHints = {
|
|
|
105
106
|
}
|
|
106
107
|
return r;
|
|
107
108
|
},
|
|
109
|
+
/** PERSON "<part> x person:<id>": the part moves out of the other person's deepest capsule, along the line between their nearest points. */
|
|
110
|
+
PERSON(X, g) {
|
|
111
|
+
const [, part, id] = /^(\S+) x person:(.+)$/.exec(g.subject) ?? [], mine = capsule(part), p0 = X.out?.ctx?.scene?.people?.[id];
|
|
112
|
+
if (!mine || !p0) return null;
|
|
113
|
+
const f = g.worstFrame, { s } = X.fr(f), pts = segPoints(s.P[X.I[mine[1]]], s.P[X.I[mine[2]]]);
|
|
114
|
+
const near = ([other, C, D, q]) => {
|
|
115
|
+
const cd = sub(D, C), dd = Math.max(dot(cd, cd), 1e-12);
|
|
116
|
+
return pts.map(p => { const at = add(C, scale(cd, clamp(dot(sub(p, C), cd) / dd, 0, 1))); return { other, p, q: at, depth: mine[3] + q - len(sub(p, at)) }; })
|
|
117
|
+
.reduce((m, x) => (x.depth > m.depth ? x : m));
|
|
118
|
+
};
|
|
119
|
+
const hit = personCapsules(p0, f).map(near).reduce((m, x) => (x.depth > m.depth ? x : m));
|
|
120
|
+
const away = len(sub(hit.p, hit.q)) > 1e-6 ? norm(sub(hit.p, hit.q)) : norm([hit.p[0] - hit.q[0] || 1, 0, hit.p[2] - hit.q[2]]);
|
|
121
|
+
const r = moveHint(X, part, f, scale(away, g.worst - g.limit + MARGIN), ` (out of ${p0.name}'s ${hit.other})`);
|
|
122
|
+
return { ...r, hint: `${r.hint}; or, for a touch you mean, checks.allow [{ part: "${part}", object: "person:${id}", depth: ${m3(g.worst + MARGIN)} }]` };
|
|
123
|
+
},
|
|
108
124
|
CLEARANCE(X, g) {
|
|
109
125
|
const k = (X.spec.checks?.clearance ?? []).find(c => g.subject.startsWith(`${c.name}:`));
|
|
110
126
|
return k && boxHint(X, g, k.part, k.object, k.min - g.worst + MARGIN);
|
package/src/motion3/qa/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Motion QA entry: general checks on every take, the checks the spec declares, the objects it
|
|
2
|
-
// moves (object-checks.js), its goals (goals.js) and the style checks it opted into by naming
|
|
2
|
+
// moves (object-checks.js), the other people in the scene (people-checks.js, PERSON), its goals (goals.js) and the style checks it opted into by naming
|
|
3
3
|
// their limit (style.js; otherwise style is measured, not judged: observeMotion). Groups are
|
|
4
|
-
// ranked by severity.js.
|
|
4
|
+
// ranked by severity.js. total counts frame-level issues (a goal held on "all" frames adds one per failing frame); problems
|
|
5
|
+
// counts the groups, which is what a person reads (src/qa-label.js).
|
|
5
6
|
import { generalChecks, QA_LIMITS } from "./checks.js";
|
|
6
7
|
import { styleChecks, floatChecks } from "./style.js";
|
|
7
8
|
import { specChecks } from "./spec-checks.js";
|
|
8
9
|
import { objectChecks } from "./object-checks.js";
|
|
9
10
|
import { goalChecks } from "./goals.js";
|
|
10
11
|
import { wristIssues } from "./wrist-checks.js";
|
|
12
|
+
import { peopleChecks } from "./people-checks.js";
|
|
11
13
|
import { explainGroups } from "./explain.js";
|
|
12
14
|
import { LOW_IS_BAD, groupKey, rankGroups } from "./severity.js";
|
|
13
15
|
|
|
@@ -34,9 +36,9 @@ export function runMotionQa(sk, out, spec, opts = {}) {
|
|
|
34
36
|
const checks = spec.checks ?? {}, goals = goalChecks(out, spec.goals ?? []);
|
|
35
37
|
const issues = [...generalChecks(sk, out, { allow: checks.allow, limits: checks.limits, mouthPoints: checks.mouthPoints }), ...specChecks(out, checks),
|
|
36
38
|
...objectChecks(sk, out, spec), ...floatChecks(out, checks),
|
|
37
|
-
...styleChecks(sk, out, checks, { ...QA_LIMITS, ...checks.limits }), ...wristIssues(sk, out), ...goals.issues];
|
|
39
|
+
...styleChecks(sk, out, checks, { ...QA_LIMITS, ...checks.limits }), ...wristIssues(sk, out), ...peopleChecks(sk, out, checks), ...goals.issues];
|
|
38
40
|
if (out.f0) for (const i of issues) i.frame += out.f0;
|
|
39
41
|
const groups = summarizeIssues(issues);
|
|
40
42
|
if (opts.explain !== false) explainGroups(sk, out, spec, groups, { userSpec: opts.userSpec });
|
|
41
|
-
return { ok: issues.length === 0, total: issues.length, ...(spec.goals ? { goals: goals.results } : {}), groups };
|
|
43
|
+
return { ok: issues.length === 0, total: issues.length, problems: groups.length, ...(spec.goals ? { goals: goals.results } : {}), groups };
|
|
42
44
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// PERSON: the compiled body against the other people in the scene (people.js), capsule against capsule (geometry.js
|
|
2
|
+
// CAPSULES on both bodies, at the same timeline frame). "<part> x person:<id>": how deep the part goes into the
|
|
3
|
+
// other body (its deepest capsule there; the hint names which), past QA_LIMITS.allow. An intended touch (a push, a hand
|
|
4
|
+
// on a shoulder) is allowed like any contact: checks.allow [{ part, object: "person:<id>", depth }].
|
|
5
|
+
import { segDist, CAPSULES } from "./geometry.js";
|
|
6
|
+
import { allowance, QA_LIMITS } from "./checks.js";
|
|
7
|
+
import { dist } from "../compiler/math.js";
|
|
8
|
+
|
|
9
|
+
/** Two bodies whose hips are this far apart (m) cannot touch: a body is under 1 m from its hips to any skin. */
|
|
10
|
+
const APART = 2.2;
|
|
11
|
+
|
|
12
|
+
/** The person's capsule endpoints at timeline frame f: [part, A, B, r] with the joints read from P. */
|
|
13
|
+
export function personCapsules(p, f) {
|
|
14
|
+
const at = Math.max(0, Math.min(p.frames - 1, f)) * p.names.length, J = name => { const j = p.names.indexOf(name), i = (at + j) * 3; return [p.P[i], p.P[i + 1], p.P[i + 2]]; };
|
|
15
|
+
return CAPSULES.map(([part, a, b, r]) => [part, J(a), J(b), r]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** PERSON issues of compile result out (frames indexed in out.frames; reported at their absolute frame by qa/index.js). */
|
|
19
|
+
export function peopleChecks(sk, out, checks = {}) {
|
|
20
|
+
const people = Object.entries(out.ctx?.scene?.people ?? {}), issues = [];
|
|
21
|
+
if (!people.length) return issues;
|
|
22
|
+
const I = sk.index, f0 = out.f0 ?? 0, lim = { ...QA_LIMITS, ...checks.limits }.allow;
|
|
23
|
+
out.frames.forEach(({ s }, i) => {
|
|
24
|
+
const f = i + f0, mine = CAPSULES.map(([part, a, b, r]) => [part, s.P[I[a]], s.P[I[b]], r]);
|
|
25
|
+
for (const [id, p] of people) {
|
|
26
|
+
// a saved take keeps only the joints its anchors read (people.js keptPeople): no body to collide with there
|
|
27
|
+
if (!CAPSULES.every(([, a, b]) => p.names.includes(a) && p.names.includes(b))) continue;
|
|
28
|
+
const theirs = personCapsules(p, f), hips = theirs.find(c => c[0] === "hips")[1];
|
|
29
|
+
if (dist(s.P[I.Hips], hips) > APART) continue;
|
|
30
|
+
for (const [part, A, B, r] of mine) {
|
|
31
|
+
const depth = Math.max(...theirs.map(([, C, D, q]) => r + q - segDist(A, B, C, D))), allow = allowance(checks.allow, part, `person:${id}`) ?? lim;
|
|
32
|
+
if (depth > allow) issues.push({ kind: "PERSON", msg: `${part} x person:${id}`, frame: i, value: +depth.toFixed(4), limit: allow });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return issues;
|
|
37
|
+
}
|
package/src/motion3/qa/style.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// See docs/motion-api-v3.1.md C13.
|
|
11
11
|
import { add, sub, scale, len, dot, angle } from "../compiler/math.js";
|
|
12
12
|
import { CAPSULES, segPoints, sdBox } from "./geometry.js";
|
|
13
|
-
import { groundSole } from "./ground.js";
|
|
13
|
+
import { groundSole, groundY } from "./ground.js";
|
|
14
14
|
|
|
15
15
|
const SPEED_JOINTS = ["Hips", "Head", "LeftHand", "RightHand", "LeftFoot", "RightFoot", "LeftLeg", "RightLeg", "LeftForeArm", "RightForeArm"];
|
|
16
16
|
const CORE = new Set(["Hips", "Head"]);
|
|
@@ -68,9 +68,12 @@ export function popChecks(sk, out, L, push) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/** Trunk capsules that rest a body on the floor (lying, sitting on it): within this of the floor, the floor carries it. */
|
|
72
|
+
const FLOOR_REST = ["hips", "torso_lo", "torso_hi"], FLOOR_TOL = 0.04;
|
|
73
|
+
/** True when the floor under the trunk or a declared support (checks.supports) carries the body at frame f. */
|
|
72
74
|
function supported(out, f, supports) {
|
|
73
75
|
const { P } = out.frames[f].s, I = out.ctx.sk.index;
|
|
76
|
+
if (CAPSULES.some(([part, a, b, r]) => FLOOR_REST.includes(part) && Math.min(P[I[a]][1], P[I[b]][1]) - r < FLOOR_TOL)) return true;
|
|
74
77
|
return supports.some(sp => {
|
|
75
78
|
const box = out.objectsAt(f)[sp.object], cap = CAPSULES.find(c => c[0] === (sp.part ?? "hips"));
|
|
76
79
|
return box && cap && Math.min(...segPoints(P[I[cap[1]]], P[I[cap[2]]]).map(p => sdBox(p, box))) - cap[3] < (sp.tol ?? 0.04);
|
|
@@ -101,11 +104,18 @@ function balanceMargins(out, supports = []) {
|
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
/**
|
|
107
|
+
/** True when a hand rests on the ground at frame f: its wrist or knuckles within HAND_DOWN of the ground under them. */
|
|
108
|
+
const HAND_DOWN = 0.06;
|
|
109
|
+
function handDown(out, f) {
|
|
110
|
+
const { P } = out.frames[f].s, I = out.ctx.sk.index;
|
|
111
|
+
return ["Left", "Right"].some(S => [`${S}Hand`, `${S}HandMiddle1`].some(j => I[j] !== undefined && P[I[j]][1] - groundY(out, f, P[I[j]]) < HAND_DOWN));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Runs [a, b] (indexes of out.frames) with both soles 2 cm or more above the ground (ground.js), no hand on it and no declared support. */
|
|
105
115
|
function airRuns(out, supports = []) {
|
|
106
116
|
const n = out.frames.length, runs = [];
|
|
107
117
|
const air = out.frames.map((_, f) => !supported(out, f, supports)
|
|
108
|
-
&& ["Left", "Right"].every(S => { const sp = groundSole(out, f, S); return Math.min(sp.heel[1], sp.ball[1]) >= 0.02; }));
|
|
118
|
+
&& ["Left", "Right"].every(S => { const sp = groundSole(out, f, S); return Math.min(sp.heel[1], sp.ball[1]) >= 0.02; }) && !handDown(out, f));
|
|
109
119
|
for (let a = 0; a < n; a++) {
|
|
110
120
|
if (!air[a]) continue;
|
|
111
121
|
let b = a;
|
package/src/motion3/refs.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// prop:<prop>.<point> a point of a held prop (spec.props; "origin" always exists)
|
|
5
5
|
// anchor:<name> a spec anchor, resolved as the compiler resolves it
|
|
6
6
|
// landmark:mouth the measured face landmark
|
|
7
|
+
// person:<id>.<Bone> another character's joint where its installed take has it (people.js)
|
|
7
8
|
// object:<id>[.center|.top|.base] a body or a group (object-pose.js objectBoxesAt): the centre,
|
|
8
9
|
// top-face centre or bottom-face centre of the AABB of its
|
|
9
10
|
// part boxes' corners (default center)
|
|
@@ -17,8 +18,9 @@ import { bonePoint } from "./compiler/skeleton.js";
|
|
|
17
18
|
import { didYouMean } from "./compiler/vocab.js";
|
|
18
19
|
import { add, apply, sub, len } from "./compiler/math.js";
|
|
19
20
|
import { sdBox } from "./qa/geometry.js";
|
|
21
|
+
import { personJoint, peopleList } from "./people.js";
|
|
20
22
|
|
|
21
|
-
const SYNTAX = { joint: "joint:<Bone>", bone: "bone:<Bone>", prop: "prop:<prop>.<point>", anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base]" };
|
|
23
|
+
const SYNTAX = { joint: "joint:<Bone>", bone: "bone:<Bone>", prop: "prop:<prop>.<point>", anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base]", person: "person:<id>.<Bone>" };
|
|
22
24
|
|
|
23
25
|
function parse(ref) {
|
|
24
26
|
const m = /^([a-z]+):(.+)$/s.exec(String(ref));
|
|
@@ -26,6 +28,7 @@ function parse(ref) {
|
|
|
26
28
|
const [, kind, rest] = m;
|
|
27
29
|
if (kind === "prop") { const [name, point] = rest.split("."); return { kind, name, point: point ?? "" }; }
|
|
28
30
|
if (kind === "object") { const o = /^(.+)\.(center|top|base)$/s.exec(rest); return { kind, name: o ? o[1] : rest, point: o ? o[2] : null }; }
|
|
31
|
+
if (kind === "person") { const dot = rest.lastIndexOf("."); return { kind, name: dot > 0 ? rest.slice(0, dot) : rest, point: dot > 0 ? rest.slice(dot + 1) : "" }; }
|
|
29
32
|
return { kind, name: rest, point: null };
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -50,6 +53,7 @@ export function refPoint(out, f, ref) {
|
|
|
50
53
|
case "anchor": return out.ctx.spec.anchors?.[r.name] ? resolveAnchor(out.ctx, r.name, s, { ...diag, frame: f }, c) : null;
|
|
51
54
|
case "landmark": return sk.landmarks?.[r.name] ? bonePoint(s, sk.index.Head, sk.landmarks[r.name]) : null;
|
|
52
55
|
case "object": { const boxes = objectBoxesAt(out, r.name, f); return boxes ? boxesPoint(boxes, r.point) : null; }
|
|
56
|
+
case "person": return personJoint(out.ctx.scene, r.name, r.point, f);
|
|
53
57
|
default: return null;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -116,5 +120,10 @@ export function checkRef(spec, err, { sk, scene }, at, ref, kinds = REF_KINDS) {
|
|
|
116
120
|
if (r.name !== "mouth") err(`${tag}: the only landmark is "mouth"`);
|
|
117
121
|
return r.name === "mouth";
|
|
118
122
|
}
|
|
123
|
+
if (r.kind === "person") {
|
|
124
|
+
const ok = Boolean(personJoint(scene, r.name, r.point, 0));
|
|
125
|
+
if (!ok) err(`${tag}: no person "${r.name}" with joint "${r.point}" (people with an installed take: ${peopleList(scene)})`);
|
|
126
|
+
return ok;
|
|
127
|
+
}
|
|
119
128
|
return objectOk(err, scene, tag, r.name, { groups: true });
|
|
120
129
|
}
|
package/src/motion3/scene.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// and scene.others (take-source.js), so the groups are rebuilt from the entries: groupsOf.
|
|
8
8
|
// `others` lists the records with no box (an Empty, a light, an effect, a plane) by id with their
|
|
9
9
|
// renderer and ancestors: a switch entry (C15) may name them, and a skill finds a kit's lit parts
|
|
10
|
-
// there. A body that glows (record.glow) says so (`glow: true`).
|
|
10
|
+
// there. A body that glows (record.glow) says so (`glow: true`); a record's grasp sockets (record.grasp,
|
|
11
|
+
// ../object-grasp.js) ride along for the hand (sockets.js).
|
|
11
12
|
import { objectSize } from "../object-size.js";
|
|
12
13
|
import { mul, rx, ry, rz } from "./compiler/math.js";
|
|
13
14
|
|
|
@@ -50,11 +51,11 @@ export function motionScene(objects = []) {
|
|
|
50
51
|
if (!o || o.hidden) continue;
|
|
51
52
|
const size = objectSize(o), up = ancestorsOf(o, byId);
|
|
52
53
|
if (NO_BODY.has(o.renderer) || !(size.width > 0 && size.height > 0 && size.depth > 0)) {
|
|
53
|
-
others[o.id] = { renderer: o.renderer, ancestors: up.map(p => p.id) };
|
|
54
|
+
others[o.id] = { renderer: o.renderer, ancestors: up.map(p => p.id), ...(o.grasp ? { grasp: o.grasp } : {}) };
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
57
|
out[o.id] = { name: o.name, renderer: o.renderer, ...poseOf(o), half: [size.width / 2, size.height / 2, size.depth / 2],
|
|
57
|
-
ancestors: up.map(p => p.id), ...(up.length ? { ancestorPoses: up.map(poseOf) } : {}), ...(o.glow ? { glow: true } : {}) };
|
|
58
|
+
ancestors: up.map(p => p.id), ...(up.length ? { ancestorPoses: up.map(poseOf) } : {}), ...(o.glow ? { glow: true } : {}), ...(o.grasp ? { grasp: o.grasp } : {}) };
|
|
58
59
|
}
|
|
59
60
|
return { objects: out, groups: groupsOf({ objects: out }), others };
|
|
60
61
|
}
|