cozyclay 3.0.2 → 3.0.3

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/app/index.html +4 -4
  3. package/dist/assets/analytics-BibyE5ar.js +6 -0
  4. package/dist/assets/{app-34i-iUy1.js → app-DhRJ-PBz.js} +40 -40
  5. package/dist/assets/compile-worker-DqiA4AaN.js +5 -0
  6. package/dist/assets/{first-shot-handoff-CrGZ3n_O.js → first-shot-handoff-Qx7SKl38.js} +1 -1
  7. package/dist/assets/{landing-mq5bnqFE.js → landing-DYg87MDY.js} +1 -1
  8. package/dist/assets/scene-asset-sync-DBYQBR0T.js +20 -0
  9. package/dist/assets/{workflow-dUhurYUc.js → workflow-BSA22Ofr.js} +1 -1
  10. package/dist/cozyclay-package.json +1 -1
  11. package/dist/index.html +3 -3
  12. package/dist/workflow/index.html +3 -3
  13. package/package.json +1 -1
  14. package/src/bus-task-scopes.js +69 -0
  15. package/src/bus-task-targets.js +48 -0
  16. package/src/bus-tasks.js +175 -0
  17. package/src/command-bus.js +14 -39
  18. package/src/command-history.js +36 -0
  19. package/src/commands/cast.js +10 -3
  20. package/src/commands/motion.js +2 -1
  21. package/src/commands/motion3.js +10 -4
  22. package/src/commands/shared.js +9 -0
  23. package/src/motion3/compiler/anchors.js +33 -5
  24. package/src/motion3/compiler/compile.js +20 -3
  25. package/src/motion3/compiler/grip.js +9 -3
  26. package/src/motion3/compiler/socket-grip.js +34 -10
  27. package/src/motion3/docs.js +1 -1
  28. package/src/motion3/fix/index.js +5 -3
  29. package/src/motion3/pipeline.js +2 -0
  30. package/src/motion3/qa/spec-checks.js +12 -1
  31. package/src/motion3/schema/topics.js +2 -2
  32. package/src/motion3/schema/types.js +1 -0
  33. package/src/motion3/skills/basic.js +61 -13
  34. package/src/motion3/skills/index.js +4 -2
  35. package/src/motion3/skills/info.js +1 -1
  36. package/src/motion3/skills/reach-human.js +139 -0
  37. package/src/motion3/skills/stance.js +92 -0
  38. package/src/motion3/sockets.js +21 -4
  39. package/src/motion3/views.js +28 -5
  40. package/src/studio-app-binding.js +2 -2
  41. package/src/studio-error-codes.js +1 -1
  42. package/src/studio-motion-visual.js +4 -4
  43. package/tools/run-tests.mjs +1 -1
  44. package/dist/assets/analytics-DbKG0NgD.js +0 -6
  45. package/dist/assets/compile-worker-DYpa2nvO.js +0 -5
  46. package/dist/assets/scene-asset-sync-7J-w5oNh.js +0 -20
@@ -15,7 +15,7 @@
15
15
  // where its installed take has it at that frame (people.js)
16
16
  // spec.props = { name: { hand: "r", origin: ["RightHandIndex2", "RightHandMiddle2"],
17
17
  // points: { name: [d, l, n] } } } offsets along the palm axes (m)
18
- import { add, sub, scale, len, apply, ry, T } from "./math.js";
18
+ import { add, sub, scale, len, dot, norm, apply, ry, T } from "./math.js";
19
19
  import { bonePoint } from "./skeleton.js";
20
20
  import { palmAxes } from "./limbs.js";
21
21
  import { SIDES } from "./pose.js";
@@ -23,10 +23,12 @@ import { HAND_CLEAR, keepOut } from "../body.js";
23
23
  import { groupsOf } from "../scene.js";
24
24
  import { personJoint, peopleList } from "../people.js";
25
25
  import { isCarried } from "../carried.js";
26
- import { socketsOf, socketNamed, socketWorld, socketDistance, faceFrom } from "../sockets.js";
26
+ import { socketsOf, socketNamed, socketWorld, socketDistance, faceFrom, rimPoint } from "../sockets.js";
27
27
  import { partsOf } from "../object-pose.js";
28
+ import { GRASP_POINTS } from "./vocab.js";
28
29
 
29
- function armReach(sk, k) {
30
+ /** Shoulder to wrist with the arm straight (rest bone lengths), m. */
31
+ export function armReach(sk, k) {
30
32
  const side = SIDES[k], P = sk.restWorldPos, I = sk.index;
31
33
  return len(sub(P[I[`${side}ForeArm`]], P[I[`${side}Arm`]])) + len(sub(P[I[`${side}Hand`]], P[I[`${side}ForeArm`]]));
32
34
  }
@@ -146,13 +148,39 @@ export function anchorSocket(ctx, name, s, diag, c, side) {
146
148
  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
149
  ctx.sockets.set(name, pick ?? null);
148
150
  }
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
+ const sock = ctx.sockets.get(name);
152
+ let w = sock ? socketWorld(sock, poseOf(sock.owner)) : { p: local ?? poseOf(a.object).pos };
153
+ if (sock?.rim) w = rimHold(ctx, name, side, sock, poseOf(sock.owner), w, local ?? s.P[ctx.sk.index[side ? `${SIDES[side]}Arm` : "Hips"]]);
154
+ else if (local) w.p = local;
155
+ // a thick body (a cup) holds its axis further off the finger middles than the grasp's point does (vocab.js GRASP_POINTS,
156
+ // 2 cm on a power grip): the point moves out from the axis toward the hand's shoulder by the difference, so the fingers
157
+ // close on the surface instead of being asked into the body (session 7f15b4ae: a 4 cm cup, fingers asked 2 cm inside)
158
+ else if (w.axis && w.radius > 0) {
159
+ const inset = Math.abs(GRASP_POINTS[a.grasp]?.[2] ?? 0), out = w.radius - inset, sh = s.P[ctx.sk.index[`${SIDES[side] ?? "Right"}Arm`]];
160
+ const v = sub(sh, w.p), across = sub(v, scale(w.axis, dot(v, w.axis)));
161
+ if (out > 1e-4 && len(across) > 1e-6) w.p = add(w.p, scale(norm(across), out));
162
+ }
151
163
  // 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
164
  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
165
  return w;
154
166
  }
