cozyclay 3.0.1 → 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 (83) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/THIRD_PARTY_NOTICES.md +10 -0
  3. package/bin/agent/pi-tools.mjs +12 -1
  4. package/bin/agent/studio-context-budget.mjs +8 -2
  5. package/bin/agent/studio-prompt.mjs +1 -1
  6. package/bin/agent/studio-tools.mjs +1 -1
  7. package/dist/app/index.html +4 -4
  8. package/dist/assets/analytics-BibyE5ar.js +6 -0
  9. package/dist/assets/app-DhRJ-PBz.js +5088 -0
  10. package/dist/assets/{compile-worker-BxKXh07s.js → compile-worker-DqiA4AaN.js} +3 -3
  11. package/dist/assets/{first-shot-handoff-DNO8kv_N.js → first-shot-handoff-Qx7SKl38.js} +1 -1
  12. package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
  13. package/dist/assets/{landing-yfZaJkWc.js → landing-DYg87MDY.js} +1 -1
  14. package/dist/assets/scene-asset-sync-DBYQBR0T.js +20 -0
  15. package/dist/assets/{workflow-D78eiKTa.js → workflow-BSA22Ofr.js} +1 -1
  16. package/dist/cozyclay-package.json +1 -1
  17. package/dist/index.html +3 -3
  18. package/dist/workflow/index.html +3 -3
  19. package/package.json +1 -1
  20. package/src/agent-turn-mark.js +33 -0
  21. package/src/bus-task-scopes.js +69 -0
  22. package/src/bus-task-targets.js +48 -0
  23. package/src/bus-tasks.js +175 -0
  24. package/src/command-bus.js +21 -47
  25. package/src/command-history.js +36 -0
  26. package/src/commands/cast.js +10 -3
  27. package/src/commands/elements/character.js +2 -1
  28. package/src/commands/elements/motion.js +7 -1
  29. package/src/commands/elements/scene.js +3 -0
  30. package/src/commands/elements/stage.js +2 -1
  31. package/src/commands/elements/take-read.js +31 -0
  32. package/src/commands/elements.js +16 -2
  33. package/src/commands/motion.js +2 -1
  34. package/src/commands/motion3.js +13 -5
  35. package/src/commands/objects.js +2 -2
  36. package/src/commands/shared.js +9 -0
  37. package/src/motion3/compiler/anchors.js +89 -5
  38. package/src/motion3/compiler/compile.js +25 -4
  39. package/src/motion3/compiler/grip.js +16 -4
  40. package/src/motion3/compiler/socket-grip.js +131 -0
  41. package/src/motion3/compiler/validate-parts.js +7 -2
  42. package/src/motion3/compiler/vocab.js +6 -0
  43. package/src/motion3/docs.js +1 -1
  44. package/src/motion3/fix/index.js +5 -3
  45. package/src/motion3/free-motion.js +12 -4
  46. package/src/motion3/object-keys.js +36 -5
  47. package/src/motion3/object-pose.js +38 -5
  48. package/src/motion3/pipeline.js +2 -0
  49. package/src/motion3/qa/spec-checks.js +12 -1
  50. package/src/motion3/scene.js +4 -3
  51. package/src/motion3/schema/fields.js +1 -1
  52. package/src/motion3/schema/topics.js +4 -3
  53. package/src/motion3/schema/types.js +3 -2
  54. package/src/motion3/skills/basic.js +68 -14
  55. package/src/motion3/skills/index.js +4 -2
  56. package/src/motion3/skills/info.js +5 -5
  57. package/src/motion3/skills/reach-human.js +139 -0
  58. package/src/motion3/skills/stance.js +92 -0
  59. package/src/motion3/sockets.js +121 -0
  60. package/src/motion3/take-edit.js +1 -1
  61. package/src/motion3/take.js +6 -4
  62. package/src/motion3/views.js +28 -5
  63. package/src/object-catalog.jsx +3 -1
  64. package/src/object-grasp.js +38 -0
  65. package/src/object-look.js +2 -0
  66. package/src/object-travel.js +6 -3
  67. package/src/panels/ObjectLookPanel.jsx +24 -3
  68. package/src/props.jsx +5 -5
  69. package/src/scene-objects.js +19 -11
  70. package/src/shell/details-selection.js +2 -1
  71. package/src/studio-agent-protocol.js +5 -6
  72. package/src/studio-app-binding.js +3 -3
  73. package/src/studio-elements.js +3 -1
  74. package/src/studio-error-codes.js +1 -1
  75. package/src/studio-motion-visual.js +4 -4
  76. package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
  77. package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
  78. package/src/text/text-object.jsx +106 -0
  79. package/src/text/text-record.js +122 -0
  80. package/tools/run-tests.mjs +1 -1
  81. package/dist/assets/analytics-BWCqdgWA.js +0 -1
  82. package/dist/assets/app-DfTCxaKk.js +0 -5087
  83. package/dist/assets/scene-asset-sync-U3kS2rvK.js +0 -20
