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.
Files changed (101) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/THIRD_PARTY_NOTICES.md +10 -0
  3. package/bin/agent/agent-routes.mjs +3 -3
  4. package/bin/agent/agent-runner.mjs +5 -5
  5. package/bin/agent/pi-tools.mjs +12 -1
  6. package/bin/agent/session-store.mjs +2 -2
  7. package/bin/agent/studio-context-budget.mjs +99 -0
  8. package/bin/agent/studio-prompt.mjs +1 -1
  9. package/bin/agent/studio-tools.mjs +1 -1
  10. package/dist/app/index.html +4 -4
  11. package/dist/assets/analytics-DbKG0NgD.js +6 -0
  12. package/dist/assets/app-34i-iUy1.js +5088 -0
  13. package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
  14. package/dist/assets/{first-shot-handoff-CC4xe4HM.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
  15. package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
  16. package/dist/assets/{landing-DjHD8u7k.js → landing-mq5bnqFE.js} +1 -1
  17. package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
  18. package/dist/assets/workflow-dUhurYUc.js +179 -0
  19. package/dist/cozyclay-package.json +1 -1
  20. package/dist/index.html +3 -3
  21. package/dist/workflow/index.html +3 -3
  22. package/package.json +1 -1
  23. package/src/App.jsx +1 -1
  24. package/src/agent-progress.js +3 -2
  25. package/src/agent-turn-mark.js +33 -0
  26. package/src/app-context.js +7 -2
  27. package/src/command-bus.js +10 -11
  28. package/src/commands/elements/character.js +2 -1
  29. package/src/commands/elements/motion.js +7 -1
  30. package/src/commands/elements/scene.js +3 -0
  31. package/src/commands/elements/stage.js +2 -1
  32. package/src/commands/elements/take-read.js +31 -0
  33. package/src/commands/elements.js +16 -2
  34. package/src/commands/motion3.js +15 -6
  35. package/src/commands/objects.js +2 -2
  36. package/src/commands/shot.js +6 -2
  37. package/src/domains/motion/take-source.js +4 -2
  38. package/src/draft-ghost.js +3 -2
  39. package/src/motion/beat-board-model.js +1 -1
  40. package/src/motion/beat-board.jsx +2 -1
  41. package/src/motion3/compile-job.js +4 -0
  42. package/src/motion3/compiler/anchors.js +68 -4
  43. package/src/motion3/compiler/compile.js +5 -1
  44. package/src/motion3/compiler/grip.js +8 -2
  45. package/src/motion3/compiler/limbs.js +35 -8
  46. package/src/motion3/compiler/pose.js +17 -2
  47. package/src/motion3/compiler/socket-grip.js +107 -0
  48. package/src/motion3/compiler/validate-objects.js +3 -2
  49. package/src/motion3/compiler/validate-parts.js +9 -2
  50. package/src/motion3/compiler/validate.js +3 -3
  51. package/src/motion3/compiler/vocab.js +23 -16
  52. package/src/motion3/describe.js +5 -2
  53. package/src/motion3/docs.js +1 -1
  54. package/src/motion3/free-motion.js +54 -15
  55. package/src/motion3/object-keys.js +36 -5
  56. package/src/motion3/object-pose.js +39 -6
  57. package/src/motion3/people.js +82 -0
  58. package/src/motion3/qa/checks.js +6 -2
  59. package/src/motion3/qa/hints-contact.js +17 -1
  60. package/src/motion3/qa/index.js +6 -4
  61. package/src/motion3/qa/people-checks.js +37 -0
  62. package/src/motion3/qa/style.js +14 -4
  63. package/src/motion3/refs.js +10 -1
  64. package/src/motion3/scene.js +4 -3
  65. package/src/motion3/schema/fields.js +4 -4
  66. package/src/motion3/schema/topics.js +9 -6
  67. package/src/motion3/schema/types.js +11 -9
  68. package/src/motion3/settle.js +136 -0
  69. package/src/motion3/skills/basic.js +10 -3
  70. package/src/motion3/skills/flip.js +200 -0
  71. package/src/motion3/skills/impact.js +109 -0
  72. package/src/motion3/skills/index.js +3 -1
  73. package/src/motion3/skills/info.js +19 -4
  74. package/src/motion3/sockets.js +104 -0
  75. package/src/motion3/take-edit.js +1 -1
  76. package/src/motion3/take.js +6 -4
  77. package/src/object-catalog.jsx +3 -1
  78. package/src/object-grasp.js +38 -0
  79. package/src/object-look.js +2 -0
  80. package/src/object-travel.js +6 -3
  81. package/src/panels/ObjectLookPanel.jsx +24 -3
  82. package/src/props.jsx +5 -5
  83. package/src/qa-label.js +25 -0
  84. package/src/scene-objects.js +19 -11
  85. package/src/shell/details-selection.js +2 -1
  86. package/src/studio-actions.js +4 -0
  87. package/src/studio-agent-protocol.js +11 -12
  88. package/src/studio-app-binding.js +3 -3
  89. package/src/studio-elements.js +3 -1
  90. package/src/studio-error-codes.js +8 -1
  91. package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
  92. package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
  93. package/src/text/text-object.jsx +106 -0
  94. package/src/text/text-record.js +122 -0
  95. package/src/workflow/AgentProgressCard.jsx +6 -4
  96. package/tools/run-tests.mjs +2 -2
  97. package/dist/assets/analytics-C9sUnndd.js +0 -1
  98. package/dist/assets/app-CCh--1Hc.js +0 -5087
  99. package/dist/assets/compile-worker-CkxiD6lH.js +0 -5
  100. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +0 -20
  101. 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": { const o = objectFrame(ctx, a, name, { s, diag, c }, side); return add(o.pos, apply(o.R, a.local ?? [0, 0, 0])); }
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
- if (Math.abs(c[`wtwist_${k}`] ?? 0) > 1e-3 || Math.abs(c[`wflex_${k}`] ?? 0) > 1e-3) return;
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
- /** Aim a foot: Foot->ToeBase along yaw/pitch of its rest direction, sole kept level. */
107
- export function orientFoot(sk, s, side, yawDeg, pitchDeg) {
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, [0, 1, 0]));
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: [45, 30, 35] },
151
- grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [40, 20, 20] },
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") objectOk(err, scene, at, a.object, { groups: true });
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 = FK arm, 1 = IK to the hand target"],
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, "a hand-target weight; the weights of one hand add up to at most 1"],
20
- [/^wtwist_[lr]$/, -85, 85, "wrist twist about the forearm in degrees"],
21
- [/^wflex_[lr]$/, -70, 70, "wrist flex about the knuckle line in degrees"],
22
- [/^fpitch_[lr]$/, -60, 90, "toe-down pitch in degrees: + lifts the heel (the toes stay flat), - lifts the toes"],
23
- [/^(pitch|spine_pitch|head_pitch)$/, -120, 120, "degrees, + bends forward"],
24
- [/^(roll|spine_roll|head_roll)$/, -90, 90, "degrees"],
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 in degrees, as yaw",
31
- pole: "a body-frame direction (turned by the root yaw only): +z forward, +x the character's left, +y up",
32
- foot: "the ANKLE target in world metres (standing: 0.087 X Bot, 0.105 Y Bot)",
33
- hand: "the WRIST target in world metres",
34
- root: "the hips joint in world metres (standing: 1.03 X Bot, 0.99 Y Bot)",
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: `degrees; ${CONVENTIONS.yaw}`, pitch: "whole-body pitch, degrees", roll: "whole-body roll, degrees",
40
- spine_yaw: "degrees", spine_pitch: "degrees, + bends forward", spine_roll: "degrees", head_yaw: "degrees", head_pitch: "degrees, + looks down", head_roll: "degrees",
41
- arm_rest: "FK arms down from the T-pose (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",
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, degrees, + forward", abend: "FK elbow bend, degrees",
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]$/, "");
@@ -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 above 0.5" } },
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
- const skills = SKILL_NAMES.map(n => {
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
  });
@@ -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.",