155
167
 
168
+ /**
169
+ * A rim socket held by hand `side`: the rim point nearest q (the anchor's local point, else the shoulder) the first time
170
+ * it is asked, kept in the rim's own axes so the hand stays on that spot of the rim as the rim moves (a wheel in a truck).
171
+ */
172
+ function rimHold(ctx, name, side, sock, pose, w, q) {
173
+ ctx.rims ??= new Map();
174
+ // asked with no hand (a contact check, a goal): the spot a hand already holds
175
+ const held = side ? null : [...ctx.rims.keys()].find(k => k.startsWith(`${name}|`) && !k.endsWith("|"));
176
+ const key = held ?? `${name}|${side ?? ""}`;
177
+ if (!ctx.rims.has(key)) {
178
+ const r = rimPoint(w, q), back = v => apply(T(pose.R), v);
179
+ ctx.rims.set(key, { at: back(sub(r.p, pose.pos)), axis: back(r.axis) });
180
+ }
181
+ return socketWorld(ctx.rims.get(key), pose);
182
+ }
183
+
156
184
  /** Points of every prop held by hand k, in world space, written to diag.props. */
157
185
  export function measureProps(ctx, k, s, diag) {
158
186
  const { sk } = ctx;
@@ -23,10 +23,11 @@ import { isCarried, isFollow, hingePose, movesRecord, compose, relative } from "
23
23
  import { isFree, freePoseAt } from "../free-motion.js";
24
24
  import { groupsOf } from "../scene.js";
25
25
  import { pose, SIDES } from "./pose.js";
26
+ import { graspAnchor } from "./socket-grip.js";
26
27
  import { gripHand } from "./grip.js";
27
28
  import { bakeChannels, channelsAt, flowOptions } from "./channels.js";
28
29
  import { idleLayer, withIdle, busyHands } from "./idle.js";
29
- import { resolveAnchor, measureProps } from "./anchors.js";
30
+ import { resolveAnchor, measureProps, armReach } from "./anchors.js";
30
31
  import { expandGenerators } from "./walk.js";
31
32
  import { steadyHead } from "./head-steady.js";
32
33
  import { fixOptions, applyFixes } from "../fix/index.js";
@@ -36,6 +37,8 @@ import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
36
37
  import { objectPoseAt, handOf, restingAt, frameAt, spanOf } from "../object-pose.js";
37
38
 
38
39
  const smooth = x => x * x * (3 - 2 * x);
40
+ // how far past the straight arm a grasp fit's wrist target may end before it counts as wound up (solveFrame), m
41
+ const REACH_SLACK = 0.02;
39
42
 
40
43
  /**
41
44
  * Scene boxes by frame. A body moves with its own spec.objects entry or its nearest group's (a
@@ -146,15 +149,25 @@ export function solveFrame(ctx, c0, f, lead = true) {
146
149
  if (!(c[wch] > 1e-4)) continue;
147
150
  const full = { ...c, [wch]: 1, [`armw_${fit.side}`]: 1 };
148
151
  for (const ch of Object.keys(full)) if (ch !== wch && new RegExp(`^w_.+_${fit.side}$`).test(ch)) full[ch] = 0;
149
- let corr = [0, 0, 0];
152
+ // each pass adds what the effector still misses; an arm at full reach misses the same each pass, so on a grasp's fit
153
+ // the sum wound the wrist target far past the anchor (session 7f15b4ae: 0.6 m, read back as REACH 0.845 m on a cup a
154
+ // few cm out of reach). A grasp fit whose wrist target ends past the straight arm (REACH_SLACK) aims instead at the
155
+ // anchor by the hand's own wrist-to-effector offset of its best pass: the arm reaches straight for it and its
156
+ // residual (REACH) is what the effector misses. Other fits keep their passes as they were (their takes are pinned).
157
+ let corr = [0, 0, 0], best = null, last = null;
158
+ const grasp = graspAnchor(ctx.spec, fit.anchor), shoulder = ctx.sk.index[`${SIDES[fit.side]}Arm`], wristAt = ctx.sk.index[`${SIDES[fit.side]}Hand`];
150
159
  for (let i = 0; i < 6; i++) {
151
160
  full[`fit_${fit.anchor}_${fit.side}`] = corr;
152
161
  const { s, diag } = pose(ctx.sk, full, hooksFor(ctx, full, f, true, lead, fit.side));
153
162
  const eff = effectorPoint(ctx.sk, s, diag, fit.effector);
154
163
  const goal = add(resolveAnchor(ctx, fit.anchor, s, diag, full, fit.side), fit.offset ?? [0, 0, 0]);
155
164
  if (!eff) break;
156
- corr = add(corr, sub(goal, eff));
165
+ const miss = sub(goal, eff), err = len(miss);
166
+ if (grasp && (!best || err < best.err)) best = { err, hand: sub(s.P[wristAt], eff) };
167
+ last = { goal, shoulder: s.P[shoulder] };
168
+ corr = add(corr, miss);
157
169
  }
170
+ if (grasp && best && last && len(sub(add(last.goal, corr), last.shoulder)) > armReach(ctx.sk, fit.side) + REACH_SLACK) corr = best.hand;
158
171
  c[`fit_${fit.anchor}_${fit.side}`] = corr;
159
172
  }
160
173
  const { s, diag } = pose(ctx.sk, c, hooksFor(ctx, c, f, false, lead));
@@ -183,11 +196,15 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
183
196
  // a prop anchor outside its hand's carry reads the carry's grab (release) frame (anchors.js propOnCarry)
184
197
  ctx.carrySpan = o => spanOf(out, o);
185
198
  ctx.solvedAt = f => frameAt(out, f);
199
+ // a reach's grasp is planned on its first full-weight frame (socket-grip.js plannedGrasp)
200
+ ctx.weightAt = (ch, f) => out.baked?.[ch]?.[f];
201
+ ctx.frameCount = frameCount;
186
202
  const engine = {
187
203
  rebake(keys) {
188
204
  out.keys = keys;
189
205
  out.baked = bakeChannels(keys, frameCount, flowOptions(spec.flow));
190
206
  ctx.socketTurns = new Map();
207
+ ctx.graspPlans = new Map();
191
208
  steadyHead(out.baked, keys, frameCount);
192
209
  // a held pose breathes and shifts its weight (idle.js): offsets solveFrame adds, contacts and out.baked stay as keyed
193
210
  ctx.idle = idleLayer(out.baked, { spec, sk, frameCount });
@@ -34,7 +34,7 @@ import { sdBox } from "../qa/geometry.js";
34
34
  import { partsOf } from "../object-pose.js";
35
35
  import { graspAnchor, socketCarry, socketHand, keepSocketTurn, ownWrist } from "./socket-grip.js";
36
36
 
37
- const NEAR = 0.06, THIN = 0.045, ALIGN = 0.3, PREFER = 30, DEG = 180 / Math.PI, ITER = 2, UP = [0, 1, 0];
37
+ const NEAR = 0.06, UPRIGHT = 0.8, THIN = 0.045, ALIGN = 0.3, PREFER = 30, DEG = 180 / Math.PI, ITER = 2, UP = [0, 1, 0];
38
38
  const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
39
39
  const flat = (v, n) => sub(v, scale(n, dot(v, n)));
40
40
 
@@ -97,7 +97,7 @@ function bodyAt(ctx, f, k, A, anchor, now) {
97
97
  // the same with keys or without), and no other body takes its place
98
98
  const carry = best && carryOf(ctx, best.id);
99
99
  if (!best || (carry?.side === k && ownGrip(ctx, carry.key, k, f) === null)) return null;
100
- return anchor?.kind === "object" || best.d <= NEAR ? best.box : null;
100
+ return anchor?.kind === "object" || best.d <= NEAR ? { ...best.box, id: best.id } : null;
101
101
  }
102
102
 
103
103
  const AXES = [0, 1, 2];
@@ -149,7 +149,7 @@ export function gripHand(ctx, sk, s, side, k, c, f, diag) {
149
149
  // a hand that holds what it carries by a socket keeps the grasp it took on the grab frame
150
150
  if (carry && f > carry.grab) return carry.keyed ? undefined : keepSocketTurn(ctx, sk, s, side, k, carry);
151
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 ?? {}));
152
+ if (grasp) return socketHand(ctx, sk, s, side, k, c, diag, on, st => effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}), null, f);
153
153
  // a hand that carries something on this frame holds that (its carry's grip), whatever its fit's anchor
154
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;
155
155
  const now = { s, diag, c }, anchor = ctx.spec.anchors?.[on.fit.anchor];
@@ -162,6 +162,12 @@ export function gripHand(ctx, sk, s, side, k, c, f, diag) {
162
162
  const measure = st => ({ pos: effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}), R: (({ d, l, n }) => cols(d, l, n))(palmAxes(sk, st, side)) });
163
163
  const I = sk.index, a = I[`${side}Arm`], fa = I[`${side}ForeArm`], h = I[`${side}Hand`];
164
164
  const long = thinAxis(box.half), axis = long >= 0 ? apply(box.R, AXES.map(i => (i === long ? 1 : 0))) : null;
165
+ // an upright thin body (a cup, a can) under a plain anchor that nothing carries is held the way a grasp holds it
166
+ // (socket-grip.js): a power wrap from the side, not the hand over its top with the fingers down along the forearm
167
+ // (session 7f15b4ae). A carried one keeps its carry's grip, a lying bar or handle the roll rule below.
168
+ if (axis && Math.abs(axis[1]) > UPRIGHT && !carryOf(ctx, box.id)) {
169
+ return socketHand(ctx, sk, s, side, k, c, diag, on, st => effectorAt(sk, ctx.spec, st, side, on.fit.effector ?? {}), { w: { p: A, axis }, type: "power" }, f);
170
+ }
165
171
  const ax = palmAxes(sk, s, side), roll = norm(flat(ax.l, ax.d));
166
172
  const axT = norm(sub(s.P[h], s.P[fa])), neutral = mul(s.R[fa], mul(T(sk.restWorldRot[fa]), sk.restWorldRot[h]));
167
173
  const twist = wristNeed(neutral, s.R[h], axT, apply(mul(neutral, T(s.R[h])), ax.l)).twist;
@@ -60,23 +60,27 @@ export function socketCarry(ctx, k, f) {
60
60
  }
61
61
 
62
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.
63
+ * socketHand(ctx, sk, s, side, k, c, diag, on, effector, implicit, f): turn the hand of fit `on` ({ fit, w }) into its
64
+ * anchor's socket grasp (see above); effector(st) is the fit's effector point on a pose. `implicit` ({ w, type }) is
65
+ * the world socket and grasp of a fit without one (grip.js: an upright thin body under a plain anchor); f the frame.
65
66
  */
66
- export function socketHand(ctx, sk, s, side, k, c, diag, on, effector) {
67
+ export function socketHand(ctx, sk, s, side, k, c, diag, on, effector, implicit = null, f = undefined) {
67
68
  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);
69
+ const w = implicit?.w ?? anchorSocket(ctx, on.fit.anchor, s, diag, c, k), F = effector(s), fwd = norm(sub(s.P[h], s.P[fa]));
70
+ const type = implicit?.type ?? ctx.spec.anchors[on.fit.anchor].grasp, base = socketPalm(side, w, type, fwd, c.yaw);
70
71
  const measure = st => ({ pos: effector(st), R: cols(palmAxes(sk, st, side)) }), now = measure(s);
71
72
  // free to roll about its axis (no `from`): the roll the arm holds most easily, the Boltzmann mean of ROLLS rolls pulled
72
73
  // lightly toward the fingers on the forearm's line, so the hand closes on the bar from the side the arm comes from
73
74
  // then the knuckle line tilts on the bar (an oblique grip) up to TILT deg, priced, where the forearm cannot come across it
74
75
  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
76
  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);
77
+ const pick = () => {
78
+ const free = w.axis && !w.from && type !== "palm", roll = free ? softmin(th => strain(turned(th)) + PULL * (1 - Math.cos(th))) : 0;
79
+ const rolled = turned(roll), tilted = a => mul(axisRot(col(rolled, 2), a), rolled);
80
+ const tilt = w.axis && type !== "palm" ? mean(TILTS.map(a => [a, strain(tilted(a)) + SLANT * (a / TILT) ** 2])) : 0;
81
+ return { R: tilted(tilt), roll, tilt };
82
+ };
83
+ const { R, roll, tilt } = plannedGrasp(ctx, on.fit.anchor, k, f, pick);
80
84
  const { goal, plane, wristAt } = aimArm(sk, s, side, { pos: F, R }, now, on.w, "palm");
81
85
  const arm = swivel(sk, s, side, goal, wristAt(goal), plane, ELBOW_REST, { w: on.w });
82
86
  let need = { twist: 0, flex: 0, dev: 0 };
@@ -84,10 +88,30 @@ export function socketHand(ctx, sk, s, side, k, c, diag, on, effector) {
84
88
  twoBone(sk, s, a, fa, h, wristAt(i ? s.R[h] : goal), arm.pole, ELBOW_REST, "elbow");
85
89
  need = orient(sk, s, side, h, fa, goal);
86
90
  }
87
- diag[`grip_${k}`] = { w: on.w, socket: true, ...need };
91
+ diag[`grip_${k}`] = { w: on.w, socket: true, ...need, roll: roll * DEG, tilt };
88
92
  diag[`socket_${k}`] = { rel: mul(T(plain), s.R[h]) };
89
93
  }