@@ -1,7 +1,7 @@
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";
@@ -9,6 +9,7 @@ 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
11
  import { personJoint, peopleList } from "../people.js";
12
+ import { socketsOf, socketNamed } from "../sockets.js";
12
13
 
13
14
  const SIDE = new Set(["l", "r"]);
14
15
  const isObject = v => Boolean(v) && typeof v === "object" && !Array.isArray(v);
@@ -29,7 +30,11 @@ function validateAnchors(spec, err, { sk, scene, unknown = err }) {
29
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}` : ""}`);
30
31
  if (a.kind === "bone") boneOk(err, sk, at, a.bone);
31
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"`); }
32
- 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
+ }
33
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)}`);
34
39
  if (a.kind === "landmark" && a.name !== "mouth") err(`${at}: the only landmark is "mouth", got ${fmt(a.name)}`);
35
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`);
@@ -11,6 +11,12 @@ 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 = [
@@ -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 }. */
@@ -29,7 +29,9 @@ import { shapeOf, airPath, settlePath, settledAt, spinOf, spinEnergy, TIP } from
29
29
  export const G = 9.8;
30
30
  /** The highest a bounce goes, metres. */
31
31
  export const BOUNCE_MAX = 0.03;
32
- const BOUNCE = 0.25, TOUCH = 0.005;
32
+ const BOUNCE = 0.25;
33
+ /** A support's top may sit this far above the lowest point of what rests on it (m). */
34
+ export const TOUCH = 0.005;
33
35
  const HELD_OR_HINGED = ["prop", "follow", "pivot", "axis", "channel"];
34
36
 
35
37
  /** A free entry (C17): no prop, follow or hinge field, and keys or fall. */
@@ -41,14 +43,14 @@ const restOf = (scene, id) => scene?.objects?.[id] ?? groupsOf(scene)[id] ?? nul
41
43
  const partsIn = (scene, id) => Object.keys(scene?.objects ?? {}).filter(k => k === id || scene.objects[k].ancestors?.includes(id));
42
44
 
43
45
  /** The 8 corners of a base-pivot box { pos, R, half }. */
44
- function corners(box) {
46
+ export function corners(box) {
45
47
  const out = [];
46
48
  for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) out.push(add(box.pos, apply(box.R, [x * box.half[0], y * box.half[1], z * box.half[2]])));
47
49
  return out;
48
50
  }
49
- const topOf = box => Math.max(...corners(box).map(p => p[1]));
51
+ export const topOf = box => Math.max(...corners(box).map(p => p[1]));
50
52
  /** True when the vertical through x/z point p crosses the footprint of box. */
51
- const under = (box, p) => { const q = apply(T(box.R), sub([p[0], box.pos[1] + box.half[1], p[2]], box.pos)); return Math.abs(q[0]) <= box.half[0] && Math.abs(q[2]) <= box.half[2]; };
53
+ export const under = (box, p) => { const q = apply(T(box.R), sub([p[0], box.pos[1] + box.half[1], p[2]], box.pos)); return Math.abs(q[0]) <= box.half[0] && Math.abs(q[2]) <= box.half[2]; };
52
54
 
53
55
  /** Bodies a spec.objects entry moves (itself or a group above it): they hold nothing up. */
