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
|
@@ -9,15 +9,22 @@
|
|
|
9
9
|
// { kind: "object", object: "Car_Door_L", local: [x,y,z] } point fixed to a scene object or group where it is at
|
|
10
10
|
// that frame (hinged, carried, following; object-pose.js objectPoseAt), in its own axes
|
|
11
11
|
// { kind: "landmark", name: "mouth" } measured face landmark on the Head bone
|
|
12
|
-
// { kind: "prop", prop: "cig", point: "tip", offset: [x,y,z] } a point on a prop held by the other hand
|
|
12
|
+
// { kind: "prop", prop: "cig", point: "tip", offset: [x,y,z] } a point on a prop held by the other hand; while
|
|
13
|
+
// that hand does not hold the object it carries, the point stays on that object (propOnCarry)
|
|
14
|
+
// { kind: "person", person: "char-b", bone: "LeftArm", offset: [x,y,z] } world offset from another character's joint
|
|
15
|
+
// where its installed take has it at that frame (people.js)
|
|
13
16
|
// spec.props = { name: { hand: "r", origin: ["RightHandIndex2", "RightHandMiddle2"],
|
|
14
17
|
// points: { name: [d, l, n] } } } offsets along the palm axes (m)
|
|
15
|
-
import { add, sub, scale, len, apply, ry } from "./math.js";
|
|
18
|
+
import { add, sub, scale, len, apply, ry, T } from "./math.js";
|
|
16
19
|
import { bonePoint } from "./skeleton.js";
|
|
17
20
|
import { palmAxes } from "./limbs.js";
|
|
18
21
|
import { SIDES } from "./pose.js";
|
|
19
22
|
import { HAND_CLEAR, keepOut } from "../body.js";
|
|
20
23
|
import { groupsOf } from "../scene.js";
|
|
24
|
+
import { personJoint, peopleList } from "../people.js";
|
|
25
|
+
import { isCarried } from "../carried.js";
|
|
26
|
+
import { socketsOf, socketNamed, socketWorld, socketDistance, faceFrom } from "../sockets.js";
|
|
27
|
+
import { partsOf } from "../object-pose.js";
|
|
21
28
|
|
|
22
29
|
function armReach(sk, k) {
|
|
23
30
|
const side = SIDES[k], P = sk.restWorldPos, I = sk.index;
|
|
@@ -78,17 +85,74 @@ function anchorPoint(ctx, a, name, s, diag, c, side) {
|
|
|
78
85
|
for (let t = a.t ?? 0.45; t >= (a.tMin ?? 0.15); t -= 0.01) if (len(sub(at(t), sh)) <= r) return at(t);
|
|
79
86
|
return at(a.tMin ?? 0.15);
|
|
80
87
|
}
|
|
81
|
-
case "object": {
|
|
88
|
+
case "object": {
|
|
89
|
+
if (a.grasp && a.local === undefined) return anchorSocket(ctx, name, s, diag, c, side).p;
|
|
90
|
+
const o = objectFrame(ctx, a, name, { s, diag, c }, side);
|
|
91
|
+
return add(o.pos, apply(o.R, a.local ?? [0, 0, 0]));
|
|
92
|
+
}
|
|
82
93
|
case "landmark": return bonePoint(s, I.Head, sk.landmarks[a.name] ?? (() => { throw new Error(`MOTION_SPEC_INVALID: unknown landmark "${a.name}"`); })());
|
|
83
94
|
case "prop": {
|
|
84
|
-
const p = diag.props?.[a.prop]?.[a.point];
|
|
95
|
+
const p = propOnCarry(ctx, a, name, diag?.frame) ?? diag.props?.[a.prop]?.[a.point];
|
|
85
96
|
if (!p) throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" needs prop ${a.prop}.${a.point}, which is held by an arm solved later`);
|
|
86
97
|
return add(p, a.offset ?? [0, 0, 0]);
|
|
87
98
|
}
|
|
99
|
+
case "person": {
|
|
100
|
+
const p = personJoint(ctx.scene, a.person, a.bone, diag?.frame ?? 0);
|
|
101
|
+
if (!p) throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" names person "${a.person}" bone "${a.bone}", which the scene does not have (people: ${peopleList(ctx.scene)})`);
|
|
102
|
+
return add(p, a.offset ?? [0, 0, 0]);
|
|
103
|
+
}
|
|
88
104
|
default: throw new Error(`MOTION_SPEC_INVALID: anchor "${name}" has unknown kind "${a.kind}"`);
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A point of a prop on a hand that carries an object, at a frame that hand does not hold it: where the point sits on
|
|
110
|
+
* the object at the grab (or the release) nearest in time, carried with the object (at rest before the grab, let go
|
|
111
|
+
* after the release). In hand the point rides the palm; before the grab the palm still turns into its grip (and after
|
|
112
|
+
* the release out of it), and a point held off it swept through the other arm's reach: session 02263581's barbell put
|
|
113
|
+
* the left hand on a point 55 cm off the right palm, and the left elbow jumped 28 cm in a frame at the grab and the
|
|
114
|
+
* release. Read off the object, the point is where the left hand takes the bar, and meets the palm's at the grab. null
|
|
115
|
+
* when no carry of that hand applies (the palm's point is used).
|
|
116
|
+
*/
|
|
117
|
+
function propOnCarry(ctx, a, name, f) {
|
|
118
|
+
const hand = ctx.spec.props?.[a.prop]?.hand;
|
|
119
|
+
if (!hand || f === undefined || !ctx.solvedAt || !ctx.carrySpan) return null;
|
|
120
|
+
let pick = null;
|
|
121
|
+
for (const [id, o] of Object.entries(ctx.spec.objects ?? {})) {
|
|
122
|
+
if (!isCarried(o) || ctx.spec.props?.[o.prop]?.hand !== hand) continue;
|
|
123
|
+
const [grab, release] = ctx.carrySpan(o);
|
|
124
|
+
if (f >= grab && f <= release) return null;
|
|
125
|
+
const at = f < grab ? grab : release;
|
|
126
|
+
if (!pick || Math.abs(f - at) < Math.abs(f - pick.at)) pick = { id, at };
|
|
127
|
+
}
|
|
128
|
+
if (!pick) return null;
|
|
129
|
+
const there = nested(ctx, name, pick.id, pick.at, () => ctx.solvedAt(pick.at).diag.props?.[a.prop]?.[a.point]);
|
|
130
|
+
const was = ctx.objectPose(pick.id, pick.at), now = ctx.objectPose(pick.id, f);
|
|
131
|
+
return there && was && now ? add(now.pos, apply(now.R, apply(T(was.R), sub(there, was.pos)))) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The socket (../sockets.js) an object anchor with `grasp` takes the hand by, in world space on its object's pose that
|
|
136
|
+
* frame: { p, axis?, from?, thumb? }, p the anchor's point (its `local` when it has one). Its `socket`, else the one
|
|
137
|
+
* nearest its `local` point or, without one, nearest the shoulder of the hand: picked once per compile (ctx.sockets),
|
|
138
|
+
* so the hand does not hop between sockets. An object with no socket gives the bare point: the palm faces it.
|
|
139
|
+
*/
|
|
140
|
+
export function anchorSocket(ctx, name, s, diag, c, side) {
|
|
141
|
+
const a = ctx.spec.anchors[name], now = { s, diag, c }, poseOf = id => objectFrame(ctx, { ...a, object: id }, name, now, side);
|
|
142
|
+
const local = a.local && (o => add(o.pos, apply(o.R, a.local)))(poseOf(a.object));
|
|
143
|
+
ctx.sockets ??= new Map();
|
|
144
|
+
if (!ctx.sockets.has(name)) {
|
|
145
|
+
const all = socketsOf(ctx.scene, a.object), I = ctx.sk.index, near = local ?? s.P[I[side ? `${SIDES[side]}Arm` : "Hips"]];
|
|
146
|
+
const pick = a.socket ? socketNamed(ctx.scene, a.object, a.socket) : all.map(k => ({ k, d: socketDistance(socketWorld(k, poseOf(k.owner)), near) })).sort((x, y) => x.d - y.d)[0]?.k;
|
|
147
|
+
ctx.sockets.set(name, pick ?? null);
|
|
148
|
+
}
|
|
149
|
+
const sock = ctx.sockets.get(name), w = sock ? socketWorld(sock, poseOf(sock.owner)) : { p: local ?? poseOf(a.object).pos };
|
|
150
|
+
if (local) w.p = local;
|
|
151
|
+
// a socket with neither an axis nor a side (a box, a wall): the palm faces against the face of the body nearest the point
|
|
152
|
+
if (!w.axis && !w.from) w.from = faceFrom(partsOf(ctx.scene, a.object).map(id => ({ ...poseOf(id), half: ctx.scene.objects[id].half })), w.p);
|
|
153
|
+
return w;
|
|
154
|
+
}
|
|
155
|
+
|
|
92
156
|
/** Points of every prop held by hand k, in world space, written to diag.props. */
|
|
93
157
|
export function measureProps(ctx, k, s, diag) {
|
|
94
158
|
const { sk } = ctx;
|
|
@@ -33,7 +33,7 @@ import { fixOptions, applyFixes } from "../fix/index.js";
|
|
|
33
33
|
import { fitWeight } from "../fix/self.js";
|
|
34
34
|
import { holdHook } from "../object-keys.js";
|
|
35
35
|
import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
|
|
36
|
-
import { objectPoseAt, handOf, restingAt } from "../object-pose.js";
|
|
36
|
+
import { objectPoseAt, handOf, restingAt, frameAt, spanOf } from "../object-pose.js";
|
|
37
37
|
|
|
38
38
|
const smooth = x => x * x * (3 - 2 * x);
|
|
39
39
|
|
|
@@ -180,10 +180,14 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
|
|
|
180
180
|
ctx.objectHand = (id, f) => handOf(out, id, f);
|
|
181
181
|
// a keyed carry's grab offset is taken where it rests at grab (object-keys.js; a fall before grab: where it landed)
|
|
182
182
|
ctx.restingAt = (id, f) => restingAt(out, id, f);
|
|
183
|
+
// a prop anchor outside its hand's carry reads the carry's grab (release) frame (anchors.js propOnCarry)
|
|
184
|
+
ctx.carrySpan = o => spanOf(out, o);
|
|
185
|
+
ctx.solvedAt = f => frameAt(out, f);
|
|
183
186
|
const engine = {
|
|
184
187
|
rebake(keys) {
|
|
185
188
|
out.keys = keys;
|
|
186
189
|
out.baked = bakeChannels(keys, frameCount, flowOptions(spec.flow));
|
|
190
|
+
ctx.socketTurns = new Map();
|
|
187
191
|
steadyHead(out.baked, keys, frameCount);
|
|
188
192
|
// a held pose breathes and shifts its weight (idle.js): offsets solveFrame adds, contacts and out.baked stay as keyed
|
|
189
193
|
ctx.idle = idleLayer(out.baked, { spec, sk, frameCount });
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// forearm's neutral roll. A thin body (a cup, a can) keeps the neutral offset: its aim keys pick the
|
|
25
25
|
// roll the arm holds most easily from it, and the cylindrical grip measured worse on every bench cup.
|
|
26
26
|
// Those frames read the carried body where it rested before the grab, never a pose the hand moves.
|
|
27
|
+
// An anchor that takes a grasp (an object anchor with `grasp`) turns the hand by its socket instead (socket-grip.js).
|
|
27
28
|
import { add, sub, scale, norm, cross, dot, len, apply, ry, mul, T, axisRot } from "./math.js";
|
|
28
29
|
import { palmAxes, twoBone } from "./limbs.js";
|
|
29
30
|
import { aimArm, orient } from "./hold.js";
|
|
@@ -31,6 +32,7 @@ import { ELBOW_REST, WRIST_COMFORT, wristNeed } from "./arm-orient.js";
|
|
|
31
32
|
import { resolveAnchor } from "./anchors.js";
|
|
32
33
|
import { sdBox } from "../qa/geometry.js";
|
|
33
34
|
import { partsOf } from "../object-pose.js";
|
|
35
|
+
import { graspAnchor, socketCarry, socketHand, keepSocketTurn, ownWrist } from "./socket-grip.js";
|
|
34
36
|
|
|
35
37
|
const NEAR = 0.06, THIN = 0.045, ALIGN = 0.3, PREFER = 30, DEG = 180 / Math.PI, ITER = 2, UP = [0, 1, 0];
|
|
36
38
|
const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
|
|
@@ -141,9 +143,13 @@ function comfortable(F, now, twist) {
|
|
|
141
143
|
*/
|
|
142
144
|
export function gripHand(ctx, sk, s, side, k, c, f, diag) {
|
|
143
145
|
// a wrist the spec turns on this frame is the agent's (or a skill's) own; a neutral 0 asks for nothing
|
|
144
|
-
|
|
145
|
-
const on = activeFit(ctx.spec, c, k);
|
|
146
|
+
// a hand that takes a grasp (socket-grip.js) yields only to the agent's own wrist keys, not to a skill's (a walk's swing)
|
|
147
|
+
const on = activeFit(ctx.spec, c, k), carry = socketCarry(ctx, k, f), grasp = on && on.w >= 1e-3 && graspAnchor(ctx.spec, on.fit.anchor);
|
|
148
|
+
if (carry || grasp ? ownWrist(ctx.spec, k) : Math.abs(c[`wtwist_${k}`] ?? 0) > 1e-3 || Math.abs(c[`wflex_${k}`] ?? 0) > 1e-3) return;
|
|
149
|
+
// a hand that holds what it carries by a socket keeps the grasp it took on the grab frame
|
|
150
|
+
if (carry && f > carry.grab) return carry.keyed ? undefined : keepSocketTurn(ctx, sk, s, side, k, carry);
|
|
146
151
|
if (!on || on.w < 1e-3) return;
|
|
152
|
+
if (grasp) return socketHand(ctx, sk, s, side, k, c, diag, on, st => effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}));
|
|
147
153
|
// a hand that carries something on this frame holds that (its carry's grip), whatever its fit's anchor
|
|
148
154
|
if (Object.keys(ctx.spec.objects ?? {}).some(id => ctx.spec.objects[id]?.prop && ctx.objectHand?.(id, f) === k && ownGrip(ctx, id, k, f) === null)) return;
|
|
149
155
|
const now = { s, diag, c }, anchor = ctx.spec.anchors?.[on.fit.anchor];
|
|
@@ -103,16 +103,21 @@ export function twoBone(sk, s, root, mid, end, target, pole, restBend, stop = nu
|
|
|
103
103
|
return inside || d - dc;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Aim a foot: Foot->ToeBase along yaw/pitch of its rest direction, sole kept level. With `tilt` (pose.js tiltFrame: a body
|
|
108
|
+
* turned well past upright) the yaw and pitch are read in that frame and the sole levels to its up, so a foot in a flip
|
|
109
|
+
* points with the shin instead of staying flat to the floor; no toe-off then (the toes are off the ground).
|
|
110
|
+
*/
|
|
111
|
+
export function orientFoot(sk, s, side, yawDeg, pitchDeg, tilt = null) {
|
|
108
112
|
const foot = sk.index[`${side}Foot`], toe = sk.index[`${side}ToeBase`];
|
|
109
113
|
const rest = sub(sk.restWorldPos[toe], sk.restWorldPos[foot]);
|
|
110
|
-
const want = apply(ry(yawDeg), apply(rx(pitchDeg), rest));
|
|
114
|
+
const want = tilt ? apply(tilt.R, apply(ry(yawDeg - tilt.yaw), apply(rx(pitchDeg), rest))) : apply(ry(yawDeg), apply(rx(pitchDeg), rest));
|
|
115
|
+
const up = tilt ? apply(tilt.R, [0, 1, 0]) : [0, 1, 0];
|
|
111
116
|
// level the sole: the foot's own carried up vector goes to world up (no roll from the leg IK)
|
|
112
|
-
rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want,
|
|
117
|
+
rotateSubtree(sk, s, foot, s.P[foot].slice(), align(sub(s.P[toe], s.P[foot]), carried(sk, s, foot, [0, 1, 0]), want, up));
|
|
113
118
|
// Toe-off: bend the toe joint so the toes stay flat on the ground while the heel rises.
|
|
114
119
|
const tip = sk.index[`${side}Toe_End`];
|
|
115
|
-
if (pitchDeg > 0 && tip !== undefined) {
|
|
120
|
+
if (!tilt && pitchDeg > 0 && tip !== undefined) {
|
|
116
121
|
const seg = sub(s.P[tip], s.P[toe]), flat = [seg[0], 0, seg[2]];
|
|
117
122
|
if (len(flat) > 1e-6) rotateSubtree(sk, s, toe, s.P[toe].slice(), align(seg, [0, 1, 0], flat, [0, 1, 0]));
|
|
118
123
|
}
|
|
@@ -144,11 +149,13 @@ export function wrist(sk, s, side, twistDeg, flexDeg) {
|
|
|
144
149
|
rotateSubtree(sk, s, h, s.P[h].slice(), axisRot(axT, clamp(twistDeg, -85, 85)));
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
// Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl].
|
|
152
|
+
// Finger curl presets, degrees per phalanx (1, 2, 3); thumb is [opposition, curl, curl]. A fist and a grip keep the
|
|
153
|
+
// relaxed thumb here and close it with THUMB_WRAP: these thumb curls turn about the knuckle line and the hand's long axis,
|
|
154
|
+
// which only swings a thumb out to the side (a fist read as a thumbs-up).
|
|
148
155
|
const FINGER_PRESETS = {
|
|
149
156
|
relaxed: { Index: [12, 18, 10], Middle: [15, 20, 12], Ring: [18, 22, 12], Pinky: [20, 24, 14], Thumb: [15, 10, 8] },
|
|
150
|
-
fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [
|
|
151
|
-
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [
|
|
157
|
+
fist: { Index: [80, 95, 60], Middle: [85, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [15, 10, 8] },
|
|
158
|
+
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [15, 10, 8] },
|
|
152
159
|
cigarette: { Index: [8, 10, 6], Middle: [10, 12, 8], Ring: [60, 75, 45], Pinky: [65, 80, 45], Thumb: [35, 25, 20] },
|
|
153
160
|
point: { Index: [0, 0, 0], Middle: [80, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [40, 30, 30] },
|
|
154
161
|
};
|
|
@@ -166,5 +173,25 @@ export function fingers(sk, s, side, weights) {
|
|
|
166
173
|
rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(ax, sign * curl(f, k)));
|
|
167
174
|
}
|
|
168
175
|
}
|
|
176
|
+
const wrap = [0, 1, 2, 3].map(i => Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * (THUMB_WRAP[p]?.[i] ?? 0), 0));
|
|
177
|
+
if (wrap.some(Boolean)) wrapThumb(sk, s, side, wrap);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A closing hand's thumb, by preset weight after the relaxed one, degrees: turned in about the hand's long axis toward the
|
|
181
|
+
// palm and across it, then flexed at its two joints about their own axes (bone x palm normal). The fist's thumb lies over
|
|
182
|
+
// the index and middle fingers' middle phalanges; the grip's closes around a handle against the index finger. Searched on
|
|
183
|
+
// both rigs within the joints' ranges (thumb tip to the index's middle phalanx: fist 8.4 -> 3.7 cm Y Bot, 6.9 -> 2.4 X Bot).
|
|
184
|
+
const THUMB_WRAP = { fist: [-70, -5, 5, 80], grip: [-30, -30, 30, 10] };
|
|
185
|
+
function wrapThumb(sk, s, side, [oppose, across, flex2, flex3]) {
|
|
186
|
+
const sign = side === "Left" ? -1 : 1, J = k => sk.index[`${side}HandThumb${k}`];
|
|
187
|
+
if (J(1) === undefined) return;
|
|
188
|
+
const { d, n } = palmAxes(sk, s, side);
|
|
189
|
+
rotateSubtree(sk, s, J(1), s.P[J(1)].slice(), axisRot(d, sign * oppose));
|
|
190
|
+
// n = d x l flips with the mirror, so the turn across the palm keeps its sign on both hands
|
|
191
|
+
rotateSubtree(sk, s, J(1), s.P[J(1)].slice(), axisRot(n, across));
|
|
192
|
+
for (const [k, deg] of [[2, flex2], [3, flex3]]) {
|
|
193
|
+
const ax = norm(cross(norm(sub(s.P[J(k + 1)], s.P[J(k)])), n));
|
|
194
|
+
rotateSubtree(sk, s, J(k), s.P[J(k)].slice(), axisRot(ax, sign * deg));
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
|
|
@@ -82,6 +82,21 @@ function hangOf(sk, down) {
|
|
|
82
82
|
return hang;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The frame the knee and elbow poles and the feet are read in once the whole body turns well past upright (a flip, a
|
|
87
|
+
* cartwheel): the root's yaw, pitch and roll, the tilt eased in from TILT_FROM to TILT_FULL degrees (the larger of the
|
|
88
|
+
* wrapped pitch and roll). Under TILT_FROM it is null and the poles turn with the yaw only, as they always have, so an
|
|
89
|
+
* upright take is unchanged.
|
|
90
|
+
*/
|
|
91
|
+
const TILT_FROM = 60, TILT_FULL = 120;
|
|
92
|
+
const wrap180 = a => a - 360 * Math.round(a / 360);
|
|
93
|
+
export function tiltFrame(yaw, pitch, roll) {
|
|
94
|
+
const p = wrap180(pitch), r = wrap180(roll), u = (Math.max(Math.abs(p), Math.abs(r)) - TILT_FROM) / (TILT_FULL - TILT_FROM);
|
|
95
|
+
if (u <= 0) return null;
|
|
96
|
+
const w = u >= 1 ? 1 : u * u * (3 - 2 * u);
|
|
97
|
+
return { yaw, R: mul(ry(yaw), mul(rx(p * w), rz(r * w))) };
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
export function pose(sk, c, hooks = {}) {
|
|
86
101
|
const s = restState(sk);
|
|
87
102
|
const diag = {};
|
|
@@ -107,7 +122,7 @@ export function pose(sk, c, hooks = {}) {
|
|
|
107
122
|
rotateSubtree(sk, s, hips, s.P[hips].slice(), mul(ry(yaw), mul(rx(num(c.pitch)), rz(num(c.roll)))));
|
|
108
123
|
const shift = sub(v3(c.root, s.P[hips]), s.P[hips]);
|
|
109
124
|
for (let j = 0; j < s.P.length; j++) s.P[j] = add(s.P[j], shift);
|
|
110
|
-
const body = ry(yaw);
|
|
125
|
+
const tilt = tiltFrame(yaw, num(c.pitch), num(c.roll)), body = tilt?.R ?? ry(yaw);
|
|
111
126
|
// Legs: two-bone IK to the ankle targets, the knee no further folded than its stop (limbs.js KNEE_STOP), then aim the feet.
|
|
112
127
|
for (const [k, side] of Object.entries(SIDES)) {
|
|
113
128
|
if (!c[`foot_${k}`]) continue;
|
|
@@ -117,7 +132,7 @@ export function pose(sk, c, hooks = {}) {
|
|
|
117
132
|
// a target nearer the hip than the knee folds leaves a residual (leg_miss); leg_fold says what it asked
|
|
118
133
|
const fold = askedFold(sk, s, side, target, LEG);
|
|
119
134
|
if (fold > KNEE_FOLD) diag[`leg_fold_${k}`] = fold;
|
|
120
|
-
orientFoot(sk, s, side, num(c[`fyaw_${k}`], yaw), num(c[`fpitch_${k}`]));
|
|
135
|
+
orientFoot(sk, s, side, num(c[`fyaw_${k}`], yaw), num(c[`fpitch_${k}`]), tilt);
|
|
121
136
|
}
|
|
122
137
|
// Arms: blend the FK hand toward the hook's target, poles blended the same way.
|
|
123
138
|
for (const k of hooks.armOrder ?? ["r", "l"]) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The hand of a fit whose anchor takes a grasp (an object anchor with `grasp`, ../sockets.js) holds the object by its
|
|
2
|
+
// socket: the palm frame comes from the socket (the knuckle line along its axis, the palm against the side the hand
|
|
3
|
+
// comes from), not from the forearm's roll rule (grip.js). With no side given, the hand rolls about the axis to where
|
|
4
|
+
// the arm holds it most easily, and where the forearm cannot come across the axis the knuckle line tilts on it (an
|
|
5
|
+
// oblique grip, priced, up to TILT deg). The fingers' point stays where the fit put it; the elbow swivels to the pole
|
|
6
|
+
// that leaves the wrist least strained (arm-orient.js, as a held object's hand, hold.js), the forearm takes its share of
|
|
7
|
+
// the turn and the wrist the rest within its limits. Weighted by the fit's weight, so the hand turns into the grasp on
|
|
8
|
+
// the reach and out of it on the release.
|
|
9
|
+
// A hand that carries what it holds by a socket without keys keeps, after the grab, the wrist turn it had on the grab
|
|
10
|
+
// frame against the forearm (diag.socket_<k>.rel, measured on the plain forearm before the turn): the object rides the
|
|
11
|
+
// palm as it was taken (object-pose.js keeps the grab-frame offset), so neither the hand nor the object turns back. A
|
|
12
|
+
// keyed carry's hand follows the keys (hold.js), whose grab offset is taken on that same grasp (object-keys.js attach).
|
|
13
|
+
import { sub, norm, mul, T, axisRot } from "./math.js";
|
|
14
|
+
import { palmAxes, twoBone } from "./limbs.js";
|
|
15
|
+
import { aimArm, orient } from "./hold.js";
|
|
16
|
+
import { ELBOW_REST, swivel } from "./arm-orient.js";
|
|
17
|
+
import { anchorSocket } from "./anchors.js";
|
|
18
|
+
import { socketPalm } from "../sockets.js";
|
|
19
|
+
import { partsOf } from "../object-pose.js";
|
|
20
|
+
|
|
21
|
+
const ITER = 3, ROLLS = 12, TEMP = 0.05, PULL = 0.3, DEG = 180 / Math.PI, TILT = 40, SLANT = 1, STEADY = 0.3;
|
|
22
|
+
const TILTS = [-TILT, -TILT / 2, 0, TILT / 2, TILT];
|
|
23
|
+
|
|
24
|
+
/** The Boltzmann-weighted mean of [value, cost] pairs at a softer temperature (STEADY): it moves smoothly between them. */
|
|
25
|
+
function mean(all) {
|
|
26
|
+
const low = Math.min(...all.map(x => x[1])), e = all.map(([a, c]) => [a, Math.exp(-(c - low) / STEADY)]);
|
|
27
|
+
return e.reduce((m, [a, x]) => m + a * x, 0) / e.reduce((m, [, x]) => m + x, 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The Boltzmann-weighted circular mean of ROLLS angles under cost(th) (second-hand.js softmin). */
|
|
31
|
+
function softmin(cost) {
|
|
32
|
+
const all = Array.from({ length: ROLLS }, (_, i) => { const th = (2 * Math.PI * i) / ROLLS - Math.PI; return [th, cost(th)]; }), low = Math.min(...all.map(x => x[1]));
|
|
33
|
+
const m = all.reduce((acc, [th, v]) => { const e = Math.exp(-(v - low) / TEMP); return [acc[0] + e * Math.cos(th), acc[1] + e * Math.sin(th)]; }, [0, 0]);
|
|
34
|
+
return Math.atan2(m[1], m[0]);
|
|
35
|
+
}
|
|
36
|
+
const cols = ({ d, l, n }) => [[d[0], l[0], n[0]], [d[1], l[1], n[1]], [d[2], l[2], n[2]]];
|
|
37
|
+
|
|
38
|
+
/** True when the anchor named `name` takes a grasp (an object anchor with `grasp`). */
|
|
39
|
+
export const graspAnchor = (spec, name) => spec.anchors?.[name]?.kind === "object" && !!spec.anchors[name].grasp;
|
|
40
|
+
|
|
41
|
+
/** True when the agent's own keys (spec.keys, not a skill's or a generator's) turn hand k's wrist (wtwist_<k>, wflex_<k>). */
|
|
42
|
+
export const ownWrist = (spec, k) => (spec.keys ?? []).some(key => (!key.src || key.src.startsWith("keys[")) && (key.set?.[`wtwist_${k}`] !== undefined || key.set?.[`wflex_${k}`] !== undefined));
|
|
43
|
+
|
|
44
|
+
/** True when a grasp fit of hand k is anchored on body or group `id` (on it, a part of it, or a group holding it). */
|
|
45
|
+
export function graspedBy(spec, scene, id, k) {
|
|
46
|
+
const parts = new Set(partsOf(scene, id));
|
|
47
|
+
return (spec.fits ?? []).some(fit => {
|
|
48
|
+
if (fit.side !== k || !graspAnchor(spec, fit.anchor)) return false;
|
|
49
|
+
const at = spec.anchors[fit.anchor].object;
|
|
50
|
+
return at === id || parts.has(at) || partsOf(scene, at).some(p => parts.has(p));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The carry hand k holds at frame f by a socket (graspedBy): { id, grab, keyed } for that spec.objects carry, else null. */
|
|
55
|
+
export function socketCarry(ctx, k, f) {
|
|
56
|
+
const { spec, scene } = ctx;
|
|
57
|
+
if (!ctx.objectHand || !(spec.fits ?? []).some(fit => fit.side === k && graspAnchor(spec, fit.anchor))) return null;
|
|
58
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) if (o?.prop && ctx.objectHand(id, f) === k && graspedBy(spec, scene, id, k)) return { id, grab: ctx.carrySpan(o)[0], keyed: !!o.keys?.length };
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* socketHand(ctx, sk, s, side, k, c, diag, on, effector): turn the hand of fit `on` ({ fit, w }) into its anchor's
|
|
64
|
+
* socket grasp (see above); effector(st) is the fit's effector point on a pose.
|
|
65
|
+
*/
|
|
66
|
+
export function socketHand(ctx, sk, s, side, k, c, diag, on, effector) {
|
|
67
|
+
const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`], plain = s.R[fa];
|
|
68
|
+
const w = anchorSocket(ctx, on.fit.anchor, s, diag, c, k), F = effector(s), fwd = norm(sub(s.P[h], s.P[fa]));
|
|
69
|
+
const type = ctx.spec.anchors[on.fit.anchor].grasp, base = socketPalm(side, w, type, fwd, c.yaw);
|
|
70
|
+
const measure = st => ({ pos: effector(st), R: cols(palmAxes(sk, st, side)) }), now = measure(s);
|
|
71
|
+
// free to roll about its axis (no `from`): the roll the arm holds most easily, the Boltzmann mean of ROLLS rolls pulled
|
|
72
|
+
// lightly toward the fingers on the forearm's line, so the hand closes on the bar from the side the arm comes from
|
|
73
|
+
// then the knuckle line tilts on the bar (an oblique grip) up to TILT deg, priced, where the forearm cannot come across it
|
|
74
|
+
const strain = R => { const t = aimArm(sk, s, side, { pos: F, R }, now, 1, "palm"); return swivel(sk, s, side, t.goal, t.wristAt(t.goal), t.plane, ELBOW_REST).cost; };
|
|
75
|
+
const col = (M, i) => [M[0][i], M[1][i], M[2][i]], turned = th => mul(axisRot(col(base, 1), th * DEG), base);
|
|
76
|
+
const free = w.axis && !w.from && type !== "palm", roll = free ? softmin(th => strain(turned(th)) + PULL * (1 - Math.cos(th))) : 0;
|
|
77
|
+
const rolled = turned(roll), tilted = a => mul(axisRot(col(rolled, 2), a), rolled);
|
|
78
|
+
const tilt = w.axis && type !== "palm" ? mean(TILTS.map(a => [a, strain(tilted(a)) + SLANT * (a / TILT) ** 2])) : 0;
|
|
79
|
+
const R = tilted(tilt);
|
|
80
|
+
const { goal, plane, wristAt } = aimArm(sk, s, side, { pos: F, R }, now, on.w, "palm");
|
|
81
|
+
const arm = swivel(sk, s, side, goal, wristAt(goal), plane, ELBOW_REST, { w: on.w });
|
|
82
|
+
let need = { twist: 0, flex: 0, dev: 0 };
|
|
83
|
+
for (let i = 0; i < ITER; i++) {
|
|
84
|
+
twoBone(sk, s, a, fa, h, wristAt(i ? s.R[h] : goal), arm.pole, ELBOW_REST, "elbow");
|
|
85
|
+
need = orient(sk, s, side, h, fa, goal);
|
|
86
|
+
}
|
|
87
|
+
diag[`grip_${k}`] = { w: on.w, socket: true, ...need };
|
|
88
|
+
diag[`socket_${k}`] = { rel: mul(T(plain), s.R[h]) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The hand of a socket carry after its grab keeps the grab frame's wrist turn against the forearm (see above), read
|
|
93
|
+
* once per bake (ctx.socketTurns) off the grab frame solved without the keys: a keyed carry's curve is built on
|
|
94
|
+
* frames solved without keys, which come here, so the keyed solve of the grab frame would need that curve again.
|
|
95
|
+
*/
|
|
96
|
+
export function keepSocketTurn(ctx, sk, s, side, k, carry) {
|
|
97
|
+
const key = `${k}:${carry.grab}`;
|
|
98
|
+
ctx.socketTurns ??= new Map();
|
|
99
|
+
if (!ctx.socketTurns.has(key)) {
|
|
100
|
+
ctx.socketTurns.set(key, null);
|
|
101
|
+
ctx.socketTurns.set(key, (ctx.hold?.plainFrame ?? ctx.solvedAt)(carry.grab)?.diag?.[`socket_${k}`]?.rel ?? null);
|
|
102
|
+
}
|
|
103
|
+
const rel = ctx.socketTurns.get(key);
|
|
104
|
+
if (!rel) return;
|
|
105
|
+
const I = sk.index, fa = I[`${side}ForeArm`], h = I[`${side}Hand`];
|
|
106
|
+
orient(sk, s, side, h, fa, mul(s.R[fa], rel));
|
|
107
|
+
}
|
|
@@ -15,7 +15,7 @@ import { isFree } from "../free-motion.js";
|
|
|
15
15
|
import { checkRef } from "../refs.js";
|
|
16
16
|
import { partsOf } from "../object-pose.js";
|
|
17
17
|
|
|
18
|
-
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce? }";
|
|
18
|
+
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce?, tumble? }";
|
|
19
19
|
const isQuat = q => Array.isArray(q) && q.length === 4 && q.every(Number.isFinite) && Math.abs(Math.hypot(...q) - 1) <= 0.01;
|
|
20
20
|
const isDir = v => isVec3(v) && Math.hypot(...v) > 1e-9;
|
|
21
21
|
|
|
@@ -77,7 +77,7 @@ function frameOk(err, at, v, frames) {
|
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/** objects.<id>.fall (C17): { at, onto?, bounce? } or a list, each on its own frame; a carry falls only out of hand. */
|
|
80
|
+
/** objects.<id>.fall (C17): { at, onto?, bounce?, tumble? } or a list, each on its own frame; a carry falls only out of hand. */
|
|
81
81
|
function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
82
82
|
const list = Array.isArray(o.fall) ? o.fall : [o.fall], seen = new Set();
|
|
83
83
|
if (!list.length) err(`${at}.fall is an empty list; give ${FALL} or a list of them`);
|
|
@@ -94,6 +94,7 @@ function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
|
94
94
|
}
|
|
95
95
|
if (fl.onto !== undefined) objectOk(err, scene, `${fat}.onto`, fl.onto, { groups: true });
|
|
96
96
|
if (fl.bounce !== undefined && !(Number.isFinite(fl.bounce) && fl.bounce >= 0 && fl.bounce <= 0.6)) err(`${fat}.bounce must be 0..0.6 (the share of its landing speed it bounces back with; 0 lands dead), got ${fmt(fl.bounce)}`);
|
|
97
|
+
if (fl.tumble !== undefined && typeof fl.tumble !== "boolean") err(`${fat}.tumble must be true (it rolls to rest on a stable face, the default) or false (it drops straight down as it was), got ${fmt(fl.tumble)}`);
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Validation of the spec parts other than keys: anchors, props, fits, generators, checks and
|
|
2
2
|
// fix; objects (validate-objects.js) and goals (validate-goals.js) run from here too. Unknown
|
|
3
3
|
// fields go through opts.unknown, which ignores them on a lenient reload. See validate.js.
|
|
4
|
-
import { didYouMean, suggestField, CONVENTIONS, isVec3, fmt } from "./vocab.js";
|
|
4
|
+
import { didYouMean, suggestField, CONVENTIONS, isVec3, fmt, GRASPS } from "./vocab.js";
|
|
5
5
|
import { FIX_KEYS } from "../fix/index.js";
|
|
6
6
|
import { QA_LIMITS } from "../qa/checks.js";
|
|
7
7
|
import { ANCHOR_FIELDS, PROP_FIELDS, FIT_FIELDS, WALK_FIELDS, CHECK_FIELDS, CHECK_ENTRY_FIELDS } from "../schema/fields.js";
|
|
8
8
|
import { boneOk, objectOk, refOk } from "./validate-common.js";
|
|
9
9
|
import { validateObjects } from "./validate-objects.js";
|
|
10
10
|
import { validateGoals } from "./validate-goals.js";
|
|
11
|
+
import { personJoint, peopleList } from "../people.js";
|
|
12
|
+
import { socketsOf, socketNamed } from "../sockets.js";
|
|
11
13
|
|
|
12
14
|
const SIDE = new Set(["l", "r"]);
|
|
13
15
|
const isObject = v => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
@@ -28,7 +30,12 @@ function validateAnchors(spec, err, { sk, scene, unknown = err }) {
|
|
|
28
30
|
if ((a.kind === "world" || a.kind === "body") && a.p === undefined) err(`${at} (${a.kind}) needs p: [x, y, z]${a.kind === "body" ? `, ${CONVENTIONS.pole}` : ""}`);
|
|
29
31
|
if (a.kind === "bone") boneOk(err, sk, at, a.bone);
|
|
30
32
|
if (a.kind === "segment") { boneOk(err, sk, `${at}.from`, a.from); boneOk(err, sk, `${at}.to`, a.to); if (a.reach !== undefined && !SIDE.has(a.reach)) err(`${at}.reach must be "l" or "r"`); }
|
|
31
|
-
if (a.kind === "object"
|
|
33
|
+
if (a.kind === "object" && objectOk(err, scene, at, a.object, { groups: true })) {
|
|
34
|
+
if (a.grasp !== undefined && !GRASPS.includes(a.grasp)) err(`${at}.grasp must be one of ${GRASPS.join(", ")}, got ${fmt(a.grasp)}`);
|
|
35
|
+
if (a.socket !== undefined && scene?.objects && !socketNamed(scene, a.object, a.socket)) err(`${at}.socket: "${a.object}" has no socket ${fmt(a.socket)} (sockets: ${socketsOf(scene, a.object).map(k => k.name).join(", ") || "none"})`);
|
|
36
|
+
if (a.socket !== undefined && a.grasp === undefined) err(`${at}.socket needs grasp (${GRASPS.join(", ")}): the socket says how the hand takes it`);
|
|
37
|
+
}
|
|
38
|
+
if (a.kind === "person" && !personJoint(scene, a.person, a.bone ?? "", 0)) err(`${at} (person) needs person: the id of another character with an installed take (people: ${peopleList(scene)}) and bone: one of its joints, got ${fmt(a.person)} ${fmt(a.bone)}`);
|
|
32
39
|
if (a.kind === "landmark" && a.name !== "mouth") err(`${at}: the only landmark is "mouth", got ${fmt(a.name)}`);
|
|
33
40
|
if (a.kind === "prop" && !spec.props?.[a.prop]?.points?.[a.point] && a.point !== "origin") err(`${at} needs prop point ${a.prop}.${a.point}, which spec.props does not define`);
|
|
34
41
|
}
|
|
@@ -23,10 +23,10 @@ function checkValue(err, at, ch, v) {
|
|
|
23
23
|
if (!isVec3(v)) return err(`${at} ${ch} must be [x, y, z] (3 finite numbers), got ${fmt(v)}`);
|
|
24
24
|
if (POLE_CHANNELS.has(ch)) {
|
|
25
25
|
const l = Math.hypot(...v);
|
|
26
|
-
if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is ${CONVENTIONS.pole}`);
|
|
27
|
-
if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is ${CONVENTIONS.pole}`);
|
|
26
|
+
if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is a ${CONVENTIONS.pole}`);
|
|
27
|
+
if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is a ${CONVENTIONS.pole}`);
|
|
28
28
|
}
|
|
29
|
-
if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is ${CONVENTIONS.foot}`);
|
|
29
|
+
if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is the ${CONVENTIONS.foot}`);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
if (MAP_CHANNELS.has(ch)) {
|
|
@@ -11,36 +11,43 @@ export const MAP_CHANNELS = new Set(sided(["fingers"]));
|
|
|
11
11
|
// Channels the skills key (standIdle, walks): valid in a spec, left out of motion.describe so a first spec need not read them.
|
|
12
12
|
export const SKILL_CHANNELS = new Set(["arm_hang"]);
|
|
13
13
|
export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point"];
|
|
14
|
+
/** The grasps an object anchor takes a hand by (../sockets.js), and the finger preset each closes the hand to by default. */
|
|
15
|
+
export const GRASPS = ["power", "pinch", "hook", "palm"];
|
|
16
|
+
export const GRASP_FINGERS = { power: "grip", pinch: "cigarette", hook: "grip", palm: "relaxed" };
|
|
17
|
+
/** Where the held thing sits in the hand, [d, l, n] on the palm axes from the finger middles (n toward the palm; m): a bar in a
|
|
18
|
+
* power grip or a hook lies across the palm under the finger middles, a pinch is held at the fingertips, a flat palm presses with its centre. */
|
|
19
|
+
export const GRASP_POINTS = { power: [0, 0, 0.02], hook: [0, 0, 0.02], pinch: [0.025, 0, 0.01], palm: [-0.045, 0, 0.015] };
|
|
14
20
|
|
|
15
21
|
// [min, max, convention] for numeric channels whose values have a hard range.
|
|
16
22
|
const RANGES = [
|
|
17
|
-
[/^(armw)_[lr]$/, 0, 1, "0
|
|
23
|
+
[/^(armw)_[lr]$/, 0, 1, "0 FK, 1 IK to the hand target"],
|
|
18
24
|
[/^arm_hang$/, 0, 1, "1 = a free FK forearm hangs plumb"],
|
|
19
|
-
[/^w_.+_[lr]$/, 0, 1, "
|
|
20
|
-
[/^wtwist_[lr]$/, -85, 85, "
|
|
21
|
-
[/^wflex_[lr]$/, -70, 70, "
|
|
22
|
-
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch
|
|
23
|
-
|
|
24
|
-
[/^(
|
|
25
|
+
[/^w_.+_[lr]$/, 0, 1, "hand-target weight; a hand's sum <= 1"],
|
|
26
|
+
[/^wtwist_[lr]$/, -85, 85, "twist about the forearm, deg"],
|
|
27
|
+
[/^wflex_[lr]$/, -70, 70, "flex about the knuckle line, deg"],
|
|
28
|
+
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch, deg: + lifts the heel (toes flat), - lifts the toes"],
|
|
29
|
+
// the whole-body pitch and roll have none: they turn in full (flips, cartwheels; pose.js tiltFrame)
|
|
30
|
+
[/^(spine_pitch|head_pitch)$/, -120, 120, "degrees, + bends forward"],
|
|
31
|
+
[/^(spine_roll|head_roll)$/, -90, 90, "degrees"],
|
|
25
32
|
];
|
|
26
33
|
export const rangeOf = ch => RANGES.find(([re]) => re.test(ch)) ?? null;
|
|
27
34
|
|
|
28
35
|
export const CONVENTIONS = {
|
|
29
36
|
yaw: "yaw 0 faces +Z and + turns toward +X",
|
|
30
|
-
fyaw: "foot heading
|
|
31
|
-
pole: "
|
|
32
|
-
foot: "
|
|
33
|
-
hand: "
|
|
34
|
-
root: "
|
|
37
|
+
fyaw: "foot heading, as yaw",
|
|
38
|
+
pole: "body-frame direction (root yaw): +z forward, +x its left, +y up",
|
|
39
|
+
foot: "ANKLE target, world m (standing 0.087 X Bot, 0.105 Y Bot)",
|
|
40
|
+
hand: "WRIST target, world m",
|
|
41
|
+
root: "hips joint, world m (standing 1.03 X Bot, 0.99 Y Bot)",
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
/** What each channel family means (a family drops the _l/_r side), for motion.describe "channels". */
|
|
38
45
|
export const CHANNEL_DOCS = {
|
|
39
|
-
root: CONVENTIONS.root, yaw: `
|
|
40
|
-
spine_yaw: "degrees", spine_pitch: "
|
|
41
|
-
arm_rest: "FK arms down
|
|
46
|
+
root: CONVENTIONS.root, yaw: `deg; ${CONVENTIONS.yaw}`, pitch: "degrees", roll: "degrees",
|
|
47
|
+
spine_yaw: "degrees", spine_pitch: "deg, + bends forward", spine_roll: "degrees", head_yaw: "degrees", head_pitch: "degrees, + looks down", head_roll: "degrees",
|
|
48
|
+
arm_rest: "FK arms down, deg, default 72", arm_hang: "1 = a free FK forearm hangs plumb (standIdle and walks key it)", foot: CONVENTIONS.foot, fyaw: CONVENTIONS.fyaw, fpitch: "toe-down pitch, degrees, + heel up",
|
|
42
49
|
knee: `knee pole, ${CONVENTIONS.pole}`, armw: "0 = FK arm, 1 = IK to hand_* / the anchors", hand: CONVENTIONS.hand, elbow: `elbow pole, ${CONVENTIONS.pole}`,
|
|
43
|
-
wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing,
|
|
50
|
+
wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing, deg, + forward", abend: "FK elbow bend, deg",
|
|
44
51
|
fingers: "{ preset: weight 0..1 }",
|
|
45
52
|
};
|
|
46
53
|
export const channelFamily = ch => ch.replace(/_[lr]$/, "");
|
package/src/motion3/describe.js
CHANGED
|
@@ -81,7 +81,7 @@ const SCHEMAS = {
|
|
|
81
81
|
probes: () => listOf({ ...objectOf(PROBE_FIELDS, PROBE_TYPES, "probes", ["name"]), ...oneOfRequired(["distance", "position"]) }, { maxItems: 16 }),
|
|
82
82
|
channels: () => ({ type: "object", description: "key.set: channel -> value",
|
|
83
83
|
properties: Object.fromEntries([...VEC_CHANNELS, ...NUM_CHANNELS, ...MAP_CHANNELS].filter(ch => !SKILL_CHANNELS.has(ch)).map(ch => [ch, channelSchema(ch)])),
|
|
84
|
-
patternProperties: { "^w_.+_[lr]$": { type: "number", minimum: 0, maximum: 1, description: rangeOf("w_a_l")[3] }, "_vis$": { type: "number", description: "shown
|
|
84
|
+
patternProperties: { "^w_.+_[lr]$": { type: "number", minimum: 0, maximum: 1, description: rangeOf("w_a_l")[3] }, "_vis$": { type: "number", description: "shown > 0.5" } },
|
|
85
85
|
additionalProperties: { type: "number", description: "spec.channels, an object's or a check's when" } }),
|
|
86
86
|
refs: () => ({ type: "string", oneOf: REF_KINDS.map(k => ({ pattern: `^${k}:`, description: typeOf(REF_DOCS, k, "refs") })) }),
|
|
87
87
|
fix: () => ({ oneOf: [{ const: false }, objectOf(FIX_KEYS, FIX_TYPES, "fix")] }),
|
|
@@ -103,6 +103,8 @@ function describeSkill(name) {
|
|
|
103
103
|
const MEANT = { topics: ["objects", "channels"], skills: ["reachGrip"] };
|
|
104
104
|
/** Topics no round-2 session needed for a first spec: essentials names their parts, the full topic has the fields. */
|
|
105
105
|
const SHALLOW = ["checks"];
|
|
106
|
+
/** Skills essentials leaves to topic skills: a flip or cartwheel is no first spec's need, and essentials has no room. */
|
|
107
|
+
const FIRST_SPEC_SKIP = ["flip"];
|
|
106
108
|
|
|
107
109
|
/** A shared shape whose fields are mostly a skill's args (walk, walkTo): its own fields and `...<skill> args`. */
|
|
108
110
|
function shapeLine(d) {
|
|
@@ -134,7 +136,8 @@ function essentials() {
|
|
|
134
136
|
}
|
|
135
137
|
// use.skill without the enum: the skills list below names them
|
|
136
138
|
if (shared.use) shared.use = objectOf(USE_FIELDS, USE_TYPES, "use", ["skill", "f0"]);
|
|
137
|
-
|
|
139
|
+
// a skill no first spec needs (FIRST_SPEC_SKIP) is in topic skills only
|
|
140
|
+
const skills = SKILL_NAMES.filter(n => !FIRST_SPEC_SKIP.includes(n)).map(n => {
|
|
138
141
|
const { args, required = [], needs, frames, adds } = SKILL_INFO[n] ?? bad(`describe: skill ${n} has no entry in skills/info.js`);
|
|
139
142
|
return { name: n, args: briefType({ type: "object", properties: args, required }, { means: MEANT.skills.includes(n) }), needs, frames, adds };
|
|
140
143
|
});
|
package/src/motion3/docs.js
CHANGED
|
@@ -6,7 +6,7 @@ export const MOTION_ACTION_HELP = {
|
|
|
6
6
|
+ "Send spec, or base (an answered specId) + optional patch (JSON Patch); base alone or patch: [] re-measures (new probes). "
|
|
7
7
|
+ "spec = { schema: \"cozyclay.motion-spec.v1\", fps: 24, frames (number or \"auto\"), ... } (parts: motion.describe); any number may be { range: [lo, hi] } (searched). "
|
|
8
8
|
+ "probes (at most 16, request level; spec.probes: measured, not stored): { name, distance: [ref, ref] | position: ref, frames, every? }. "
|
|
9
|
-
+ "Refs need a prefix (joint: prop: anchor: landmark: object:; goals[].moved: a bare id too) and take loose bone names (any case); spec.checks need exact ones. "
|
|
9
|
+
+ "Refs need a prefix (joint: prop: anchor: landmark: object: person:; goals[].moved: a bare id too) and take loose bone names (any case); spec.checks need exact ones. "
|
|
10
10
|
+ "repair: { rounds } (1-8, default 4) applies group patches while severity drops. "
|
|
11
11
|
+ "qa judges only what is broken; speed, acceleration, balance, knee bend are observations unless checks.limits names them. "
|
|
12
12
|
+ "Answers frames, installed, specId, qa { ok, total, goals, groups (GOAL first, then severity; cause, hint, patch?) }, observations, delta (with base), repair, probes, timeline, fixed, search, skills. output.more: motion.report pages the rest.",
|