90
94
 
95
+ /**
96
+ * The grasp frame a reach turns the hand into, chosen once on the reach's first full-weight frame and kept on the
97
+ * approach to it (ctx.graspPlans, per bake): a person sets the hand's turn for the grasp at its end and keeps it on the way
98
+ * (end-state comfort), and a choice taken anew every frame flipped between two near-equal rolls as the fingers closed
99
+ * (a 5.5 cm wrist jump in one frame). pick() chooses it on the pose being solved: { R, roll, tilt }.
100
+ */
101
+ function plannedGrasp(ctx, name, k, f, pick) {
102
+ const ch = `w_${name}_${k}`, at = g => ctx.weightAt?.(ch, g) ?? 0;
103
+ if (!ctx.weightAt || !Number.isInteger(f)) return pick();
104
+ let a = f, g;
105
+ while (a > 0 && at(a - 1) > 1e-3) a--;
106
+ for (g = a; g < ctx.frameCount - 1 && at(g) < 0.999 && at(g + 1) > 1e-3; g++);
107
+ const key = `${ch}:${a}`;
108
+ ctx.graspPlans ??= new Map();
109
+ // from the grasp on, the hand follows what it holds (carried, it moves): the plan covers the approach only
110
+ if (f >= g) { const plan = pick(); if (f === g) ctx.graspPlans.set(key, plan); return plan; }
111
+ if (!ctx.graspPlans.has(key)) { ctx.graspPlans.set(key, null); (ctx.hold?.plainFrame ?? ctx.solvedAt)(g); }
112
+ return ctx.graspPlans.get(key) ?? pick();
113
+ }
114
+
91
115
  /**
92
116
  * The hand of a socket carry after its grab keeps the grab frame's wrist turn against the forearm (see above), read
93
117
  * once per bake (ctx.socketTurns) off the grab frame solved without the keys: a keyed carry's curve is built on
@@ -22,6 +22,6 @@ export const MOTION_ACTION_HELP = {
22
22
  + "A carried object takes pos [x, y, z] and rot [x, y, z, w], or aim [x, y, z] (its +Y) and roll?: its base's world pose (a group: its root), grab..release only (objects.<id>.keys); the hand follows. "
23
23
  + "A hinge takes angle (degrees) or rot (its turn about the axis). remove: true deletes the key at frame. Refused where the take neither holds nor hinges it (drag it then). "
24
24
  + "Answers keyed and the motion.compile output; REACH \"<id>: n cm off its keys\": what the hand missed.",
25
- report: "Page through the parts of a motion.compile or motion.qa answer that did not fit (output.more): part is groups (the default), timeline or probes, from the item index to start at; "
25
+ report: "Page through the parts of a motion.compile or motion.qa answer that did not fit (output.more): specId names the answer (or characterId: its installed take's); part is groups (the default), timeline or probes, from the item index to start at; "
26
26
  + "answers { specId, part, from, items, next } (next = the index to ask for next, null at the end). Changes nothing.",
27
27
  };
@@ -14,6 +14,8 @@
14
14
  // self arms out of the rest of the body on its fitted skin: the elbow turns about the
15
15
  // shoulder-wrist line, the hand is pushed out along the skin (self.js)
16
16
  // retime speed spikes get more frames, borrowed from the neighbouring keys
17
+ // stance a standIdle stance whose legs, feet or hips are in a scene body slides out of it, the least
18
+ // distance (skills/stance.js; applied when the skills expand, reported in observations.placed)
17
19
  import { lowerHips, armReach } from "./reach.js";
18
20
  import { lockFeet } from "./slide.js";
19
21
  import { holdTurns } from "./turn.js";
@@ -22,14 +24,14 @@ import { pushOut } from "./push.js";
22
24
  import { retime } from "./retime.js";
23
25
  import { selfClear } from "./self.js";
24
26
 
25
- const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime"];
27
+ const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance"];
26
28
  export { FIX_KEYS };
27
29
 
28
30
  export function fixOptions(fix) {
29
- if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false };
31
+ if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false };
30
32
  const f = fix ?? {};
31
33
  const pen = f.penetration === true ? 0.02 : Math.min(0.05, Math.max(0, Number(f.penetration) || 0));
32
- return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true };
34
+ return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true, stance: f.stance !== false };
33
35
  }
34
36
 
35
37
  /** The slide report with the flat feet whose turn waited (turn.js): turns { frames, max deg }. */