54
56
  function moversIn(spec, scene) {
@@ -195,6 +197,12 @@ export function afterRelease(ctx, id, o, f, release, releasePose, beforePose = n
195
197
  return events.length ? walk(events, f, releasePose, plan, false) : null;
196
198
  }
197
199
 
200
+ /** True when carry entry o falls at or after its release (absolute frame; afterRelease then moves it). */
201
+ export function fallsAfter(ctx, o, release) {
202
+ const map = ctx.timeMap ?? (f => f);
203
+ return fallsOf(o).some(fl => map(fl.at) >= release);
204
+ }
205
+
198
206
  /** The falls of entry `id` as planned under the current time map, for readers (timeline, tests): [{ t0, land, end, h, support, top }]. */
199
207
  export function fallPlans(ctx, id) {
200
208
  const o = ctx.spec.objects?.[id];
@@ -4,7 +4,8 @@
4
4
  // between keys a cubic Hermite on pos and the same in rotation-vector space on rot (squad
5
5
  // like), Catmull-Rom tangents through inner keys, zero at the first and last key
6
6
  // around the span the hand blends from the plain carry into the keyed span over KEY_BLEND frames
7
- // (inside grab..release; smoothstep), so nothing jumps
7
+ // (inside grab..release; smoothstep), so nothing jumps; keys that start within KEY_BLEND frames
8
+ // of the grab with a grip off where the hand is turn the hand onto it before (holdHook approach)
8
9
  // the object leads the hand: compile.js asks holdHook for the palm frame the keys put the grip
9
10
  // at, palm = key o attach^-1, and pose.js solves the hand to it (compiler/hold.js)
10
11
  // The object itself is still drawn in the hand (object-pose.js): the residual is what the arm
@@ -20,11 +21,14 @@ import { polePlan, smoothPath, smoothed } from "./hold-plan.js";
20
21
  import { secondGrip, secondHold, gripGate } from "./second-hand.js";
21
22
  import { SIDES } from "./compiler/pose.js";
22
23
  import { chooseGrip, between, thinBody } from "./grip-choice.js";
24
+ import { graspedBy } from "./compiler/socket-grip.js";
23
25
 
24
26
  /** Frames over which the hand blends into and out of the keyed span. */
25
27
  export const KEY_BLEND = 8;
26
28
  // the frames a hand takes to turn into a chosen grip before its grab and out of it after its release (grip-choice.js)
27
29
  const GRAB_EASE = 12;
30
+ // how far (m, deg) the grip keys put the hand on at the grab may be from where the reach brought it before the hand turns onto it first
31
+ const OFF_GRIP = { m: 0.01, deg: 5 };
28
32
 
29
33
  const smooth = x => x * x * (3 - 2 * x);
30
34
  /** [grab, release] in absolute frames of `out` (object-pose.js spanOf, kept here to stay out of its import cycle). */
@@ -108,8 +112,8 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
108
112
  if (!held.length) return null;
109
113
  const palmOf = (o, s) => carriedPose(sk, spec, s, { prop: o.prop }), attached = new WeakMap(), attaching = new Set();
110
114
  const restAt = (id, f) => ctx.restingAt?.(id, f) ?? scene?.objects?.[id] ?? groupsOf(scene)[id];
111
- // a keyed carry of a thin body (a cup, a can, a bar) whose grip is free (no at/along, no orient: false, no keyed wrist at
112
- // the grab) keeps the grip its keyed path holds most comfortably (grip-choice.js), solved on the plain frames with the
115
+ // a keyed carry of a thin body (a cup, a can, a bar) whose grip is free (no at/along, no orient: false, no grasp fit, no keyed
116
+ // wrist at the grab) keeps the grip its keyed path holds most comfortably (grip-choice.js), solved on the plain frames with the
113
117
  // body read at rest; a box keeps the grip grip.js gives it at the grab
114
118
  const choices = new WeakMap(), mapped = f => (out.timeMap ? Math.round(out.timeMap(f)) : f);
115
119
  // chosen once per compile (the first bake): the fixes' rebakes move the body a little, the carry's keys not at all
@@ -118,7 +122,7 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
118
122
  const cache = choices.get(out), o = spec.objects[id], k = spec.props?.[o.prop]?.hand, grab = spanIn(out, o)[0], b = out.baked ?? {};
119
123
  if (cache.has(id)) return cache.get(id);
120
124
  cache.set(id, null);
121
- const free = k && o.at === undefined && o.along === undefined && (o.grab !== undefined || !scene?.objects?.[id]) && thinBody(scene, id) && !(spec.fits ?? []).some(fit => fit.side === k && fit.orient === false)
125
+ const free = k && o.at === undefined && o.along === undefined && (o.grab !== undefined || !scene?.objects?.[id]) && thinBody(scene, id) && !(spec.fits ?? []).some(fit => fit.side === k && fit.orient === false) && !graspedBy(spec, scene, id, k)
122
126
  && !["wtwist", "wflex"].some(ch => Math.abs(b[`${ch}_${k}`]?.[grab] ?? 0) > 1e-3)
123
127
  // keys that start after the grab: the body rides the hand from where it rests, which only today's grip meets exactly
124
128
  && mapped(Math.min(...o.keys.map(key => key.f))) <= grab;
@@ -203,6 +207,33 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
203
207
  const h = heldAt(id, o, f);
204
208
  return { w: 1, palm: h ? { pos: add(scale(grip.pos, 1 - h.w), scale(h.palm.pos, h.w)), R: between(grip.R, h.palm.R, h.w) } : grip, measure, pole };
205
209
  };
210
+ // keys that lead from the grab frame, or from fewer than KEY_BLEND frames after it, put the hand on their grip at once (or
211
+ // within those frames: palm = first key o attach^-1). Where that grip is not where the hand would be at the first key (an
212
+ // at/along offset of the agent's choosing: bench session cb2503b5's cup, 13.6 cm in the grab frame; keys two frames after
213
+ // the grab: 7146050f's box), the hand turns onto it over the GRAB_EASE frames up to the first key instead, as onto a chosen
214
+ // grip (gripAround): before the grab with the object at rest, after it carrying the object there. A grip within OFF_GRIP
215
+ // of where the hand would be changes nothing.
216
+ const offGrips = new WeakMap();
217
+ const offGrip = (id, o, grip) => {
218
+ if (!offGrips.has(out.baked)) offGrips.set(out.baked, new Map());
219
+ const cache = offGrips.get(out.baked), had = cache.get(id);
220
+ if (had && had.map === out.timeMap) return had.off;
221
+ attaching.add(id);
222
+ let palm;
223
+ try { palm = palmOf(o, solvePlain(Math.max(spanIn(out, o)[0], Math.round(curveIn(out, id).t[0]))).s); } finally { attaching.delete(id); }
224
+ const turn = relative(palm, grip).R, deg = Math.acos(Math.min(1, Math.max(-1, (turn[0][0] + turn[1][1] + turn[2][2] - 1) / 2))) * 180 / Math.PI;
225
+ const off = Math.hypot(...sub(grip.pos, palm.pos)) > OFF_GRIP.m || deg > OFF_GRIP.deg;
226
+ cache.set(id, { map: out.timeMap, off });
227
+ return off;
228
+ };
229
+ const approach = (id, o, f) => {
230
+ const curve = curveIn(out, id), [grab, release] = spanIn(out, o), t0 = Math.max(grab, Math.round(curve.t[0]));
231
+ if (!(f < t0 && f >= t0 - GRAB_EASE) || t0 - grab >= KEY_BLEND || t0 > release) return null;
232
+ const grip = compose(keyedPose(curve, t0), invert(attach(id)));
233
+ if (!offGrip(id, o, grip)) return null;
234
+ const w = smooth(1 - (t0 - f) / GRAB_EASE);
235
+ return w > 1e-3 ? { w, palm: grip, measure: s => palmOf(o, s), pole: planOf(id, o).get(t0) } : null;
236
+ };
206
237
  // a release within KEY_BLEND frames of the last key: the hands finish easing out of the keys after it (weight from the last
207
238
  // key), the holding hand from the palm they gave it at the release (the object stays where it was let go), each hand with
208
239
  // the elbow pole (and the second hand the roll) planned for the release frame
@@ -217,7 +248,7 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
217
248
  // the hand that holds a keyed carry follows its keys
218
249
  const lead = (k, f, s, c) => {
219
250
  for (const [id, o] of held) {
220
- const pre = spec.props?.[o.prop]?.hand === k ? gripAround(id, o, f) ?? easeOut(id, o, f) : null;
251
+ const pre = spec.props?.[o.prop]?.hand === k ? gripAround(id, o, f) ?? approach(id, o, f) ?? easeOut(id, o, f) : null;
221
252
  if (pre) return pre;
222
253
  const h = spec.props?.[o.prop]?.hand === k ? heldAt(id, o, f) : null;
223
254
  if (h) return { ...h, pole: planOf(id, o).get(f) };
@@ -14,7 +14,9 @@
14
14
  // f < grab the scene pose
15
15
  // grab <= f <= release follows the prop's hand; without at/along it keeps the transform it had
16
16
  // against the palm axes at grab (no jump); at/along pin an offset (a group: its root, C15)
17
- // f > release frozen at the release pose
17
+ // f > release frozen at the release pose; let go over a body that moves on its own (a seat of a truck the spec
18
+ // drives with keys, a hinged lid), it rides that body's mover from the release on, keeping its world
19
+ // pose at the release (riderOf): a lighter put down in the cab leaves with the truck
18
20
  // no grab: in hand from frame 0; no release: in hand to the end (PR #14 behaviour)
19
21
  // keys (C14, object-keys.js): the object leads and the hand follows; the object is still drawn
20
22
  // in the hand, so what the hand cannot reach shows as a residual (QA)
@@ -29,7 +31,8 @@ import { groupsOf } from "./scene.js";
29
31
  import { solveFrame } from "./compiler/compile.js";
30
32
  import { channelsAt } from "./compiler/channels.js";
31
33
  import { resolveBone } from "./bones.js";
32
- import { isFree, freePoseAt, beforeGrab, afterRelease } from "./free-motion.js";
34
+ import { isFree, freePoseAt, beforeGrab, afterRelease, fallsAfter, corners, topOf, under, TOUCH } from "./free-motion.js";
35
+ import { add } from "./compiler/math.js";
33
36
 
34
37
  /**
35
38
  * Body ids that make up `id` in a motionScene: [id] for a body (plus any bodies parented under
@@ -68,7 +71,7 @@ export function spanOf(out, o) {
68
71
  /** Without at/along a grabbed body, and a group, keeps its grab-frame palm offset; at/along pin one (a kit root, C15). */
69
72
  const keepsOffset = (scene, id, o) => o.at === undefined && o.along === undefined && ((!scene?.objects?.[id] && !!groupsOf(scene)[id]) || o.grab !== undefined);
70
73
 
71
- const solved = new WeakMap(), poses = new WeakMap();
74
+ const solved = new WeakMap(), poses = new WeakMap(), riders = new WeakMap();
72
75
 
73
76
  /** What free-motion.js reads of a compile result: the spec, the scene and the time map. */
74
77
  const freeCtx = out => ({ spec: out.ctx.spec, scene: out.ctx.scene, timeMap: out.timeMap });
@@ -84,8 +87,36 @@ export function restingAt(out, id, f) {
84
87
  return root && (mover === id ? root : compose(root, relative(restPose(scene, mover), restPose(scene, id))));
85
88
  }
86
89
 
87
- /** The solved frame at absolute f: out.frames, or solved once on demand outside a window. */
88
- function frameAt(out, f) {
90
+ /**
91
+ * The spec.objects key a carry `id` (entry o) rides after its release: the key that moves the highest body under the
92
+ * record's centre at the release frame, when that key moves on its own (keys, a fall or a hinge; never a hand or a follow)
93
+ * and is not the record itself. null when the carry falls after its release, is never let go, or nothing under it moves.
94
+ */
95
+ export function riderOf(out, id) {
96
+ if (!riders.has(out)) riders.set(out, new Map());
97
+ const cache = riders.get(out);
98
+ if (cache.has(id)) return cache.get(id);
99
+ const { spec, scene } = out.ctx, o = spec.objects?.[id], rest = restPose(scene, id), parts = partsOf(scene, id);
100
+ let best = null;
101
+ if (isCarried(o) && o.release !== undefined && rest && parts.length) {
102
+ const release = spanOf(out, o)[1];
103
+ if (release < out.frameCount - 1 && !fallsAfter(freeCtx(out), o, release)) {
104
+ const at = carriedAt(out, id, o, release), pts = parts.flatMap(p => corners({ ...compose(at, relative(rest, scene.objects[p])), half: scene.objects[p].half }));
105
+ const low = Math.min(...pts.map(p => p[1])), c = pts.reduce((a, p) => add(a, p), [0, 0, 0]).map(v => v / pts.length);
106
+ for (const [k, body] of Object.entries(scene.objects)) {
107
+ const mover = parts.includes(k) ? null : moverOf(spec, scene, k), entry = spec.objects?.[mover];
108
+ if (!mover || mover === id || isCarried(entry) || isFollow(entry)) continue;
109
+ const box = { ...objectPoseAt(out, k, release), half: body.half }, top = topOf(box);
110
+ if (under(box, c) && top <= low + TOUCH && (!best || top > best.top)) best = { mover, top };
111
+ }
112
+ }
113
+ }
114
+ cache.set(id, best?.mover ?? null);
115
+ return best?.mover ?? null;
116
+ }
117
+
118
+ /** The solved frame at absolute f: out.frames, or solved once on demand outside a window (or ahead of the frame being solved). */
119
+ export function frameAt(out, f) {
89
120
  const own = out.frames[f - (out.f0 ?? 0)];
90
121
  if (own) return own;
91
122
  if (!solved.has(out)) solved.set(out, new Map());
@@ -129,6 +160,8 @@ export function rootPoseAt(out, id, f, now = null) {
129
160
  if (f < grab && rest) return restingAt(out, id, f);
130
161
  const after = f > release ? afterRelease(freeCtx(out), id, o, f, release, carriedAt(out, id, o, release), release > 0 ? carriedAt(out, id, o, release - 1) : null) : null;
131
162
  if (after) return after;
163
+ const by = f > release ? riderOf(out, id) : null;
164
+ if (by) { const at = carriedAt(out, id, o, release); return compose(rootPoseAt(out, by, f), relative(rootPoseAt(out, by, release), at)); }
132
165
  return now && g === f ? carriedOn(out, id, o, f, now.s) : carriedAt(out, id, o, g);
133
166
  }
134
167
  if (!rest) return null;
@@ -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)) {
@@ -7,7 +7,8 @@
7
7
  // and scene.others (take-source.js), so the groups are rebuilt from the entries: groupsOf.
8
8
  // `others` lists the records with no box (an Empty, a light, an effect, a plane) by id with their
9
9
  // renderer and ancestors: a switch entry (C15) may name them, and a skill finds a kit's lit parts
10
- // there. A body that glows (record.glow) says so (`glow: true`).
10
+ // there. A body that glows (record.glow) says so (`glow: true`); a record's grasp sockets (record.grasp,
11
+ // ../object-grasp.js) ride along for the hand (sockets.js).
11
12
  import { objectSize } from "../object-size.js";
12
13
  import { mul, rx, ry, rz } from "./compiler/math.js";
13
14
 
@@ -50,11 +51,11 @@ export function motionScene(objects = []) {
50
51
  if (!o || o.hidden) continue;
51
52
  const size = objectSize(o), up = ancestorsOf(o, byId);
52
53
  if (NO_BODY.has(o.renderer) || !(size.width > 0 && size.height > 0 && size.depth > 0)) {
53
- others[o.id] = { renderer: o.renderer, ancestors: up.map(p => p.id) };
54
+ others[o.id] = { renderer: o.renderer, ancestors: up.map(p => p.id), ...(o.grasp ? { grasp: o.grasp } : {}) };
54
55
  continue;
55
56
  }
56
57
  out[o.id] = { name: o.name, renderer: o.renderer, ...poseOf(o), half: [size.width / 2, size.height / 2, size.depth / 2],
57
- ancestors: up.map(p => p.id), ...(up.length ? { ancestorPoses: up.map(poseOf) } : {}), ...(o.glow ? { glow: true } : {}) };
58
+ ancestors: up.map(p => p.id), ...(up.length ? { ancestorPoses: up.map(poseOf) } : {}), ...(o.glow ? { glow: true } : {}), ...(o.grasp ? { grasp: o.grasp } : {}) };
58
59
  }
59
60
  return { objects: out, groups: groupsOf({ objects: out }), others };
60
61
  }
@@ -11,7 +11,7 @@ export const KEY_FIELDS = ["f", "set", "step", "src"];
11
11
  export const USE_FIELDS = ["skill", "args", "f0"];
12
12
  /** Anchor fields by anchor kind (besides `kind`). */
13
13
  export const ANCHOR_FIELDS = { world: ["p"], body: ["p"], bone: ["bone", "offset"], segment: ["from", "to", "t", "tMin", "offset", "reach"],
14
- object: ["object", "local"], landmark: ["name"], prop: ["prop", "point", "offset"], person: ["person", "bone", "offset"] };
14
+ object: ["object", "local", "socket", "grasp"], landmark: ["name"], prop: ["prop", "point", "offset"], person: ["person", "bone", "offset"] };
15
15
  export const PROP_FIELDS = ["hand", "origin", "points"];
16
16
  export const FIT_FIELDS = ["side", "anchor", "effector", "offset", "orient"];
17
17
  /** spec.objects[id]: a hinge or a carry (a body or a group id), see C3; carry keys: C14; a switch (any record, C15) moves nothing; a follow (any record, C16) rides a point of the body; a free entry (C17) moves by keys and falls. */
@@ -48,7 +48,7 @@ export const EXAMPLES = {
48
48
  };
49
49
 
50
50
  const UNITS = `World metres, +Y up; ${CONVENTIONS.yaw}; the character's left is +X at yaw 0. Angles in degrees.`;
51
- const NO_JUMP = "Without at/along it keeps its grab-frame offset to the palm: no jump.";
51
+ const NO_JUMP = "Without at/along it keeps its grab-frame palm offset: no jump.";
52
52
  /** The objects example's carry: a grip prop and the object it holds. */
53
53
  const CARRY = { props: EXAMPLES.objects.props, objects: { cup: EXAMPLES.objects.objects.cup } };
54
54
 
@@ -65,13 +65,14 @@ 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; 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.",
68
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."],
69
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.",
70
71
  "fits: the hand target moves so the effector (a prop point or the mean of joints), not the wrist, lands on the anchor.",
71
72
  "A fit on a scene body also turns the hand about the forearm into a grip: a thin body (a can, a bar, a handle, a neck) across the palm, else thumb up; never past a comfortable forearm turn. A wrist you turn (wtwist/wflex) wins; fit.orient: false (reachGrip args.orient: false) turns it off.",
72
73
  "reachGrip adds props.<anchor>Grip and its fit. Carry a scene object with objects.<id>.prop (topic objects)."],
