cozyclay 3.0.0 → 3.0.1
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 +28 -0
- package/bin/agent/agent-routes.mjs +3 -3
- package/bin/agent/agent-runner.mjs +5 -5
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-DjHD8u7k.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/workflow-D78eiKTa.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/app-context.js +7 -2
- package/src/command-bus.js +2 -2
- package/src/commands/motion3.js +12 -5
- 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 +8 -0
- package/src/motion3/compiler/limbs.js +35 -8
- package/src/motion3/compiler/pose.js +17 -2
- package/src/motion3/compiler/validate-objects.js +3 -2
- package/src/motion3/compiler/validate-parts.js +2 -0
- package/src/motion3/compiler/validate.js +3 -3
- package/src/motion3/compiler/vocab.js +17 -16
- package/src/motion3/describe.js +5 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/free-motion.js +42 -11
- package/src/motion3/object-pose.js +1 -1
- 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/schema/fields.js +4 -4
- package/src/motion3/schema/topics.js +6 -4
- package/src/motion3/schema/types.js +10 -8
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +2 -1
- 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 +15 -0
- package/src/qa-label.js +25 -0
- package/src/studio-actions.js +4 -0
- package/src/studio-agent-protocol.js +8 -8
- package/src/studio-app-binding.js +2 -2
- package/src/studio-error-codes.js +8 -1
- 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,7 +22,8 @@
|
|
|
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;
|
|
@@ -76,9 +81,31 @@ export function landingOf(spec, scene, id, start, onto = null) {
|
|
|
76
81
|
return { ...best, h: Math.max(0, low - best.top) };
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
const shapes = new WeakMap();
|
|
85
|
+
/** The shape of `id` in its root frame (settle.js), once per scene. */
|
|
86
|
+
function shapeIn(scene, id) {
|
|
87
|
+
if (!shapes.has(scene)) shapes.set(scene, new Map());
|
|
88
|
+
const cache = shapes.get(scene);
|
|
89
|
+
if (!cache.has(id)) cache.set(id, shapeOf(boxesAtRoot(scene, id, { pos: [0, 0, 0], R: I3() })));
|
|
90
|
+
return cache.get(id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A fall from root pose `start` at (mapped) frame t0: its timing, its pose at a frame, where it rests. `before` is its pose
|
|
95
|
+
* a frame earlier (a release from the hand), for the spin it is let go with.
|
|
96
|
+
*/
|
|
97
|
+
function fallPlan(spec, scene, id, start, t0, fall, fps, before = null) {
|
|
98
|
+
const land = landingOf(spec, scene, id, start, fall.onto ?? null), shape = fall.tumble === false ? null : shapeIn(scene, id);
|
|
99
|
+
if (shape) {
|
|
100
|
+
const spin = before ? spinOf(before.R, start.R, fps) : [0, 0, 0], air = airPath(shape, start, spin, land.top, fps);
|
|
101
|
+
// a spin tips it the way its top moves (spin x up); a spin about the vertical tips nothing
|
|
102
|
+
const path = settlePath(shape, air.at(air.tl), land.top, TIP * land.h + spinEnergy(shape, spin), [-spin[2], 0, spin[0]]);
|
|
103
|
+
if (path.steps.length || spin.some(v => v)) {
|
|
104
|
+
const at = f => { const t = (f - t0) / fps; return t <= 0 ? start : t <= air.tl ? air.at(t) : settledAt(path, t - air.tl); };
|
|
105
|
+
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 };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const h = land.h, tl = Math.sqrt(2 * h / G);
|
|
82
109
|
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
110
|
const rest = { pos: [start.pos[0], start.pos[1] - h, start.pos[2]], R: start.R };
|
|
84
111
|
const at = f => {
|
|
@@ -157,11 +184,15 @@ export function beforeGrab(ctx, id, o, f, grab) {
|
|
|
157
184
|
return walk(timelineOf({ ...ctx, grab }, id, o, ctx.timeMap, "before"), f, { pos: rest.pos, R: rest.R }, planner(ctx, id));
|
|
158
185
|
}
|
|
159
186
|
|
|
160
|
-
/**
|
|
161
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Where carry `id` (entry o) is at absolute f after its release, from its release pose; null without a fall from the release on.
|
|
189
|
+
* `beforePose` (a frame before the release) gives the spin it is let go with to a fall that starts right at the release.
|
|
190
|
+
*/
|
|
191
|
+
export function afterRelease(ctx, id, o, f, release, releasePose, beforePose = null) {
|
|
162
192
|
const events = fallsOf(o).length ? timelineOf({ ...ctx, release }, id, o, ctx.timeMap, "after") : [];
|
|
163
193
|
// the release pose comes from the solved body, which a rebake changes: plan these falls afresh
|
|
164
|
-
|
|
194
|
+
const plan = (start, fall) => fallPlan(ctx.spec, ctx.scene, id, start, fall.t, fall, ctx.spec.fps ?? 30, fall.t <= release + 1 ? beforePose : null);
|
|
195
|
+
return events.length ? walk(events, f, releasePose, plan, false) : null;
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
/** The falls of entry `id` as planned under the current time map, for readers (timeline, tests): [{ t0, land, end, h, support, top }]. */
|
|
@@ -127,7 +127,7 @@ export function rootPoseAt(out, id, f, now = null) {
|
|
|
127
127
|
if (isCarried(o) && (rest || !keepsOffset(scene, id, o))) {
|
|
128
128
|
const [grab, release] = spanOf(out, o), g = Math.max(grab, Math.min(f, release));
|
|
129
129
|
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;
|
|
130
|
+
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
131
|
if (after) return after;
|
|
132
132
|
return now && g === f ? carriedOn(out, id, o, f, now.s) : carriedAt(out, id, o, g);
|
|
133
133
|
}
|
|
@@ -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
|
}
|
|
@@ -11,14 +11,14 @@ export const KEY_FIELDS = ["f", "set", "step", "src"];
|
|
|
11
11
|
export const USE_FIELDS = ["skill", "args", "f0"];
|
|
12
12
|
/** Anchor fields by anchor kind (besides `kind`). */
|
|
13
13
|
export const ANCHOR_FIELDS = { world: ["p"], body: ["p"], bone: ["bone", "offset"], segment: ["from", "to", "t", "tMin", "offset", "reach"],
|
|
14
|
-
object: ["object", "local"], landmark: ["name"], prop: ["prop", "point", "offset"] };
|
|
14
|
+
object: ["object", "local"], landmark: ["name"], prop: ["prop", "point", "offset"], person: ["person", "bone", "offset"] };
|
|
15
15
|
export const PROP_FIELDS = ["hand", "origin", "points"];
|
|
16
16
|
export const FIT_FIELDS = ["side", "anchor", "effector", "offset", "orient"];
|
|
17
17
|
/** spec.objects[id]: a hinge or a carry (a body or a group id), see C3; carry keys: C14; a switch (any record, C15) moves nothing; a follow (any record, C16) rides a point of the body; a free entry (C17) moves by keys and falls. */
|
|
18
18
|
export const OBJECT_FIELDS = { hinge: ["pivot", "axis", "channel"], carry: ["prop", "at", "along", "visible", "level", "grab", "release", "keys", "fall"], switch: ["visible", "level"],
|
|
19
19
|
follow: ["follow", "offset", "aim", "visible", "level"], free: ["keys", "fall", "visible", "level"] };
|
|
20
|
-
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces. */
|
|
21
|
-
export const FALL_FIELDS = ["at", "onto", "bounce"];
|
|
20
|
+
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces, whether it rolls to rest. */
|
|
21
|
+
export const FALL_FIELDS = ["at", "onto", "bounce", "tumble"];
|
|
22
22
|
/** One spec.objects[id].keys entry (C14): the held object's world pose at frame f; aim (+ roll) in place of rot (C16). */
|
|
23
23
|
export const OBJECT_KEY_FIELDS = ["f", "pos", "rot", "aim", "roll", "lean"];
|
|
24
24
|
export const WALK_FIELDS = ["type", "f0", "steps", "step", "period", "swing", "halfWidth", "lift", "heading", "turnDistance", "pelvisHigh", "pelvisLow", "firstStep", "armSwing", "vary"];
|
|
@@ -39,6 +39,6 @@ export const GOAL_FIELDS = ["name", "near", "above", "moved", "still", "speed",
|
|
|
39
39
|
/** One motion.compile probes entry (C5): exactly one of distance | position. */
|
|
40
40
|
export const PROBE_FIELDS = ["name", "distance", "position", "frames", "every"];
|
|
41
41
|
/** Point reference kinds (C2, refs.js). */
|
|
42
|
-
export const REF_KINDS = ["joint", "bone", "prop", "anchor", "landmark", "object"];
|
|
42
|
+
export const REF_KINDS = ["joint", "bone", "prop", "anchor", "landmark", "object", "person"];
|
|
43
43
|
/** motion.describe topics (C9); examples: whole specs that compile (schema/spec-examples.js). */
|
|
44
44
|
export const DESCRIBE_TOPICS = ["essentials", "spec", "skills", "anchors", "props", "objects", "checks", "goals", "probes", "channels", "refs", "fix", "examples"];
|
|
@@ -64,7 +64,8 @@ export const NOTES = {
|
|
|
64
64
|
"More: topics anchors, props, objects, checks, goals, probes, channels, refs, fix, skills."],
|
|
65
65
|
anchors: ["An anchor is a named point a hand can go to: key w_<anchor>_l / w_<anchor>_r from 0 to 1 with armw_<side> 1 (the IK arm).",
|
|
66
66
|
"An anchor name may not end in _l or _r.", "object anchors (a body or a group; local in its own axes) follow the object where it is (hinges, carries): put the other hand on a carried object with one; the carrying hand's own anchor stays where it took it. prop anchors are a point of a prop the other hand holds.",
|
|
67
|
-
"segment with reach slides from t toward tMin until the arm reaches it."
|
|
67
|
+
"segment with reach slides from t toward tMin until the arm reaches it.",
|
|
68
|
+
"person { person: <character id>, bone, offset? } is another character's joint where its installed take has it that frame (a hand on a shoulder, a push on the chest): install that character's take first; reachGrip on it allows the hand to touch that person."],
|
|
68
69
|
props: ["A prop is a set of points fixed to one hand, [d, l, n] along the palm axes; prop:<prop>.origin is the mean of its origin bones.",
|
|
69
70
|
"fits: the hand target moves so the effector (a prop point or the mean of joints), not the wrist, lands on the anchor.",
|
|
70
71
|
"A fit on a scene body also turns the hand about the forearm into a grip: a thin body (a can, a bar, a handle, a neck) across the palm, else thumb up; never past a comfortable forearm turn. A wrist you turn (wtwist/wflex) wins; fit.orient: false (reachGrip args.orient: false) turns it off.",
|
|
@@ -81,14 +82,15 @@ export const NOTES = {
|
|
|
81
82
|
+ "a left hand on a prop anchor of it turns with it; "
|
|
82
83
|
+ `the hand eases in and out over ${KEY_BLEND} frames. A REACH group "<id>: n cm off its keys" is what the hand could not reach; `
|
|
83
84
|
+ "output.observations.held measures every key as drawn (deg off the asked +Y, cm off pos, the drawn +Y, the rot an aim key used).",
|
|
84
|
-
"free { keys?, fall? } moves what nobody holds: keys at any frame; fall drops it at frame at
|
|
85
|
-
+ "one small bounce (0: none). A carry falls before grab or after release.",
|
|
85
|
+
"free { keys?, fall? } moves what nobody holds: keys at any frame; fall drops it at frame at onto the still top below (onto: that one; else the floor) "
|
|
86
|
+
+ "to rest on a stable face, one small bounce (0: none). A carry falls before grab or after release.",
|
|
86
87
|
"A follow { follow: <ref>, offset?, aim?, visible?, level? } puts any record (an effect, a light, a part) on a body point every frame: "
|
|
87
88
|
+ "aim sets its +Y (an effect emits along it): up (default), face (forward), a direction or a ref (toward it); offset is metres along its axes after aim. It is no obstacle.",
|
|
88
89
|
"The editor's motion.keyObject writes one key from a drag (a hinge: its channel key)."],
|
|
89
90
|
checks: ["Each entry adds QA groups: contacts CONTACT, orient ORIENT, below BELOW, clearance CLEARANCE, appear APPEAR; supports mark where a part rests on an object (no FLOAT, no balance there); a foot on an object's top is on the ground without one.",
|
|
90
91
|
"when: a weight channel (active above 0.99) or [f0, f1]; clearance uses frames.", "Check points take joint:, prop: and anchor: only; goals and probes take every ref (topic refs).",
|
|
91
92
|
"allow excuses a body part inside an object up to depth; limits override the QA limits. Unnamed entries are named by place, contacts[0].",
|
|
93
|
+
"Other characters with an installed take are bodies too: PERSON \"<part> x person:<id>\" is a part inside that person; a touch you mean (a push, a hug) is allow { part, object: \"person:<id>\", depth }.",
|
|
92
94
|
"Speed, acceleration, knee bend and balance are style: measured in output.observations, not judged. Naming speedCore/speedLimb, accCore/accLimb, knee or balance in limits makes it a check (SPEED, ACC, KNEE, BALANCE). observations.near: each hand's and foot's nearest object and gap (< 0 inside), measured, not judged.",
|
|
93
95
|
"motion.qa { checks } replaces every check for a re-measure."],
|
|
94
96
|
goals: ["Exactly one of near (distance <= tol), above (a.y - b.y >= by), moved (an object's base moved at least by from the scene pose; any other point ref, e.g. prop:bat.tip, at least by from where it is at at[0], at: [f0, f1]), still (stays within tol, default 0.01 m, of where moved measures from) or speed (the point reaches by m/s).",
|
|
@@ -102,7 +104,7 @@ export const NOTES = {
|
|
|
102
104
|
"Flow (spec.flow, on by default): the spine, head, arm swing/bend and wrist carry their speed through a key (a small overshoot) and trail the body (spine 1, head 2, arms 3 frames; lag: { spine, head, arms } or a number); feet, root, hand targets, weights, poles, fingers and objects land on their keys on time. flow: false turns it off; { smooth: false } or { lag: false } one part.",
|
|
103
105
|
"w_<anchor>_<side> (0..1) weights a hand onto an anchor; the weights of one hand add up to at most 1.",
|
|
104
106
|
"Any other numeric channel must come from spec.channels, a hinge (objects.<id>.channel), a check's when or a visible switch; names ending _vis are visibility switches (shown above 0.5)."],
|
|
105
|
-
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base].",
|
|
107
|
+
refs: ["A ref names a point: joint:/bone:<Bone>, prop:<prop>.<point>, anchor:<name>, landmark:mouth, object:<id>[.center|.top|.base], person:<character id>.<Bone> (another character's joint in its installed take).",
|
|
106
108
|
"object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
|
|
107
109
|
"Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
|
|
108
110
|
fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
|
|
@@ -64,11 +64,12 @@ export const ANCHOR_TYPES = {
|
|
|
64
64
|
from: str("bone"), to: str("bone"), t: num("0 = from .. 1 = to", { default: 0.5 }), tMin: num("with reach: the lowest t to slide back to", { default: 0.15 }),
|
|
65
65
|
reach: side("slide toward `from` until this arm reaches"), object: str("scene object id"), local: ref("vec3", "point in the object's frame, m"),
|
|
66
66
|
name: { const: "mouth" }, prop: str("a prop held by the other hand"), point: str("a point of that prop, or origin"),
|
|
67
|
+
person: str("a character id"),
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const palm = d => ref("vec3", `${d} [d, l, n] on the palm axes (d to the middle finger, l to the thumb side, n = d x l), m`);
|
|
70
71
|
/** Fields an anchor of each kind must have. */
|
|
71
|
-
export const ANCHOR_REQUIRED = { world: ["p"], body: ["p"], bone: ["bone"], segment: ["from", "to"], object: ["object"], landmark: ["name"], prop: ["prop", "point"] };
|
|
72
|
+
export const ANCHOR_REQUIRED = { world: ["p"], body: ["p"], bone: ["bone"], segment: ["from", "to"], object: ["object"], landmark: ["name"], prop: ["prop", "point"], person: ["person", "bone"] };
|
|
72
73
|
|
|
73
74
|
export const PROP_TYPES = {
|
|
74
75
|
hand: side(), origin: { type: "array", items: { type: "string" }, minItems: 1, description: "bones whose mean is the prop origin" },
|
|
@@ -85,17 +86,17 @@ export const FIT_TYPES = {
|
|
|
85
86
|
const OBJECT_KEY_TYPES = { f: int(), pos: ref("vec3"), rot: { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4, description: "quat x, y, z, w" },
|
|
86
87
|
aim: ref("vec3", "or rot: +Y dir"), roll: num("deg about aim"), lean: num("aim tilt", { minimum: 0, maximum: 45 }) };
|
|
87
88
|
|
|
88
|
-
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }) }, required: ["at"], additionalProperties: false };
|
|
89
|
+
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }), tumble: { type: "boolean" } }, required: ["at"], additionalProperties: false };
|
|
89
90
|
/** A free entry's keys and fall (C17) are a carry's, at any frame: said once, not twice. */
|
|
90
91
|
const AS_CARRY = { description: "as carry; keys at any frame" };
|
|
91
92
|
export const FREE_TYPES = { keys: AS_CARRY, fall: AS_CARRY };
|
|
92
93
|
|
|
93
94
|
export const OBJECT_TYPES = {
|
|
94
|
-
pivot: ref("vec3", "world hinge point
|
|
95
|
-
prop: str("spec.props name: it rides that hand"), at: ref("vec3", "palm offset [d, l, n]
|
|
96
|
-
visible: str("channel: shown > 0.5"), level: str("channel: strength 0..4"), grab: int("
|
|
95
|
+
pivot: ref("vec3", "world hinge point"), axis: ref("vec3", "default +Y"), channel: str("angle channel, deg"),
|
|
96
|
+
prop: str("spec.props name: it rides that hand"), at: ref("vec3", "palm offset [d, l, n]"), along: ref("vec3", "+Y on the palm, fixed"),
|
|
97
|
+
visible: str("channel: shown > 0.5"), level: str("channel: strength 0..4"), grab: int("hand takes it", { default: 0 }), release: int("let go (else end)"),
|
|
97
98
|
keys: { type: "array", items: { type: "object", properties: Object.fromEntries(OBJECT_KEY_FIELDS.map(f => [f, OBJECT_KEY_TYPES[f]])), required: ["f", "pos"], additionalProperties: false },
|
|
98
|
-
description: "world pose at f
|
|
99
|
+
description: "world pose at f; hand follows" },
|
|
99
100
|
fall: { ...FALL, description: "or a list; topic objects" },
|
|
100
101
|
follow: ref("ref"), offset: ref("vec3"), aim: { oneOf: [{ enum: ["face", "up"] }, { $ref: "#/$defs/vec3" }, { $ref: "#/$defs/ref" }], default: "up" },
|
|
101
102
|
};
|
|
@@ -142,6 +143,7 @@ export const FIX_TYPES = {
|
|
|
142
143
|
};
|
|
143
144
|
|
|
144
145
|
export const REF_DOCS = {
|
|
145
|
-
joint: "joint:<Bone
|
|
146
|
-
anchor: "anchor:<name
|
|
146
|
+
joint: "joint:<Bone>", bone: "bone:<Bone> (= joint:)", prop: "prop:<prop>.<point> (origin exists)",
|
|
147
|
+
anchor: "anchor:<name>", landmark: "landmark:mouth", object: "object:<id>[.center|.top|.base], a body or group; bare = its surface (distance)",
|
|
148
|
+
person: "person:<id>.<Bone>",
|
|
147
149
|
};
|