@@ -95,6 +95,8 @@ export function compileSpec(sent, { sk, scene }, opts = {}) {
95
95
  if (paced.length) observations.paced = paced;
96
96
  const cut = used.filter(u => u.cut).map(u => `${u.skill}: ${u.cut}`);
97
97
  if (cut.length) observations.cut = cut;
98
+ const placed = used.filter(u => u.placed).map(u => `${u.skill} f${u.f0}: ${u.placed.why}: at [${u.placed.at.join(", ")}], heading ${u.placed.heading}`);
99
+ if (placed.length) observations.placed = placed;
98
100
  if (plans.length) observations.planned = plans.map(p => p.note);
99
101
  const timeline = timelineOf(out, { used, spec: expanded }), probes = asked && runProbes(out, asked, { spec: expanded, sk, scene, notes });
100
102
  return { out, qa, observations, spec: resolved, expanded, used, fixed: out.fixed, search, timeline, ...(probes ? { probes } : {}), ...(notes.length ? { notes } : {}),
@@ -11,6 +11,7 @@ import { sub, dot, norm, len } from "../compiler/math.js";
11
11
  import { carried } from "../compiler/limbs.js";
12
12
  import { resolveAnchor } from "../compiler/anchors.js";
13
13
  import { CAPSULES, segPoints, sdBox } from "./geometry.js";
14
+ import { refDistance } from "../refs.js";
14
15
 
15
16
  export function pointOf(out, f, ref) {
16
17
  const { s, diag, c } = out.frames[f], [kind, rest] = ref.split(/:(.*)/s);
@@ -20,6 +21,13 @@ export function pointOf(out, f, ref) {
20
21
  throw new Error(`MOTION_SPEC_INVALID: check point "${ref}"`);
21
22
  }
22
23
 
24
+ /** The part a contact target "anchor:<name>" holds by when that anchor takes a grasp (its socket's owner, else its object), else null. */
25
+ function graspPart(out, ref) {
26
+ const [kind, name] = ref.split(/:(.*)/s), a = kind === "anchor" ? out.ctx.spec.anchors?.[name] : null;
27
+ if (a?.kind !== "object" || !a.grasp) return null;
28
+ return out.ctx.sockets?.get(name)?.owner ?? a.object;
29
+ }
30
+
23
31
  const active = (out, f, when) => (Array.isArray(when) ? f + (out.f0 ?? 0) >= when[0] && f + (out.f0 ?? 0) <= when[1] : when ? (out.frames[f].c[when] ?? 0) > 0.99 : true);
24
32
 
25
33
  function alongDir(out, f, along) {
@@ -35,7 +43,10 @@ export function specChecks(out, checks = {}) {
35
43
  for (const k of checks.contacts ?? []) if (active(out, f, k.when)) {
36
44
  const a = pointOf(out, f, k.point), b = pointOf(out, f, k.target);
37
45
  if (!a || !b) continue;
38
- const e = len(sub(a, b));
46
+ // on a grasp's anchor the fingers close on the held part wherever the grasp turned them round it: they are on it
47
+ // when they are on its surface (as goals' near on an object), not on one point of it (session 7f15b4ae: 0.4 cm
48
+ // on the cup, read as 5.3 cm from the anchor)
49
+ const held = graspPart(out, k.target), e = held ? refDistance(out, f, k.point, `object:${held}`) ?? len(sub(a, b)) : len(sub(a, b));
39
50
  if (e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
40
51
  }
41
52
  for (const k of checks.orient ?? []) if (active(out, f, k.when)) {
@@ -65,7 +65,7 @@ export const NOTES = {
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
67
  "segment with reach slides from t toward tMin until the arm reaches it.",
68
- "An object anchor with grasp (power, pinch, hook, palm) takes the hand by a socket of its object and the compiler turns the hand: power and hook put the knuckle line along the socket's axis with the palm on it, pinch holds at the fingertips, palm presses flat on the nearest face. socket names one (object.grasp sockets the user or you stored, or a thin part by its id: a bar, a grip, a magazine); without it the socket nearest local, or the shoulder. Two hands on one carried object: each hand an object anchor with grasp on its part (a rifle's grip and magazine). reachGrip on such an anchor closes the fingers to the grasp. Never key the wrist for it; a wrist you key (wtwist/wflex) wins.",
68
+ "An object anchor with grasp (power, pinch, hook, palm) takes the hand by a socket of its object and the compiler turns the hand: power and hook put the knuckle line along the socket's axis with the palm on it, pinch holds at the fingertips, palm presses flat on the nearest face. socket names one (object.grasp sockets the user or you stored, or a thin part by its id: a bar, a grip, a magazine; a disc, a steering wheel, is its rim, held where nearest the shoulder, and the hand stays there as it moves); without it the socket nearest local, or the shoulder. Two hands on one carried object: each hand an object anchor with grasp on its part (a rifle's grip and magazine). reachGrip on such an anchor closes the fingers to the grasp. Never key the wrist for it; a wrist you key (wtwist/wflex) wins.",
69
69
  "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."],
70
70
  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.",
71
71
  "fits: the hand target moves so the effector (a prop point or the mean of joints), not the wrist, lands on the anchor.",
@@ -109,6 +109,6 @@ export const NOTES = {
109
109
  "object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
110
110
  "Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
111
111
  fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
112
- "reach, slide and self are on by default; penetration and retime are opt-in.",
112
+ "reach, slide, self and stance are on by default; penetration and retime are opt-in.",
113
113
  "self keeps the arms out of the body on its fitted skin (QA BODY measures the same skin): wrist targets inside it go onto it, elbows turn about the shoulder-wrist line, a hand a little inside is pushed out. A fit's target (the mouth) is never moved; what is left is BODY, with checks.allow { part, object: <body part>, depth } for a contact meant to touch (a hand on a knee)."],
114
114
  };
@@ -140,6 +140,7 @@ export const FIX_TYPES = {
140
140
  penetration: { oneOf: [{ type: "boolean" }, { type: "number", minimum: 0, maximum: 0.05 }], default: 0, description: "push hands out of boxes up to this depth, m (true = 0.02)" },
141
141
  self: bool("arms kept out of the body's skin (wrists, elbows)", { default: true }),
142
142
  retime: bool("speed spikes borrow frames from the neighbouring keys", { default: false }),
143
+ stance: bool("a standIdle stance in a scene body slides out of it (observations.placed)", { default: true }),
143
144
  };
144
145
 
145
146
  export const REF_DOCS = {
@@ -5,22 +5,36 @@ import { walk } from "../compiler/walk.js";
5
5
  import { keyBook, skillError } from "./book.js";
6
6
  import { walkEnd } from "./info.js";
7
7
  import { GRASP_FINGERS, GRASP_POINTS } from "../compiler/vocab.js";
8
+ import { clearStance, reachStance } from "./stance.js";
9
+ import { CLOSE, GAZE, gazeAt, headKeyed, earlyTransport, holdsOpen, transportKeys } from "./reach-human.js";
8
10
 
9
11
  /** The fingers' point a reachGrip fits onto its anchor: the finger middles, or where a grasp holds the thing (the palm is -n on the right hand). */
10
12
  const graspPoint = (grasp, k) => (GRASP_POINTS[grasp] ? GRASP_POINTS[grasp].map((v, i) => (i === 2 && k === "r" ? -v : v)) : [0, 0, 0]);
11
13
 
12
- /** standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). */
14
+ /**
15
+ * standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). Without `at` (and no root
16
+ * keyed) before a reachGrip it stands within that hand's reach of the anchor, facing it; a stance in a scene body
17
+ * slides out of it (stance.js). Either says so in `placed`.
18
+ */
13
19
  export function standIdle(ctx) {
14
20
  const { sk, args, state } = ctx, I = sk.index, P = sk.restWorldPos, ank = P[I.LeftFoot][1];
15
- const h = args.heading ?? state.yaw ?? 0, base = args.at ?? (state.root ? [state.root[0], state.root[2]] : null);
16
- if (!base) skillError(ctx.tag, "needs args.at: [x, z] (no root is keyed before f0)");
21
+ let h = args.heading ?? state.yaw ?? 0, base = args.at ?? (state.root ? [state.root[0], state.root[2]] : null), placed = null;
22
+ if (!base) {
23
+ const auto = reachStance(ctx);
24
+ if (!auto) skillError(ctx.tag, "needs args.at: [x, z] (no root is keyed before f0), or a reachGrip after it to stand within reach of");
25
+ ({ at: base, heading: h } = auto);
26
+ placed = { at: base, heading: h, why: `within reach of anchor ${auto.anchor}` };
27
+ } else if (args.at && ctx.place !== false) {
28
+ const out = clearStance(sk, ctx.scene, [base[0], base[base.length - 1]], h);
29
+ if (out) { base = out.at; placed = { at: base, heading: h, why: `moved ${out.moved} m out of a scene body` }; }
30
+ }
17
31
  const c = [base[0], 0, base[base.length - 1]], R = ry(h), foot = v => { const p = add(c, apply(R, v)); p[1] = ank; return p; };
18
32
  const set = { root: [c[0], P[I.Hips][1] - 0.01, c[2]], yaw: h, pitch: 0, roll: 0, spine_yaw: 0, spine_pitch: 0, spine_roll: 0, head_yaw: 0, head_pitch: 0,
19
33
  foot_l: foot([0.09, 0, -0.03]), foot_r: foot([-0.09, 0, 0.03]), fyaw_l: h, fyaw_r: h, fpitch_l: 0, fpitch_r: 0, knee_l: [0.05, 0, 1], knee_r: [-0.05, 0, 1],
20
34
  armw_l: 0, armw_r: 0, aswing_l: 0, aswing_r: 0, abend_l: 0, abend_r: 0, arm_hang: 1 };
21
35
  const keys = [{ f: ctx.f0, set }];
22
36
  if (args.until !== undefined) keys.push({ f: args.until, set: structuredClone(set) });
23
- return { keys, end: args.until ?? ctx.f0 };
37
+ return { keys, end: args.until ?? ctx.f0, ...(placed ? { placed } : {}) };
24
38
  }
25
39
 
26
40
  /**
@@ -52,6 +66,23 @@ export function walkTo(ctx) {
52
66
  return { keys, end };
53
67
  }
54
68
 
69
+ /** base when `props` has no other prop of that name (the same one again is fine), else base_<side>, base_<side>2, ... */
70
+ function freeName(props = {}, base, k, mine) {
71
+ const taken = n => props[n] && JSON.stringify(props[n]) !== JSON.stringify(mine);
72
+ if (!taken(base)) return base;
73
+ let name = `${base}_${k}`;
74
+ for (let i = 2; taken(name); i++) name = `${base}_${k}${i}`;
75
+ return name;
76
+ }
77
+
78
+ /** What a hand on `obj` may touch: the object, or for a group (an Empty, no body of its own) every part under it. */
79
+ const touchable = (scene, obj) => {
80
+ const objects = scene?.objects ?? {};
81
+ if (!obj || objects[obj] || obj.startsWith("person:")) return obj ? [obj] : [];
82
+ const parts = Object.keys(objects).filter(k => objects[k].ancestors?.includes(obj));
83
+ return parts.length ? parts : [obj];
84
+ };
85
+
55
86
  /**
56
87
  * reachGrip({ anchor, side, reach = 20, hold = 22, release = 12, elbow, fingers = "grip",
57
88
  * define, orient = true }): the hand goes to the anchor (defined elsewhere, or by `define`) over
@@ -67,23 +98,40 @@ export function reachGrip(ctx) {
67
98
  // an anchor that takes a grasp closes the hand to that grasp's fingers (../sockets.js)
68
99
  const grasp = (args.define ?? ctx.anchors?.[name])?.grasp, preset = args.fingers ?? GRASP_FINGERS[grasp] ?? "grip";
69
100
  const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
70
- // a take that ends after the hold but before the hand lets go cuts the release: the grip holds to the last frame
71
- const last = ctx.frames - 1, cut = ctx.f0 + reach + hold <= last && ctx.f0 + reach + hold + release > last;
101
+ // a take that ends before the hand lets go cuts the hold or the release: the grip holds to the last frame (a hold
102
+ // asked past the end was refused, session 7f15b4ae: "needs 92 frames from f10 (to f102), past the last frame f85")
103
+ const last = ctx.frames - 1, cut = ctx.f0 + reach <= last && ctx.f0 + reach + hold + release > last;
104
+ const held = Math.min(reach + hold, last - ctx.f0);
72
105
  for (const [c, v] of [[w, 0], [fg, { [preset]: 0 }], [armw, 0]]) if (state[c] === undefined) state[c] = v;
106
+ // the head looks at what the hand goes for unless the keys already move it (reach-human.js); read before the keys move state
107
+ // the reach's own view: a thing that spec.objects moves before the hand gets there is not at rest (reach-human.js)
108
+ const end = reach + hold + release, rc = { ...ctx, until: ctx.f0 + reach };
109
+ const look = !headKeyed(ctx.keys ?? [], ctx.f0, ctx.f0 + end) && gazeAt(rc, args.define ?? ctx.anchors?.[name], state);
110
+ const head = look ? { head_yaw: state.head_yaw ?? 0, head_pitch: state.head_pitch ?? 0 } : null;
73
111
  const b = keyBook(state, ctx.f0);
74
- b.hold(0, w, fg, armw, el);
112
+ b.hold(0, w, fg, armw, el, ...(look ? ["head_yaw", "head_pitch"] : []));
113
+ // the transport's early-peaked bell; on a thing the open hand clears, the fingers stay open to CLOSE of the reach, then close
114
+ // (a reach for a moving point, or below the knees, keeps the plain ease: reach-human.js earlyTransport)
115
+ const target = args.define ?? ctx.anchors?.[name], w0 = state[w], a0 = state[armw], lerp = (a, z, t) => a + (z - a) * t;
116
+ if (earlyTransport(rc, target)) for (const [f, t, ta] of transportKeys(reach)) b.at(f, { [w]: lerp(w0, 1, t), [armw]: lerp(a0, 1, ta) });
117
+ const close = Math.round(CLOSE * reach);
118
+ if (close > 0 && close < reach && holdsOpen(rc, target, state)) b.hold(close, fg);
119
+ if (look) b.at(Math.max(1, Math.round(GAZE * reach)), look);
75
120
  b.at(reach, { [w]: 1, [fg]: { ...state[fg], [preset]: 1 }, [armw]: 1, ...(args.elbow ? { [el]: args.elbow } : {}) });
76
- b.hold(reach + hold, w, fg, armw, el);
77
- if (!cut) b.at(reach + hold + release, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}) });
78
- const S = k === "l" ? "Left" : "Right", prop = `${name}Grip`;
121
+ b.hold(held, w, fg, armw, el, ...(look ? ["head_yaw", "head_pitch"] : []));
122
+ if (!cut) b.at(end, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}), ...(head ?? {}) });
123
+ // the fingers' prop is <anchor>Grip, unless the spec (or another skill) has a prop of that name of its own: its points
124
+ // would replace these (session 7f15b4ae: props.ballGrip for a carry broke reachGrip on anchor "ball")
125
+ const S = k === "l" ? "Left" : "Right", fingersProp = { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: graspPoint(grasp, k) } };
126
+ const prop = freeName(ctx.props, `${name}Grip`, k, fingersProp);
79
127
  const def = args.define ? { [name]: args.define } : {};
80
128
  // the hand may touch what it grips: an object, or the other person (people.js) whose joint the anchor is on
81
129
  const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
82
130
  return {
83
- keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `release cut at f${last} (hold to the end)` } : {}), anchors: def,
84
- props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: graspPoint(grasp, k) } } },
131
+ keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `${held < reach + hold ? "hold" : "release"} cut at f${last} (hold to the end)` } : {}), anchors: def,
132
+ props: { [prop]: fingersProp },
85
133
  fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
86
134
  checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
87
- allow: obj ? [{ part: "hand", object: obj, depth: 0.04 }] : [] },
135
+ allow: touchable(ctx.scene, obj).map(object => ({ part: "hand", object, depth: 0.04 })) },
88
136
  };
89
137
  }
@@ -7,6 +7,7 @@ import { stateAt } from "../compiler/walk.js";
7
7
  import { didYouMean, suggestField } from "../compiler/vocab.js";
8
8
  import { USE_FIELDS } from "../schema/fields.js";
9
9
  import { SKILL_INFO } from "./info.js";
10
+ import { fixOptions } from "../fix/index.js";
10
11
  import { standIdle, walkTo, reachGrip } from "./basic.js";
11
12
  import { sitInSeat, exitCar } from "./car.js";
12
13
  import { smokeCigarette } from "./smoke.js";
@@ -60,7 +61,8 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
60
61
  argsOk(u, i, lenient);
61
62
  if (!Number.isInteger(u.f0) || u.f0 < 0 || u.f0 > spec.frames - 1) bad(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
62
63
  const keys = [...skillKeys, ...own];
63
- const ctx = { sk, scene, frames: spec.frames, auto, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys, state: structuredClone(stateAt(keys, u.f0)), anchors: { ...merged.anchors, ...spec.anchors } };
64
+ const ctx = { sk, scene, frames: spec.frames, auto, props: { ...merged.props, ...spec.props }, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys,
65
+ use: spec.use, index: i, place: fixOptions(spec.fix).stance, state: structuredClone(stateAt(keys, u.f0)), anchors: { ...merged.anchors, ...spec.anchors }, objects: { ...merged.objects, ...spec.objects } };
64
66
  const part = SKILLS[u.skill](ctx), last = Math.max(part.end ?? u.f0, ...part.keys.map(k => k.f));
65
67
  if (last > spec.frames - 1) bad(`${tag} needs ${last - u.f0} frames from f${u.f0} (to f${last}), past the last frame f${spec.frames - 1}; set frames >= ${last + 1} or frames: "auto"`);
66
68
  skillKeys.push(...part.keys.map(k => ({ ...k, src: k.src ?? tag })));
@@ -70,7 +72,7 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
70
72
  for (const fit of part.fits ?? []) if (!merged.fits.some(x => same(x, fit))) merged.fits.push(fit);
71
73
  for (const ch of part.channels ?? []) if (!merged.channels.includes(ch)) merged.channels.push(ch);
72
74
  mergeChecks(merged.checks, part.checks);
73
- used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}) });
75
+ used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}), ...(part.placed ? { placed: part.placed } : {}) });
74
76
  });
75
77
  const out = { ...spec, keys: [...skillKeys, ...own], anchors: { ...merged.anchors, ...spec.anchors }, props: { ...merged.props, ...spec.props },
76
78
  objects: { ...merged.objects, ...spec.objects } };
@@ -19,7 +19,7 @@ const walkArgs = Object.fromEntries(WALK_FIELDS.filter(f => f !== "type" && f !=
19
19
 
20
20
  export const SKILL_INFO = {
21
21
  standIdle: {
22
- args: { at: xz("[x, z] world m; default the root keyed before f0"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
22
+ args: { at: xz("[x, z] world m; default the root keyed before f0, else within reach of the next reachGrip's anchor (output.observations.placed); a stance in a body slides out of it"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
23
23
  needs: "args.at, or a root keyed before f0", frames: "1 (to args.until when given)",
24
24
  adds: ["keys: a standing pose, FK arms down"], example: { f0: 0, args: { at: [0, 0], heading: 0 } },
25
25
  place: { at: 0.03 },