73
74
  objects: ["A hinge turns the object (a body or a group) about pivot/axis by the angle channel your keys set (degrees).",
74
- "A carry follows the prop's hand: before grab the scene pose, grab..release in the hand, after release frozen where it was let go.",
75
+ "A carry follows the prop's hand: before grab the scene pose, grab..release in the hand, after release where it was let go, riding a body under it that keys or a hinge move.",
75
76
  `No grab: in hand from frame 0; no release: to the end. ${NO_JUMP}`,
76
77
  "A group (Empty) id moves all its parts; at/along pin a group's root to the palm too (an object.createKit kit). A carried object is an obstacle for the body and the scene.",
77
78
  "A switch { visible?, level? } (no prop, no hinge field) moves nothing: it shows or hides any record (a part, a light, an effect) and sets its level, 0..4 light, glow and effect strength "
@@ -108,6 +109,6 @@ export const NOTES = {
108
109
  "object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
109
110
  "Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
110
111
  fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
111
- "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.",
112
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)."],
113
114
  };
@@ -3,7 +3,7 @@
3
3
  // looks each field up here, and throws when one is missing, so a new field needs a type here.
4
4
  // Shared shapes are $refs into DEFS (describe.js adds the ones an answer uses).
5
5
  import { REF_KINDS, OBJECT_KEY_FIELDS } from "./fields.js";
