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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// How a person's reach to grasp unfolds in time (reachGrip's keys), from the prehension literature:
|
|
2
|
+
// transport the wrist's speed is a single bell whose peak comes early, at about a third of the movement, and the
|
|
3
|
+
// longer deceleration (about two thirds) is the slow approach onto the object (time to peak velocity 33 %
|
|
4
|
+
// of movement time: Paulignan et al., Exp Brain Res 1991, doi:10.1007/BF00231858). A minimum-jerk bell
|
|
5
|
+
// (Flash & Hogan 1985) on time warped by t^WARP puts the peak at 0.5^(1/WARP) of the reach.
|
|
6
|
+
// grasp the hand stays open wider than the object through the transport and closes onto it in the slow
|
|
7
|
+
// approach: the maximum grip aperture comes at about two thirds to three quarters of the movement (Jeannerod,
|
|
8
|
+
// J Mot Behav 1984; Hu et al., Exp Brain Res 2005), and it scales with the object's size (Marteniuk et al.,
|
|
9
|
+
// Hum Mov Sci 1990), a couple of centimetres wider than the object (MARGIN). These rigs' relaxed hands are
|
|
10
|
+
// already open (thumb tip to index tip: Y Bot 11.5 cm, X Bot 9.7 cm) and there is no wider preset: a thing
|
|
11
|
+
// that fits that opening with the margin (a can, a cup) is reached with the hand held open to CLOSE of the
|
|
12
|
+
// reach, then closed on it; a wider one closes along the reach as before, or the open fingers would sweep
|
|
13
|
+
// into it on the way (a 10 cm box: 6 cm deep). So does a thing that sits more than OVER m in from the near
|
|
14
|
+
// edge of what it stands on: the open fingertips hang below the hand and dragged across the table top on the
|
|
15
|
+
// way (a cup 12 cm in, bench S8). CLOSE is 0.4, earlier than a person's: the hand comes straight in, and the
|
|
16
|
+
// open fingertips (about 8 cm past the finger middles) went into a cup wrapped from the side when they
|
|
17
|
+
// stayed open later (1.5 cm at 0.7); a hand that plans an arc around the thing is the next step.
|
|
18
|
+
// gaze the eyes go to the grasp point before the hand gets there and stay on it until the hand has it
|
|
19
|
+
// (Johansson et al., J Neurosci 2001;21:6917). The rigs have no eyes: the head turns HEAD_SHARE of the
|
|
20
|
+
// way, done by GAZE of the reach, held through the hold, back by the end of the release.
|
|
21
|
+
import { add, apply, len, sub } from "../compiler/math.js";
|
|
22
|
+
import { restState } from "../compiler/skeleton.js";
|
|
23
|
+
import { fingers } from "../compiler/limbs.js";
|
|
24
|
+
import { sdBox } from "../qa/geometry.js";
|
|
25
|
+
|
|
26
|
+
const WARP = 0.65, ARM_WARP = 0.75, HEAD_SHARE = 0.75, MARGIN = 0.025, KNEE = 0.1, OVER = 0.08, BELOW = 0.12;
|
|
27
|
+
export const CLOSE = 0.4, GAZE = 0.3;
|
|
28
|
+
const TRANSPORT = [0.15, 0.3, 0.45, 0.6, 0.7, 0.8, 0.9];
|
|
29
|
+
const minJerk = t => t * t * t * (10 - 15 * t + 6 * t * t);
|
|
30
|
+
/** The share of the way the hand has come at fraction t of the reach. */
|
|
31
|
+
// two phases: the fast transport covers NEAR of the way by SLOW of the time, the low-velocity approach the rest while the
|
|
32
|
+
// fingers close (about half the movement time is that slow approach after peak deceleration, Paulignan et al. 1991)
|
|
33
|
+
const NEAR = 0.9, SLOW = 0.7;
|
|
34
|
+
const transport = (t, warp) => {
|
|
35
|
+
const u = Math.min(1, Math.max(0, t));
|
|
36
|
+
return u < SLOW ? NEAR * minJerk(Math.pow(u / SLOW, warp)) : NEAR + (1 - NEAR) * minJerk((u - SLOW) / (1 - SLOW));
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Inner keys of the transport for a reach of n frames: [frame from f0, the target's share, the arm's IK share]. The arm's
|
|
40
|
+
* IK weight follows a milder bell (ARM_WARP): the hand is the blend of the two, and the target's bell alone on an arm
|
|
41
|
+
* blending in at its plain ease peaked at mid-reach, while both at WARP pushed a cup reach past the QA's one-frame pop.
|
|
42
|
+
*/
|
|
43
|
+
export const transportKeys = n => [...new Map(TRANSPORT.map(t => Math.round(t * n)).filter(f => f > 0 && f < n).map(f => [f, [transport(f / n, WARP), transport(f / n, ARM_WARP)]])).entries()].map(([f, [t, a]]) => [f, t, a]);
|
|
44
|
+
|
|
45
|
+
const DEG = 180 / Math.PI, clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
46
|
+
/**
|
|
47
|
+
* True when the reach to anchor a takes the early-peaked transport: a thing at rest (a world point or a scene object's;
|
|
48
|
+
* a reach to a body, a bone or a prop chases a moving point) above the knees (KNEE m over the rest knee). A person
|
|
49
|
+
* reaching lower bends or crouches, which these keys do not plan: the arm alone, sped up early, swept the thigh and
|
|
50
|
+
* shin on the way to a vending tray (QA 5 -> 7), so a low reach keeps the plain ease.
|
|
51
|
+
*/
|
|
52
|
+
export function earlyTransport(ctx, a) {
|
|
53
|
+
const p = restPoint(ctx, a), I = ctx.sk.index;
|
|
54
|
+
return !!p && p[1] >= ctx.sk.restWorldPos[I.LeftLeg][1] + KNEE;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The world point an anchor rests at before the grab (world, or a scene object's point), or null for one that moves with a body. */
|
|
58
|
+
function restPoint(ctx, a) {
|
|
59
|
+
if (a?.kind === "world" && Array.isArray(a.p)) return a.p.map(Number); // a copy, as in bodyOf
|
|
60
|
+
const o = a?.kind === "object" ? ctx.scene?.objects?.[a.object] : null;
|
|
61
|
+
if (!o || [a.object, ...(o.ancestors ?? [])].some(id => movesBy(ctx, id))) return null;
|
|
62
|
+
return add(o.pos, apply(o.R, a.local ?? [0, o.half?.[1] ?? 0, 0]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* True when spec.objects moves record `id` before the reach is done (ctx.until): a follow or a hinge, or keys or a fall
|
|
67
|
+
* from before that frame (a carry whose keys start at a grab after the hand arrives rests until then).
|
|
68
|
+
*/
|
|
69
|
+
function movesBy(ctx, id) {
|
|
70
|
+
const o = ctx.objects?.[id];
|
|
71
|
+
if (!o || typeof o !== "object") return false;
|
|
72
|
+
if (o.follow !== undefined || o.hinge !== undefined) return true;
|
|
73
|
+
const from = Math.min(...(o.keys ?? []).map(k => k.f).filter(Number.isFinite), o.fall?.at ?? Infinity);
|
|
74
|
+
return from < (ctx.until ?? Infinity);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const opened = new WeakMap();
|
|
78
|
+
/** The rig's relaxed hand opening, thumb tip to index tip (m). */
|
|
79
|
+
function openHand(sk) {
|
|
80
|
+
if (!opened.has(sk)) {
|
|
81
|
+
const s = restState(sk), I = sk.index;
|
|
82
|
+
fingers(sk, s, "Right", {});
|
|
83
|
+
opened.set(sk, len(sub(s.P[I.RightHandThumb4], s.P[I.RightHandIndex4])));
|
|
84
|
+
}
|
|
85
|
+
return opened.get(sk);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The scene body anchor a reaches for (its object, or the body its world point lies in), or null. */
|
|
89
|
+
function bodyOf(ctx, a) {
|
|
90
|
+
const objects = ctx.scene?.objects ?? {};
|
|
91
|
+
if (a?.kind === "object") return objects[a.object]?.half ? objects[a.object] : null;
|
|
92
|
+
if (a?.kind !== "world" || !Array.isArray(a.p)) return null;
|
|
93
|
+
// a copy: the spec's own (frozen) array passed into the shared vector math deoptimised it for the whole compile (2x)
|
|
94
|
+
const p = a.p.map(Number);
|
|
95
|
+
return Object.values(objects).filter(o => o.half && sdBox(p, o) <= 0.01).sort((x, y) => sdBox(p, x) - sdBox(p, y))[0] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** True when, OVER m short of point p toward the character (root r), the hand is still above a surface within BELOW m under p. */
|
|
99
|
+
function overSupport(ctx, p, r, skip) {
|
|
100
|
+
const dx = r[0] - p[0], dz = r[2] - p[2], d = Math.hypot(dx, dz);
|
|
101
|
+
if (d < 1e-6) return false;
|
|
102
|
+
const q = [p[0] + (dx / d) * OVER, p[1], p[2] + (dz / d) * OVER];
|
|
103
|
+
return Object.entries(ctx.scene?.objects ?? {}).some(([id, o]) => {
|
|
104
|
+
if (!o.half || skip.has(id) || skip.has(o.parent)) return false;
|
|
105
|
+
const top = sub(q, [0, BELOW, 0]);
|
|
106
|
+
return sdBox(q, o) > 0 && sdBox(top, o) <= 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* True when the hand stays open on the way to the thing anchor a reaches for, from the pose at f0 (state): it fits the
|
|
112
|
+
* rig's open hand with MARGIN, and the hand's last OVER m to it are not over the surface it rests on.
|
|
113
|
+
*/
|
|
114
|
+
export function holdsOpen(ctx, a, state) {
|
|
115
|
+
const o = bodyOf(ctx, a), p = restPoint(ctx, a) ?? (o && add(o.pos, apply(o.R, [0, o.half[1], 0])));
|
|
116
|
+
if (!o || !p || 2 * Math.min(...o.half) + MARGIN > openHand(ctx.sk)) return false;
|
|
117
|
+
const skip = new Set(Object.entries(ctx.scene.objects).filter(([, x]) => x === o).map(([id]) => id));
|
|
118
|
+
return !(Array.isArray(state.root) && overSupport(ctx, p, state.root, skip));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The head's yaw and pitch (deg, the head's own channels) that turn the gaze HEAD_SHARE of the way onto anchor a from the
|
|
123
|
+
* pose at f0 (state), or null when the anchor moves with a body or the pose has no root.
|
|
124
|
+
*/
|
|
125
|
+
export function gazeAt(ctx, a, state) {
|
|
126
|
+
const p = restPoint(ctx, a), r = state.root;
|
|
127
|
+
if (!p || !Array.isArray(r)) return null;
|
|
128
|
+
const P = ctx.sk.restWorldPos, I = ctx.sk.index, eye = [r[0], r[1] + P[I.Head][1] - P[I.Hips][1], r[2]];
|
|
129
|
+
const d = [p[0] - eye[0], p[1] - eye[1], p[2] - eye[2]], flat = Math.hypot(d[0], d[2]);
|
|
130
|
+
if (flat < 0.05) return null;
|
|
131
|
+
let yaw = Math.atan2(d[0], d[2]) * DEG - (state.yaw ?? 0) - (state.spine_yaw ?? 0);
|
|
132
|
+
while (yaw > 180) yaw -= 360;
|
|
133
|
+
while (yaw < -180) yaw += 360;
|
|
134
|
+
const down = Math.atan2(-d[1], flat) * DEG - (state.pitch ?? 0) - (state.spine_pitch ?? 0);
|
|
135
|
+
return { head_yaw: clamp(HEAD_SHARE * yaw, -60, 60), head_pitch: clamp(HEAD_SHARE * down, -30, 50) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** True when keys before this skill already move the head within [a, b]: a keyed look wins. */
|
|
139
|
+
export const headKeyed = (keys, a, b) => keys.some(k => k.f >= a && k.f <= b && (k.set?.head_yaw !== undefined || k.set?.head_pitch !== undefined));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where a standing body stands (standIdle): clear of the scene's bodies, and within reach of what it reaches for.
|
|
2
|
+
// Session 7f15b4ae: every cup and ball attempt stood with a foot in the table (COLLIDE foot_r x cube over the whole
|
|
3
|
+
// take) and the arm at full reach, while QA's hint already said how far to move the body; the agent kept guessing
|
|
4
|
+
// `at` by hand. Two rules:
|
|
5
|
+
// clear a stance whose legs, feet or hips are inside a body (a table, a box on the floor) slides out of it, the
|
|
6
|
+
// least distance that clears it by MARGIN: straight back first, then sideways, then the diagonals
|
|
7
|
+
// reach standIdle with no `at` (and no root keyed) before a reachGrip in the same spec keeps its heading and steps
|
|
8
|
+
// up to the anchor: the reaching shoulder in line with it, as near as COMFORT of the arm's length allows,
|
|
9
|
+
// then back out of whatever is in the way (it stands at a table's edge)
|
|
10
|
+
// The body is the rest pose's legs, feet and hips as capsules (qa/geometry.js radii); a body whose top is under
|
|
11
|
+
// SUPPORT m (a floor, a mat, a low step) is stood on, not stood clear of.
|
|
12
|
+
import { add, sub, len, apply, ry } from "../compiler/math.js";
|
|
13
|
+
import { sdBox } from "../qa/geometry.js";
|
|
14
|
+
import { partsOf } from "../object-pose.js";
|
|
15
|
+
|
|
16
|
+
const MARGIN = 0.01, SUPPORT = 0.12, COMFORT = 0.8, SLIDE = 0.6, STEP = 0.01, NEAREST = 0.15, HANG = 0.62;
|
|
17
|
+
// [from, to, radius] of the standing body, as in qa/geometry.js CAPSULES (the foot: ankle to the toe tip)
|
|
18
|
+
const PARTS = ["Left", "Right"].flatMap(S => [[`${S}UpLeg`, `${S}Leg`, 0.07], [`${S}Leg`, `${S}Foot`, 0.05], [`${S}Foot`, `${S}ToeBase`, 0.045], [`${S}ToeBase`, `${S}Toe_End`, 0.04]])
|
|
19
|
+
.concat([["LeftUpLeg", "RightUpLeg", 0.095], ["Hips", "Spine2", 0.12]]);
|
|
20
|
+
const DIRS = [[0, -1], [1, 0], [-1, 0], [Math.SQRT1_2, -Math.SQRT1_2], [-Math.SQRT1_2, -Math.SQRT1_2]];
|
|
21
|
+
|
|
22
|
+
/** The standing body's sample points [p, r] at ground point c = [x, z] facing heading h (deg): legs, feet, hips and the arms hanging at the sides (standIdle's FK arms down). */
|
|
23
|
+
function bodyPoints(sk, c, h) {
|
|
24
|
+
const I = sk.index, P = sk.restWorldPos, hip = P[I.Hips], R = ry(h), at = p => add([c[0], 0, c[1]], apply(R, [p[0] - hip[0], p[1], p[2] - hip[2]]));
|
|
25
|
+
const seg = (A, B, r) => [0, 0.25, 0.5, 0.75, 1].map(t => [add(A, sub(B, A).map(v => v * t)), r]);
|
|
26
|
+
const arms = ["Left", "Right"].flatMap(S => { const sh = P[I[`${S}Arm`]], side = Math.sign(sh[0] - hip[0]) * 0.04; return seg(at(sh), at([sh[0] + side, sh[1] - HANG, sh[2]]), 0.045); });
|
|
27
|
+
return PARTS.filter(([a, b]) => I[a] !== undefined && I[b] !== undefined).flatMap(([a, b, r]) => seg(at(P[I[a]]), at(P[I[b]]), r)).concat(arms);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The bodies a stance must stay clear of: every scene body whose top is above SUPPORT m. */
|
|
31
|
+
const obstacles = scene => Object.values(scene?.objects ?? {}).filter(o => o.half && o.pos && o.R && o.pos[1] + 2 * o.half[1] * o.R[1][1] > SUPPORT);
|
|
32
|
+
|
|
33
|
+
/** How far the body at c, h is clear of the obstacles (m; negative: inside one by that much, past MARGIN). */
|
|
34
|
+
function clearance(sk, boxes, c, h) {
|
|
35
|
+
let worst = Infinity;
|
|
36
|
+
for (const [p, r] of bodyPoints(sk, c, h)) for (const b of boxes) worst = Math.min(worst, sdBox(p, b) - r - MARGIN);
|
|
37
|
+
return worst;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A stance at c ([x, z]) facing h moved the least distance out of the scene's bodies: { at, moved } or null when it is clear. */
|
|
41
|
+
export function clearStance(sk, scene, c, h) {
|
|
42
|
+
const boxes = obstacles(scene);
|
|
43
|
+
if (!boxes.length || clearance(sk, boxes, c, h) >= 0) return null;
|
|
44
|
+
const R = ry(h);
|
|
45
|
+
for (let d = STEP; d <= SLIDE + 1e-9; d += STEP) {
|
|
46
|
+
for (const [x, z] of DIRS) {
|
|
47
|
+
const v = apply(R, [x * d, 0, z * d]), at = [c[0] + v[0], c[1] + v[2]];
|
|
48
|
+
if (clearance(sk, boxes, at, h) >= 0) return { at: at.map(n => +n.toFixed(4)), moved: +d.toFixed(2) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The world point an anchor definition names on the still scene, as anchors.js resolves it: world p; an object's (or a
|
|
56
|
+
* group's) `local` point, its base pivot without one; with `grasp` and no `local`, the centre of its parts (where a
|
|
57
|
+
* thin part's socket sits). Null for any other kind.
|
|
58
|
+
*/
|
|
59
|
+
function anchorPoint(scene, a) {
|
|
60
|
+
if (a?.kind === "world" && Array.isArray(a.p)) return a.p;
|
|
61
|
+
if (a?.kind !== "object" || !scene) return null;
|
|
62
|
+
const o = scene.objects?.[a.object] ?? scene.groups?.[a.object];
|
|
63
|
+
if (!o) return null;
|
|
64
|
+
if (!a.grasp || a.local) return add(o.pos, apply(o.R, a.local ?? [0, 0, 0]));
|
|
65
|
+
const parts = partsOf(scene, a.object).map(id => scene.objects[id]).filter(p => p?.half);
|
|
66
|
+
const centres = parts.map(p => add(p.pos, apply(p.R, [0, p.half[1], 0])));
|
|
67
|
+
return centres.reduce((m, p) => add(m, p.map(v => v / centres.length)), [0, 0, 0]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The stance for a standIdle with no `at`: the first reachGrip after it in `use` (index i) and its anchor. The body
|
|
72
|
+
* keeps its facing (args.heading, else the keyed yaw, else 0: the agent turns it by heading) and steps up to the
|
|
73
|
+
* anchor: the reaching shoulder lined up with it, COMFORT of the arm's length from it (at least NEAREST m ahead), then
|
|
74
|
+
* straight back out of any body in the way (a table: the body stands at its edge). { at, heading, anchor, moved } or
|
|
75
|
+
* null when no reachGrip follows or its anchor has no still point.
|
|
76
|
+
*/
|
|
77
|
+
export function reachStance(ctx) {
|
|
78
|
+
const { sk, scene, use = [], index = -1 } = ctx, next = use.slice(index + 1).find(u => u?.skill === "reachGrip");
|
|
79
|
+
const a = next && (next.args?.define ?? ctx.anchors?.[next.args?.anchor]), T = anchorPoint(scene, a);
|
|
80
|
+
if (!T) return null;
|
|
81
|
+
const I = sk.index, P = sk.restWorldPos, S = next.args.side === "l" ? "Left" : "Right", hip = P[I.Hips], sh = P[I[`${S}Arm`]];
|
|
82
|
+
const arm = len(sub(P[I[`${S}ForeArm`]], sh)) + len(sub(P[I[`${S}Hand`]], P[I[`${S}ForeArm`]])) + len(sub(P[I[`${S}HandMiddle2`]], P[I[`${S}Hand`]]));
|
|
83
|
+
const dy = sh[1] - T[1], ahead = Math.max(NEAREST, Math.sqrt(Math.max(0, (COMFORT * arm) ** 2 - dy * dy)));
|
|
84
|
+
const h = ctx.args.heading ?? ctx.state?.yaw ?? 0, R = ry(h), boxes = obstacles(scene);
|
|
85
|
+
const off = apply(R, [sh[0] - hip[0], 0, sh[2] - hip[2] + ahead]), c0 = [T[0] - off[0], T[2] - off[2]];
|
|
86
|
+
let moved = 0, c = c0;
|
|
87
|
+
for (; moved <= SLIDE + 1e-9 && clearance(sk, boxes, c, h) < 0; moved += STEP) {
|
|
88
|
+
const back = apply(R, [0, 0, -(moved + STEP)]);
|
|
89
|
+
c = [c0[0] + back[0], c0[1] + back[2]];
|
|
90
|
+
}
|
|
91
|
+
return { at: c.map(n => +n.toFixed(4)), heading: h, anchor: next.args.anchor, moved: +moved.toFixed(2) };
|
|
92
|
+
}
|
package/src/motion3/sockets.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// bar, a can, a handle) is one too, named by its id, through its centre along its long axis. An object anchor with
|
|
12
12
|
// `grasp` (power, pinch, hook, palm) takes the hand by a socket of its object: `socket` names it, else the one
|
|
13
13
|
// nearest the anchor's `local` point or, without one, nearest the shoulder of the hand, picked once per compile.
|
|
14
|
+
// A disc (a cylinder whose height is under THIN m and whose two sides are wider: a steering wheel) is a rim socket
|
|
15
|
+
// instead: `rim` its radius, `normal` its axis; the hand takes the rim where it is nearest the anchor's `local` point
|
|
16
|
+
// or the hand's shoulder, the knuckle line along the rim (session 2ad2760a: the wheel was held by points in the
|
|
17
|
+
// driver's frame, 15 cm off the rim, and they did not follow the truck). anchorSocket fixes that point once per hand.
|
|
14
18
|
import { add, sub, scale, norm, cross, dot, len, apply, ry, T } from "./compiler/math.js";
|
|
15
19
|
import { partsOf } from "./object-pose.js";
|
|
16
20
|
|
|
@@ -24,6 +28,9 @@ function thinAxis(half) {
|
|
|
24
28
|
return [0, 1, 2].filter(i => i !== long).every(i => half[i] < THIN) ? long : -1;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/** A cylinder flat along its axis (its height under THIN m) and round across it, both sides over THIN m. */
|
|
32
|
+
const isDisc = (renderer, half) => renderer === "cylinder" && !!half && half[1] < THIN && Math.min(half[0], half[2]) >= THIN && Math.abs(half[0] - half[2]) < 0.1 * half[0];
|
|
33
|
+
|
|
27
34
|
/** The record's stored sockets: a body's in scene.objects, a group's (an Empty) in scene.others. */
|
|
28
35
|
const stored = (scene, id) => scene?.objects?.[id]?.grasp ?? scene?.others?.[id]?.grasp ?? null;
|
|
29
36
|
|
|
@@ -36,8 +43,10 @@ export function socketsOf(scene, id) {
|
|
|
36
43
|
own(id, "");
|
|
37
44
|
for (const part of partsOf(scene, id)) {
|
|
38
45
|
if (part !== id) own(part, `${part}.`);
|
|
39
|
-
const half = scene.objects[part]
|
|
40
|
-
|
|
46
|
+
const { half, renderer } = scene.objects[part], long = thinAxis(half);
|
|
47
|
+
// a thin body's radius: the thinner of the two half sizes across the axis (how far the palm sits off the axis)
|
|
48
|
+
if (long >= 0) out.push({ name: part, owner: part, at: [0, half[1], 0], axis: AXES[long], radius: Math.min(...half.filter((_, i) => i !== long)) });
|
|
49
|
+
else if (isDisc(renderer, half)) out.push({ name: part, owner: part, at: [0, half[1], 0], rim: half[0], normal: AXES[1] });
|
|
41
50
|
}
|
|
42
51
|
return out;
|
|
43
52
|
}
|
|
@@ -48,7 +57,14 @@ export const socketNamed = (scene, id, name) => socketsOf(scene, id).find(s => s
|
|
|
48
57
|
/** A socket in world space on its owner's pose { pos, R }: { p, axis?, from?, thumb? }. */
|
|
49
58
|
export function socketWorld(socket, pose) {
|
|
50
59
|
const dir = v => (v ? norm(apply(pose.R, v)) : undefined);
|
|
51
|
-
return { p: add(pose.pos, apply(pose.R, socket.at ?? [0, 0, 0])), axis: dir(socket.axis), from: dir(socket.from), thumb: dir(socket.thumb)
|
|
60
|
+
return { p: add(pose.pos, apply(pose.R, socket.at ?? [0, 0, 0])), axis: dir(socket.axis), from: dir(socket.from), thumb: dir(socket.thumb), radius: socket.radius,
|
|
61
|
+
...(socket.rim ? { rim: socket.rim, normal: dir(socket.normal) } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The point of world rim socket w nearest world point q, and the rim's direction there: { p, axis }. */
|
|
65
|
+
export function rimPoint(w, q) {
|
|
66
|
+
const v = flat(sub(q, w.p), w.normal), r = len(v) > 1e-6 ? norm(v) : norm(flat(Math.abs(w.normal[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0], w.normal));
|
|
67
|
+
return { p: add(w.p, scale(r, w.rim)), axis: norm(cross(w.normal, r)) };
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/** The outward normal of the face of the box (of boxes [{ pos, R, half }], base pivots) nearest world point p, or undefined. */
|
|
@@ -62,9 +78,10 @@ export function faceFrom(boxes, p) {
|
|
|
62
78
|
return best?.n;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
/** How far world point q is from a world socket: from its axis line through p, else from p. */
|
|
81
|
+
/** How far world point q is from a world socket: from its rim, else its axis line through p, else from p. */
|
|
66
82
|
export function socketDistance(w, q) {
|
|
67
83
|
const v = sub(q, w.p);
|
|
84
|
+
if (w.rim) return Math.hypot(dot(v, w.normal), len(flat(v, w.normal)) - w.rim);
|
|
68
85
|
return len(w.axis ? flat(v, w.axis) : v);
|
|
69
86
|
}
|
|
70
87
|
|
package/src/motion3/views.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// C11 own views: the cameras of a verify_result contact_sheet with `views`. They are the
|
|
2
2
|
// agent's own cameras around the subject, never the user's shot camera. A view stays at one
|
|
3
|
-
// place for every frame of the sheet
|
|
3
|
+
// place for every frame of the sheet (or rides with the focus when it travels farther than its
|
|
4
|
+
// size) and frames what matters over those frames (the focus
|
|
4
5
|
// refs, else the whole character, plus any trail), so a small action fills the tile and
|
|
5
6
|
// motion reads as change inside one still frame. Pure over a compile result (compileSpec .out).
|
|
6
7
|
// front in front of the character, 10 degrees up
|
|
@@ -78,7 +79,8 @@ function fit(name, points, back, upHint, aspect) {
|
|
|
78
79
|
* Deterministic: the same compile, frames and arguments give the same cameras.
|
|
79
80
|
*/
|
|
80
81
|
export function viewCameras(out, { views, frames, focus = [], trails = [], aspect = 1 }) {
|
|
81
|
-
|
|
82
|
+
const ride = rideOf(out, frames, focus);
|
|
83
|
+
let points = frames.flatMap(f => focus.flatMap(ref => refPoints(out, f, ref)).map(p => (ride ? sub(p, ride[f]) : p)));
|
|
82
84
|
const body = frames.flatMap(f => jointsAt(out, f));
|
|
83
85
|
if (!points.length) points = body;
|
|
84
86
|
points = [...points, ...trails.flatMap(t => t.points.map(q => q.p))];
|
|
@@ -86,12 +88,33 @@ export function viewCameras(out, { views, frames, focus = [], trails = [], aspec
|
|
|
86
88
|
const { left, forward } = facingOf(out, frames), hips = mean(frames.map(f => jointsAt(out, f)[out.ctx.sk.index.Hips]).filter(Boolean));
|
|
87
89
|
const side = focus.length && dot(sub(mean(points), hips), left) > 0.02 ? left : scale(left, -1);
|
|
88
90
|
return views.map(name => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
91
|
+
const cam = name === "top" ? fit(name, points, UP, forward, aspect)
|
|
92
|
+
: (e => fit(name, points, add(scale(name === "front" ? forward : name === "side" ? side : norm(add(forward, side)), Math.cos(e)), scale(UP, Math.sin(e))), UP, aspect))(rad(ELEVATION[name]));
|
|
93
|
+
return ride ? { ...cam, ride } : cam;
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The focus's travel over the frames: { [frame]: its centre there less its mean centre } when it moves farther than its
|
|
99
|
+
* own size (a wheel in a driving truck, session 2ad2760a: one camera for the whole drive framed the road and showed the
|
|
100
|
+
* hands too small to read), else null. A riding view keeps its place relative to the focus (cameraAt).
|
|
101
|
+
*/
|
|
102
|
+
function rideOf(out, frames, focus) {
|
|
103
|
+
if (!focus.length) return null;
|
|
104
|
+
const at = frames.map(f => ({ f, points: focus.flatMap(ref => refPoints(out, f, ref)) })).filter(x => x.points.length);
|
|
105
|
+
if (at.length < 2) return null;
|
|
106
|
+
const centres = at.map(x => mean(x.points)), mid = mean(centres);
|
|
107
|
+
const size = Math.max(MIN_HALF, ...at.map((x, i) => Math.max(...x.points.map(p => Math.hypot(...sub(p, centres[i]))))));
|
|
108
|
+
if (Math.max(...centres.map(c => Math.hypot(...sub(c, mid)))) <= size) return null;
|
|
109
|
+
return Object.fromEntries(at.map((x, i) => [x.f, sub(centres[i], mid)]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** View camera `cam` at `frame`: a riding view moved by the focus's travel there (rideOf), else cam itself. */
|
|
113
|
+
export function cameraAt(cam, frame) {
|
|
114
|
+
const d = cam.ride?.[frame];
|
|
115
|
+
return d ? { ...cam, eye: add(cam.eye, d), target: add(cam.target, d) } : cam;
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
// A picture hides distance along its own line of sight, so a sheet holds two views at least APART
|
|
96
119
|
// degrees apart. Front and three-quarter (45 degrees apart) look along nearly one line: the
|
|
97
120
|
// guitarist's right hand 20 cm in front of the guitar (session df388a9b) showed in neither.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The Studio's agent binding: inspection, admission and the agent's commands
|
|
2
2
|
// over the editor's native state. App.jsx supplies the ports (reads, commits,
|
|
3
3
|
// history, the action registry); this module owns no React or renderer state.
|
|
4
|
+
import { taskTargetPorts } from "./bus-task-targets.js";
|
|
4
5
|
import { createCommandBus } from "./command-bus.js";
|
|
5
6
|
import { readElementDocument, elementReadback, elementPatchArgs, elementTarget, elementPatchReceipt } from "./commands/elements.js";
|
|
6
7
|
import { physicsKeyStamp } from "./motion/physics-review.js";
|
|
@@ -220,8 +221,7 @@ export function createStudioAppBinding(ports) {
|
|
|
220
221
|
}, recordAction: (...args) => ports.recordAction(...args), beginAction: (...args) => ports.beginAction(...args),
|
|
221
222
|
readback: actionReadback, remember, receipt: id => receipts.get(id), isRetained: receipt => ports.isRetained(receipt),
|
|
222
223
|
canUndo: receipt => ports.canUndo(receipt), undo: () => ports.undo(), redo: () => ports.redo(),
|
|
223
|
-
history: redo => ports.history?.(redo), finishHistoryGesture: () => ports.finishHistoryGesture?.(),
|
|
224
|
-
readTarget: id => { refresh(); return tokens.get(id)?.token; },
|
|
224
|
+
history: redo => ports.history?.(redo), finishHistoryGesture: () => ports.finishHistoryGesture?.(), readTarget: id => { refresh(); return tokens.get(id)?.token; }, ...taskTargetPorts(ports),
|
|
225
225
|
captureToasts: listener => ports.captureToasts?.(listener), showRefusal: message => ports.showRefusal?.(message), emit: event => ports.emitCommandEvent?.(event),
|
|
226
226
|
} });
|
|
227
227
|
return actionBus;
|
|
@@ -4,7 +4,7 @@ export const STUDIO_ERROR_CODES = Object.freeze([
|
|
|
4
4
|
"INVALID_ARGUMENT", "INVALID_CONTEXT", "INVALID_IDENTITY", "INVALID_TURN_ID", "INVALID_SESSION_ID", "INVALID_RECEIPT", "INVALID_RANGE", "INVALID_REQUEST",
|
|
5
5
|
"UNKNOWN_TOOL", "UNKNOWN_VARIANT", "DUPLICATE_NAME", "CONTEXT_LIMIT", "CONTEXT_TOO_LARGE", "AMBIGUOUS_TARGET", "AMBIGUOUS_BASIS", "TARGET_NOT_READY", "TARGET_BUSY",
|
|
6
6
|
"STALE_TARGET", "STALE_SCENE", "STALE_ENVIRONMENT", "STALE_CURSOR", "CAPABILITY_MISSING", "LIVE_HUB_UNAVAILABLE", "AUTH_REQUIRED", "GENERATION_LIMIT", "RATE_LIMITED", "BACKEND_UNAVAILABLE",
|
|
7
|
-
"VERIFICATION_FAILED", "REPAIR_REGRESSED", "CANCELLED", "UNCERTAIN_APPLY", "UNDO_CONFLICT", "CONFIRMATION_REQUIRED", "TIMEOUT", "UNDO_EXPIRED", "INTERNAL_ERROR",
|
|
7
|
+
"VERIFICATION_FAILED", "REPAIR_REGRESSED", "CANCELLED", "UNCERTAIN_APPLY", "UNDO_CONFLICT", "CONFIRMATION_REQUIRED", "TIMEOUT", "UNDO_EXPIRED", "INTERNAL_ERROR", "SCOPE_DENIED", "SCOPE_BUSY",
|
|
8
8
|
]);
|
|
9
9
|
/**
|
|
10
10
|
* A refusal's code. A protocol error carries its own; a JavaScript runtime error (TypeError, ReferenceError, RangeError,
|
|
@@ -9,7 +9,7 @@ import { compileSpec } from "./motion3/pipeline.js";
|
|
|
9
9
|
import { motionScene } from "./motion3/scene.js";
|
|
10
10
|
import { skeletonFor } from "./motion3/rigs/index.js";
|
|
11
11
|
import { beatFrames, grabReleases, traceSeries, TRACE_COLORS } from "./motion3/beats.js";
|
|
12
|
-
import { depthViews, projectPoint, trailPoints, viewCameras } from "./motion3/views.js";
|
|
12
|
+
import { cameraAt, depthViews, projectPoint, trailPoints, viewCameras } from "./motion3/views.js";
|
|
13
13
|
import { checkRef } from "./motion3/refs.js";
|
|
14
14
|
import { renderWithGhosts } from "./ghost-set.js";
|
|
15
15
|
import { GHOST_COLORS, sheetGhostSettings } from "./view-ghosts.js";
|
|
@@ -92,14 +92,14 @@ function viewSheet(args, r, { s, frames, span, grid, picks, ports, added }) {
|
|
|
92
92
|
if (!cameras.length) throw new StudioProtocolError("TARGET_NOT_READY", "views found no compiled frame of the take in the range.");
|
|
93
93
|
const cells = cameras.flatMap(cam => frames.map(frame => ({ cam, frame }))), target = args.ghosts ? s.targets.get(r.characterId) : null;
|
|
94
94
|
if (args.ghosts && !target?.rig) throw new StudioProtocolError("TARGET_NOT_READY", "ghosts pose a copy of the character's rig; it has not loaded in the editor yet.");
|
|
95
|
-
const sheet = buildContactSheet(cells, ({ cam, frame }) => {
|
|
96
|
-
const render = () => ports.renderFrameBuffer(frame, { eye: cam.eye, quaternion: cam.quaternion, fovDeg: cam.fovDeg, output: { width: side, height: side } });
|
|
95
|
+
const sheet = buildContactSheet(cells, ({ cam: view, frame }) => {
|
|
96
|
+
const cam = cameraAt(view, frame), render = () => ports.renderFrameBuffer(frame, { eye: cam.eye, quaternion: cam.quaternion, fovDeg: cam.fovDeg, output: { width: side, height: side } });
|
|
97
97
|
const tile = target ? renderWithGhosts({ rig: target.rig, take: target.motion, objects: s.objects }, frame, render) : render();
|
|
98
98
|
const dots = trails.flatMap(t => t.points.map(({ f, p }) => [f, projectPoint(cam, p, tile.width, tile.height), rgb(t.color)])
|
|
99
99
|
.filter(([, q]) => q).map(([f, [x, y], color]) => ({ x, y, color, r: f === frame ? 4 : 1.5, alpha: f <= frame ? 1 : 0.35, ring: f === frame })));
|
|
100
100
|
return { ...tile, data: paintDots(tile.data, tile.width, tile.height, dots) };
|
|
101
101
|
}, grid);
|
|
102
|
-
const views = cameras.map(cam => ({ name: cam.name, eye: round2(cam.eye), target: round2(cam.target) }));
|
|
102
|
+
const views = cameras.map(cam => ({ name: cam.name, eye: round2(cam.eye), target: round2(cam.target), ...(cam.ride ? { rides: "focus" } : {}) }));
|
|
103
103
|
const ref = { frames, layout: grid.layout, views, ...(added ? { addedView: added } : {}), focus: focus.length ? focus : "character", ...(trails.length ? { trails: trails.map(({ ref: of, color }) => ({ ref: of, color })) } : {}), ...(picks ? { picks } : {}),
|
|
104
104
|
...(target ? { ghosts: { ...sheetGhostSettings(), ...GHOST_COLORS } } : {}) };
|
|
105
105
|
return [{ dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: grid.layout, views: views.map(v => v.name) }, ref];
|
package/tools/run-tests.mjs
CHANGED
|
@@ -14,7 +14,7 @@ if (!process.env.COZYCLAY_AGENT_SESSIONS_DIR) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const NODE_FILES = [
|
|
17
|
-
"test/motion3/verify-motion3-core.mjs", "test/motion3/verify-motion3-idle.mjs", "test/motion3/verify-motion3-flip.mjs", "test/motion3/verify-motion3-timing.mjs", "test/motion3/verify-motion3-self.mjs", "test/motion3/verify-motion3-commands.mjs", "test/motion3/verify-motion3-exitcar.mjs", "test/motion3/verify-motion3-agentloop.mjs", "test/motion3/verify-motion3-search.mjs", "test/motion3/verify-motion3-skills.mjs", "test/motion3/verify-motion3-carried.mjs", "test/motion3/verify-motion3-people.mjs", "test/motion3/verify-motion3-hit.mjs", "test/motion3/verify-motion3-socket.mjs", "test/motion3/verify-motion3-contracts.mjs", "test/motion3/verify-motion3-describe.mjs", "test/motion3/verify-motion3-timeline.mjs", "test/motion3/verify-motion3-probes.mjs", "test/motion3/verify-motion3-grab.mjs", "test/motion3/verify-motion3-grab-blend.mjs", "test/motion3/verify-motion3-patch.mjs", "test/motion3/verify-motion3-repair.mjs", "test/motion3/verify-motion3-specstore.mjs", "test/motion3/verify-motion3-visual.mjs", "test/motion3/verify-motion3-goals.mjs", "test/motion3/verify-motion3-prompt.mjs", "test/motion3/verify-motion3-cup.mjs", "test/verify-proxy-figure.mjs", "test/verify-text-object.mjs",
|
|
17
|
+
"test/motion3/verify-motion3-core.mjs", "test/motion3/verify-motion3-idle.mjs", "test/motion3/verify-motion3-flip.mjs", "test/motion3/verify-motion3-timing.mjs", "test/motion3/verify-motion3-self.mjs", "test/motion3/verify-motion3-commands.mjs", "test/motion3/verify-motion3-exitcar.mjs", "test/motion3/verify-motion3-agentloop.mjs", "test/motion3/verify-motion3-search.mjs", "test/motion3/verify-motion3-skills.mjs", "test/motion3/verify-motion3-carried.mjs", "test/motion3/verify-motion3-people.mjs", "test/motion3/verify-motion3-hit.mjs", "test/motion3/verify-motion3-socket.mjs", "test/motion3/verify-motion3-reach-human.mjs", "test/motion3/verify-motion3-stance-grasp.mjs", "test/motion3/verify-motion3-grasp-friction.mjs", "test/motion3/verify-motion3-grasp-measure.mjs", "test/motion3/verify-motion3-contracts.mjs", "test/motion3/verify-motion3-describe.mjs", "test/motion3/verify-motion3-timeline.mjs", "test/motion3/verify-motion3-probes.mjs", "test/motion3/verify-motion3-grab.mjs", "test/motion3/verify-motion3-grab-blend.mjs", "test/motion3/verify-motion3-patch.mjs", "test/motion3/verify-motion3-repair.mjs", "test/motion3/verify-motion3-specstore.mjs", "test/motion3/verify-motion3-visual.mjs", "test/motion3/verify-motion3-goals.mjs", "test/motion3/verify-motion3-prompt.mjs", "test/motion3/verify-motion3-cup.mjs", "test/verify-proxy-figure.mjs", "test/verify-text-object.mjs",
|
|
18
18
|
"test/verify-proxy-figure-bounds.mjs",
|
|
19
19
|
"test/verify-character-capabilities.mjs",
|
|
20
20
|
"test/verify-root-path.mjs",
|