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