6
- import { CONVENTIONS } from "../compiler/vocab.js";
6
+ import { CONVENTIONS, GRASPS } from "../compiler/vocab.js";
7
7
  import { QA_LIMITS } from "../qa/checks.js";
8
8
  import { CAPSULES } from "../qa/geometry.js";
9
9
 
@@ -64,7 +64,7 @@ export const ANCHOR_TYPES = {
64
64
  from: str("bone"), to: str("bone"), t: num("0 = from .. 1 = to", { default: 0.5 }), tMin: num("with reach: the lowest t to slide back to", { default: 0.15 }),
65
65
  reach: side("slide toward `from` until this arm reaches"), object: str("scene object id"), local: ref("vec3", "point in the object's frame, m"),
66
66
  name: { const: "mouth" }, prop: str("a prop held by the other hand"), point: str("a point of that prop, or origin"),
67
- person: str("a character id"),
67
+ person: str("a character id"), socket: str("a socket of the object"), grasp: { enum: GRASPS, description: "the hand takes it by a socket" },
68
68
  };
69
69
 
70
70
  const palm = d => ref("vec3", `${d} [d, l, n] on the palm axes (d to the middle finger, l to the thumb side, n = d x l), m`);
@@ -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 = {
@@ -4,19 +4,37 @@ import { add, apply, ry } from "../compiler/math.js";
4
4
  import { walk } from "../compiler/walk.js";
5
5
  import { keyBook, skillError } from "./book.js";
6
6
  import { walkEnd } from "./info.js";
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";
7
10
 
8
- /** standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). */
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). */
12
+ const graspPoint = (grasp, k) => (GRASP_POINTS[grasp] ? GRASP_POINTS[grasp].map((v, i) => (i === 2 && k === "r" ? -v : v)) : [0, 0, 0]);
13
+
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
+ */
9
19
  export function standIdle(ctx) {
10
20
  const { sk, args, state } = ctx, I = sk.index, P = sk.restWorldPos, ank = P[I.LeftFoot][1];
11
- const h = args.heading ?? state.yaw ?? 0, base = args.at ?? (state.root ? [state.root[0], state.root[2]] : null);
12
- 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
+ }
13
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; };
14
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,
15
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],
16
34
  armw_l: 0, armw_r: 0, aswing_l: 0, aswing_r: 0, abend_l: 0, abend_r: 0, arm_hang: 1 };
17
35
  const keys = [{ f: ctx.f0, set }];
18
36
  if (args.until !== undefined) keys.push({ f: args.until, set: structuredClone(set) });
19
- return { keys, end: args.until ?? ctx.f0 };
37
+ return { keys, end: args.until ?? ctx.f0, ...(placed ? { placed } : {}) };
20
38
  }
21
39
 
22
40
  /**
@@ -48,6 +66,23 @@ export function walkTo(ctx) {
48
66
  return { keys, end };
49
67
  }
50
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
+
51
86
  /**
52
87
  * reachGrip({ anchor, side, reach = 20, hold = 22, release = 12, elbow, fingers = "grip",
53
88
  * define, orient = true }): the hand goes to the anchor (defined elsewhere, or by `define`) over
@@ -59,25 +94,44 @@ export function reachGrip(ctx) {
59
94
  const { args, state } = ctx, k = args.side, name = args.anchor;
60
95
  if (k !== "l" && k !== "r") skillError(ctx.tag, `args.side must be "l" or "r", got ${JSON.stringify(k)}`);
61
96
  if (typeof name !== "string" || /_[lr]$/.test(name)) skillError(ctx.tag, "args.anchor must name an anchor (not ending in _l/_r)");
62
- const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12, preset = args.fingers ?? "grip";
97
+ const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12;
98
+ // an anchor that takes a grasp closes the hand to that grasp's fingers (../sockets.js)
99
+ const grasp = (args.define ?? ctx.anchors?.[name])?.grasp, preset = args.fingers ?? GRASP_FINGERS[grasp] ?? "grip";
63
100
  const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
64
- // a take that ends after the hold but before the hand lets go cuts the release: the grip holds to the last frame
65
- 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);
66
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;
67
111
  const b = keyBook(state, ctx.f0);
68
- 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);
69
120
  b.at(reach, { [w]: 1, [fg]: { ...state[fg], [preset]: 1 }, [armw]: 1, ...(args.elbow ? { [el]: args.elbow } : {}) });
70
- b.hold(reach + hold, w, fg, armw, el);
71
- if (!cut) b.at(reach + hold + release, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}) });
72
- 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);
73
127
  const def = args.define ? { [name]: args.define } : {};
74
128
  // the hand may touch what it grips: an object, or the other person (people.js) whose joint the anchor is on
75
129
  const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
76
130
  return {
77
- keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `release cut at f${last} (hold to the end)` } : {}), anchors: def,
78
- props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: [0, 0, 0] } } },
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 },
79
133
  fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
80
134
  checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
81
- allow: obj ? [{ part: "hand", object: obj, depth: 0.04 }] : [] },
135
+ allow: touchable(ctx.scene, obj).map(object => ({ part: "hand", object, depth: 0.04 })) },
82
136
  };
83
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 },
@@ -34,10 +34,10 @@ export const SKILL_INFO = {
34
34
  },
35
35
  reachGrip: {
36
36
  args: { anchor: str("not ending in _l/_r"), side: side("the hand"), reach: int("frames to reach", { default: 20 }), hold: int("frames held", { default: 22 }),
37
- release: int("frames to let go", { default: 12 }), elbow: ref("vec3", "elbow pole"), fingers: { enum: FINGER_PRESETS, default: "grip" },
38
- define: { type: "object", description: "an anchor (topic anchors) named args.anchor" }, orient: { type: "boolean", description: "false: no grip turn" } },
39
- required: ["anchor", "side"], needs: "args.anchor in spec.anchors or args.define", frames: "reach + hold + release (54 by default)",
40
- adds: ["props.<anchor>Grip (fingers point)", "fits: fingers onto the anchor", "checks.contacts \"fingers on <anchor>\"", "checks.allow hand on the anchor's object",
37
+ release: int("frames to let go", { default: 12 }), elbow: ref("vec3", "pole"), fingers: { enum: FINGER_PRESETS, default: "grip" },
38
+ define: { type: "object", description: "defines anchor args.anchor" }, orient: { type: "boolean", description: "false: no grip turn" } },
39
+ required: ["anchor", "side"], needs: "args.anchor in anchors or define", frames: "reach + hold + release (54)",
40
+ adds: ["props.<anchor>Grip (fingers point)", "fits: fingers on the anchor", "checks.contacts \"fingers on <anchor>\"", "checks.allow hand on its object",
41
41
  "keys: w_<anchor>_<side>, fingers_<side>, armw_<side>"],
42
42
  example: { f0: 10, args: { anchor: "bar", side: "r", define: { kind: "world", p: [-0.2, 1.2, 0.33] } } },
43
43
  tune: { reach: 2, release: 2 },