cozyclay 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/agent/pi-tools.mjs +12 -1
- package/bin/agent/studio-context-budget.mjs +8 -2
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +1 -1
- package/dist/app/index.html +4 -4
- package/dist/assets/analytics-DbKG0NgD.js +6 -0
- package/dist/assets/app-34i-iUy1.js +5088 -0
- package/dist/assets/compile-worker-DYpa2nvO.js +5 -0
- package/dist/assets/{first-shot-handoff-DNO8kv_N.js → first-shot-handoff-CrGZ3n_O.js} +1 -1
- package/dist/assets/helvetiker-bold.typeface-BUlyMz0-.js +13 -0
- package/dist/assets/{landing-yfZaJkWc.js → landing-mq5bnqFE.js} +1 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +20 -0
- package/dist/assets/{workflow-D78eiKTa.js → workflow-dUhurYUc.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/agent-turn-mark.js +33 -0
- package/src/command-bus.js +8 -9
- package/src/commands/elements/character.js +2 -1
- package/src/commands/elements/motion.js +7 -1
- package/src/commands/elements/scene.js +3 -0
- package/src/commands/elements/stage.js +2 -1
- package/src/commands/elements/take-read.js +31 -0
- package/src/commands/elements.js +16 -2
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -2
- package/src/motion3/compiler/anchors.js +60 -4
- package/src/motion3/compiler/compile.js +5 -1
- package/src/motion3/compiler/grip.js +8 -2
- package/src/motion3/compiler/socket-grip.js +107 -0
- package/src/motion3/compiler/validate-parts.js +7 -2
- package/src/motion3/compiler/vocab.js +6 -0
- package/src/motion3/free-motion.js +12 -4
- package/src/motion3/object-keys.js +36 -5
- package/src/motion3/object-pose.js +38 -5
- package/src/motion3/scene.js +4 -3
- package/src/motion3/schema/fields.js +1 -1
- package/src/motion3/schema/topics.js +3 -2
- package/src/motion3/schema/types.js +2 -2
- package/src/motion3/skills/basic.js +8 -2
- package/src/motion3/skills/info.js +4 -4
- package/src/motion3/sockets.js +104 -0
- package/src/motion3/take-edit.js +1 -1
- package/src/motion3/take.js +6 -4
- package/src/object-catalog.jsx +3 -1
- package/src/object-grasp.js +38 -0
- package/src/object-look.js +2 -0
- package/src/object-travel.js +6 -3
- package/src/panels/ObjectLookPanel.jsx +24 -3
- package/src/props.jsx +5 -5
- package/src/scene-objects.js +19 -11
- package/src/shell/details-selection.js +2 -1
- package/src/studio-agent-protocol.js +5 -6
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +3 -1
- package/src/text/fonts/helvetiker-LICENSE.txt +13 -0
- package/src/text/fonts/helvetiker-bold.typeface.json +1 -0
- package/src/text/text-object.jsx +106 -0
- package/src/text/text-record.js +122 -0
- package/tools/run-tests.mjs +1 -1
- package/dist/assets/analytics-BWCqdgWA.js +0 -1
- package/dist/assets/app-DfTCxaKk.js +0 -5087
- package/dist/assets/compile-worker-BxKXh07s.js +0 -5
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +0 -20
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
88
|
-
|
|
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;
|
package/src/motion3/scene.js
CHANGED
|
@@ -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
|
|
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); 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
|
|
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 "
|
|
@@ -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`);
|
|
@@ -4,6 +4,10 @@ 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
|
+
|
|
9
|
+
/** 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
|
+
const graspPoint = (grasp, k) => (GRASP_POINTS[grasp] ? GRASP_POINTS[grasp].map((v, i) => (i === 2 && k === "r" ? -v : v)) : [0, 0, 0]);
|
|
7
11
|
|
|
8
12
|
/** standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). */
|
|
9
13
|
export function standIdle(ctx) {
|
|
@@ -59,7 +63,9 @@ export function reachGrip(ctx) {
|
|
|
59
63
|
const { args, state } = ctx, k = args.side, name = args.anchor;
|
|
60
64
|
if (k !== "l" && k !== "r") skillError(ctx.tag, `args.side must be "l" or "r", got ${JSON.stringify(k)}`);
|
|
61
65
|
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
|
|
66
|
+
const reach = args.reach ?? 20, hold = args.hold ?? 22, release = args.release ?? 12;
|
|
67
|
+
// an anchor that takes a grasp closes the hand to that grasp's fingers (../sockets.js)
|
|
68
|
+
const grasp = (args.define ?? ctx.anchors?.[name])?.grasp, preset = args.fingers ?? GRASP_FINGERS[grasp] ?? "grip";
|
|
63
69
|
const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
|
|
64
70
|
// a take that ends after the hold but before the hand lets go cuts the release: the grip holds to the last frame
|
|
65
71
|
const last = ctx.frames - 1, cut = ctx.f0 + reach + hold <= last && ctx.f0 + reach + hold + release > last;
|
|
@@ -75,7 +81,7 @@ export function reachGrip(ctx) {
|
|
|
75
81
|
const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
|
|
76
82
|
return {
|
|
77
83
|
keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `release cut at f${last} (hold to the end)` } : {}), anchors: def,
|
|
78
|
-
props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers:
|
|
84
|
+
props: { [prop]: { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: graspPoint(grasp, k) } } },
|
|
79
85
|
fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
|
|
80
86
|
checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
|
|
81
87
|
allow: obj ? [{ part: "hand", object: obj, depth: 0.04 }] : [] },
|
|
@@ -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", "
|
|
38
|
-
define: { type: "object", description: "
|
|
39
|
-
required: ["anchor", "side"], needs: "args.anchor in
|
|
40
|
-
adds: ["props.<anchor>Grip (fingers point)", "fits: fingers
|
|
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 },
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Grasp sockets (lane socket): where and how a hand takes hold of an object, so the agent names the part and the
|
|
2
|
+
// grasp and the compiler turns the hand. The grasp is the object's (sessions 02263581, 38322b5d: the agent computed
|
|
3
|
+
// the wrist by hand and held a barbell 90 deg off, the bar through the palm; a rifle's second hand was measured off
|
|
4
|
+
// the first). A socket is, in its record's own axes (metres from the base pivot, as an object anchor's `local`):
|
|
5
|
+
// at the point the fingers close on
|
|
6
|
+
// axis the line that runs across the palm, the knuckle line along it (a bar, a handle, a grip); none: any
|
|
7
|
+
// from the side the hand comes from: the palm faces against it; none: the roll the arm holds most easily
|
|
8
|
+
// (compiler/socket-grip.js), or on a body with no axis the face nearest the point
|
|
9
|
+
// thumb the end of the axis the thumb takes; none: the end-state-comfort end (socketPalm)
|
|
10
|
+
// A record stores named sockets in `grasp` (src/object-grasp.js); every thin body (two half sizes under THIN m: a
|
|
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
|
+
// `grasp` (power, pinch, hook, palm) takes the hand by a socket of its object: `socket` names it, else the one
|
|
13
|
+
// nearest the anchor's `local` point or, without one, nearest the shoulder of the hand, picked once per compile.
|
|
14
|
+
import { add, sub, scale, norm, cross, dot, len, apply, ry, T } from "./compiler/math.js";
|
|
15
|
+
import { partsOf } from "./object-pose.js";
|
|
16
|
+
|
|
17
|
+
const THIN = 0.045, AXES = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
|
18
|
+
const flat = (v, n) => sub(v, scale(n, dot(v, n)));
|
|
19
|
+
|
|
20
|
+
/** The long axis index of a thin body's half sizes, else -1. */
|
|
21
|
+
function thinAxis(half) {
|
|
22
|
+
if (!half) return -1;
|
|
23
|
+
const long = [0, 1, 2].reduce((a, i) => (half[i] > half[a] ? i : a), 0);
|
|
24
|
+
return [0, 1, 2].filter(i => i !== long).every(i => half[i] < THIN) ? long : -1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The record's stored sockets: a body's in scene.objects, a group's (an Empty) in scene.others. */
|
|
28
|
+
const stored = (scene, id) => scene?.objects?.[id]?.grasp ?? scene?.others?.[id]?.grasp ?? null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every socket of body or group `id`: [{ name, owner, at, axis?, from?, thumb? }], each in its owner's axes. The record's
|
|
32
|
+
* own sockets by name; a part's as "<part>.<name>"; a thin part's derived one by the part id.
|
|
33
|
+
*/
|
|
34
|
+
export function socketsOf(scene, id) {
|
|
35
|
+
const out = [], own = (owner, prefix) => Object.entries(stored(scene, owner) ?? {}).forEach(([name, s]) => out.push({ ...s, name: prefix + name, owner }));
|
|
36
|
+
own(id, "");
|
|
37
|
+
for (const part of partsOf(scene, id)) {
|
|
38
|
+
if (part !== id) own(part, `${part}.`);
|
|
39
|
+
const half = scene.objects[part].half, long = thinAxis(half);
|
|
40
|
+
if (long >= 0) out.push({ name: part, owner: part, at: [0, half[1], 0], axis: AXES[long] });
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The socket of `id` named `name`, or undefined. */
|
|
46
|
+
export const socketNamed = (scene, id, name) => socketsOf(scene, id).find(s => s.name === name);
|
|
47
|
+
|
|
48
|
+
/** A socket in world space on its owner's pose { pos, R }: { p, axis?, from?, thumb? }. */
|
|
49
|
+
export function socketWorld(socket, pose) {
|
|
50
|
+
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) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The outward normal of the face of the box (of boxes [{ pos, R, half }], base pivots) nearest world point p, or undefined. */
|
|
55
|
+
export function faceFrom(boxes, p) {
|
|
56
|
+
let best;
|
|
57
|
+
for (const b of boxes) {
|
|
58
|
+
const q = apply(T(b.R), sub(p, add(b.pos, apply(b.R, [0, b.half[1], 0])))), out = [0, 1, 2].map(i => Math.abs(q[i]) - b.half[i]);
|
|
59
|
+
const i = [0, 1, 2].reduce((m, j) => (out[j] > out[m] ? j : m), 0), gap = Math.max(...out);
|
|
60
|
+
if (!best || gap < best.gap) best = { gap, n: scale([b.R[0][i], b.R[1][i], b.R[2][i]], q[i] < 0 ? -1 : 1) };
|
|
61
|
+
}
|
|
62
|
+
return best?.n;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How far world point q is from a world socket: from its axis line through p, else from p. */
|
|
66
|
+
export function socketDistance(w, q) {
|
|
67
|
+
const v = sub(q, w.p);
|
|
68
|
+
return len(w.axis ? flat(v, w.axis) : v);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The palm frame (columns d, l, n of compiler/limbs.js palmAxes: d to the middle finger, l to the thumb side, n = d x
|
|
73
|
+
* l; the palm is -n on the right hand, +n on the left) a grasp of `type` takes on world socket w, the forearm along fwd,
|
|
74
|
+
* the body turned yawDeg. Along an axis (not a flat palm): with `from` the palm faces against it and the knuckle line
|
|
75
|
+
* runs along the axis; without, the fingers stay on the forearm's line across the axis (a power grip turns the forearm,
|
|
76
|
+
* not the wrist) and the palm closes on it. The thumb takes `thumb`'s end, else the end-state-comfort end (people take
|
|
77
|
+
* the grip that ends comfortable, Rosenbaum): up when the forearm is level, toward the midline when it points down or
|
|
78
|
+
* up. Without an axis the palm faces against `from` (anchorSocket gives a box face's), the fingers on the forearm's line.
|
|
79
|
+
*/
|
|
80
|
+
export function socketPalm(side, w, type, fwd, yawDeg = 0) {
|
|
81
|
+
const right = side === "Right", sign = v => (right ? v : scale(v, -1));
|
|
82
|
+
// a side along the axis leaves no knuckle line across it: the palm then just faces against the side (below)
|
|
83
|
+
if (w.axis && type !== "palm" && !(w.from && len(flat(w.axis, w.from)) < 1e-3)) {
|
|
84
|
+
if (w.from) {
|
|
85
|
+
const n = sign(w.from), l0 = norm(flat(w.axis, n)), pick = l => (w.thumb ? dot(l, w.thumb) : dot(cross(l, n), fwd));
|
|
86
|
+
const l = pick(l0) >= pick(scale(l0, -1)) ? l0 : scale(l0, -1);
|
|
87
|
+
return cols(cross(l, n), l, n);
|
|
88
|
+
}
|
|
89
|
+
const d = across(fwd, w.axis), medial = apply(ry(yawDeg), [right ? 1 : -1, 0, 0]), vy = Math.abs(d[1]);
|
|
90
|
+
const want = w.thumb ?? flat(add(scale([0, 1, 0], 1 - vy), scale(medial, vy)), d), l0 = norm(flat(w.axis, d));
|
|
91
|
+
const l = dot(l0, want) >= 0 ? l0 : scale(l0, -1);
|
|
92
|
+
return cols(d, l, cross(d, l));
|
|
93
|
+
}
|
|
94
|
+
const n = sign(w.from ?? scale(fwd, -1)), d = across(fwd, n);
|
|
95
|
+
return cols(d, cross(n, d), n);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** fwd made perpendicular to v (unit); any perpendicular when fwd runs along v. */
|
|
99
|
+
function across(fwd, v) {
|
|
100
|
+
const d = flat(fwd, v);
|
|
101
|
+
return norm(len(d) > 1e-3 ? d : flat(Math.abs(v[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0], v));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cols = (a, b, c) => [[a[0], b[0], c[0]], [a[1], b[1], c[1]], [a[2], b[2], c[2]]];
|
package/src/motion3/take-edit.js
CHANGED
|
@@ -148,7 +148,7 @@ export function renderTakeEdit(take) {
|
|
|
148
148
|
if (track.pos) { next.pos = new Float32Array(D * 3); at.forEach(({ i0, i1, t }, f) => lerp(next.pos, f * 3, track.pos, i0 * 3, track.pos, i1 * 3, t, 3)); }
|
|
149
149
|
if (track.quat) { next.quat = new Float32Array(D * 4); at.forEach(({ i0, i1, t }, f) => nlerp(next.quat, f * 4, track.quat, i0 * 4, track.quat, i1 * 4, t)); }
|
|
150
150
|
if (track.lvl) { next.lvl = new Float32Array(D); at.forEach(({ i0, i1, t }, f) => lerp(next.lvl, f, track.lvl, i0, track.lvl, i1, t, 1)); }
|
|
151
|
-
for (const key of ["vis", "hold"]) if (track[key]) { next[key] = new Uint8Array(D); at.forEach(({ near }, f) => { next[key][f] = track[key][near]; }); }
|
|
151
|
+
for (const key of ["vis", "hold", "world"]) if (track[key]) { next[key] = new Uint8Array(D); at.forEach(({ near }, f) => { next[key][f] = track[key][near]; }); }
|
|
152
152
|
return [id, next];
|
|
153
153
|
}));
|
|
154
154
|
return { ...take, frameCount: D, frames: D, rot, hips, objects, edit, editRendered: true };
|
package/src/motion3/take.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// world space: one track per spec.objects key (a group: its root only). A switch entry (C15)
|
|
7
7
|
// bakes only vis and lvl, no pos/quat: its record keeps its own place (object-travel.js). A follow
|
|
8
8
|
// entry (C16) bakes pos/quat with hold on every frame: its track is world space throughout. A free entry
|
|
9
|
-
// (C17) bakes pos/quat (and vis/lvl when it names them), world space, no hold.
|
|
9
|
+
// (C17) bakes pos/quat (and vis/lvl when it names them), world space, no hold. A carry that rides a moving body after
|
|
10
|
+
// its release (object-pose.js riderOf) marks those frames `world`: world space too, though no hand holds it.
|
|
10
11
|
import { toLocal } from "./compiler/skeleton.js";
|
|
11
12
|
import { sub, apply, ry, quatFromMat, mul } from "./compiler/math.js";
|
|
12
13
|
import { isCarried, isFollow, isSwitch } from "./carried.js";
|
|
13
14
|
import { isFree } from "./free-motion.js";
|
|
14
|
-
import { rootPoseAt, spanOf } from "./object-pose.js";
|
|
15
|
+
import { rootPoseAt, spanOf, riderOf } from "./object-pose.js";
|
|
15
16
|
|
|
16
17
|
const TAKE_SCHEMA = "cozyclay.take.v3";
|
|
17
18
|
|
|
@@ -36,7 +37,7 @@ export function bakeTake(sk, out, placement = { position: [0, 0, 0], yaw: 0 }, m
|
|
|
36
37
|
const pos = moves ? new Float32Array(F * 3) : null, quat = moves ? new Float32Array(F * 4) : null;
|
|
37
38
|
const named = rides || !moves || isFree(spec), vis = named && spec.visible ? new Uint8Array(F) : null, hold = rides ? new Uint8Array(F) : null;
|
|
38
39
|
const lvl = named && spec.level ? new Float32Array(F) : null;
|
|
39
|
-
const [grab, release] = carried ? spanOf(out, spec) : [0, 0];
|
|
40
|
+
const [grab, release] = carried ? spanOf(out, spec) : [0, 0], world = carried && riderOf(out, id) ? new Uint8Array(F) : null;
|
|
40
41
|
for (let f = 0; f < F; f++) {
|
|
41
42
|
if (vis) vis[f] = (out.baked?.[spec.visible]?.[f + f0] ?? 0) > 0.5 ? 1 : 0;
|
|
42
43
|
if (lvl) lvl[f] = Math.max(0, Math.min(4, out.baked?.[spec.level]?.[f + f0] ?? 1));
|
|
@@ -45,8 +46,9 @@ export function bakeTake(sk, out, placement = { position: [0, 0, 0], yaw: 0 }, m
|
|
|
45
46
|
if (!o) continue;
|
|
46
47
|
pos.set(o.pos, f * 3); quat.set(quatFromMat(o.R), f * 4);
|
|
47
48
|
if (hold) hold[f] = follows || (f + f0 >= grab && f + f0 <= release) ? 1 : 0;
|
|
49
|
+
if (world) world[f] = f + f0 > release ? 1 : 0;
|
|
48
50
|
}
|
|
49
|
-
objects[id] = { ...(moves ? { pos, quat } : {}), ...(vis ? { vis } : {}), ...(hold ? { hold } : {}), ...(lvl ? { lvl } : {}) };
|
|
51
|
+
objects[id] = { ...(moves ? { pos, quat } : {}), ...(vis ? { vis } : {}), ...(hold ? { hold } : {}), ...(world ? { world } : {}), ...(lvl ? { lvl } : {}) };
|
|
50
52
|
}
|
|
51
53
|
return { schema: TAKE_SCHEMA, fps: out.fps, frameCount: F, rig: sk.model, bones: sk.names.slice(), rot, hips, objects, placement, ...meta };
|
|
52
54
|
}
|
package/src/object-catalog.jsx
CHANGED
|
@@ -7,6 +7,7 @@ const GROUP_LABELS_KO = {
|
|
|
7
7
|
"Set pieces": "세트 소품",
|
|
8
8
|
Lights: "조명",
|
|
9
9
|
Effects: "효과",
|
|
10
|
+
Text: "텍스트",
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
const OBJECT_LABELS_KO = {
|
|
@@ -24,6 +25,7 @@ const OBJECT_LABELS_KO = {
|
|
|
24
25
|
"Spot light": "스포트 조명",
|
|
25
26
|
Light: "조명",
|
|
26
27
|
Effect: "효과",
|
|
28
|
+
Text: "텍스트",
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function displayObjectGroupName(name) {
|
|
@@ -73,7 +75,7 @@ export function CatalogueEntries({ onPick }) {
|
|
|
73
75
|
<span className={`add-object-swatch ${entry.kind}`} style={{ background: entry.color }} aria-hidden="true" />
|
|
74
76
|
<span>{displayObjectLabel(entry.label)}</span>
|
|
75
77
|
{/* An empty has no size to quote: say what it is instead of "0 × 0 m". */}
|
|
76
|
-
<small>{entry.light ? ko("practical light", "실제 조명") : entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : entry.kind === "effect" ? ko("smoke, fire, rain…", "연기, 불, 비…") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
|
|
78
|
+
<small>{entry.light ? ko("practical light", "실제 조명") : entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : entry.kind === "effect" ? ko("smoke, fire, rain…", "연기, 불, 비…") : entry.kind === "text" ? ko("words, signs, logos", "글자, 간판, 로고") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
|
|
77
79
|
</button>
|
|
78
80
|
))}
|
|
79
81
|
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grasp sockets on a record (`grasp`): where and how a hand takes hold of it, in the record's own axes, so they move
|
|
3
|
+
* with it and save with it. { <name>: { at: [x, y, z] m from the base pivot, axis?: the line across the palm (a bar,
|
|
4
|
+
* a handle), from?: the side the hand comes from (the palm faces against it), thumb?: the axis end the thumb takes } }.
|
|
5
|
+
* A motion spec's object anchor with `grasp` takes a hand by one (src/motion3/sockets.js), and a thin body is a socket
|
|
6
|
+
* of its own without one. At most MAX sockets, names of letters, digits, _ and -.
|
|
7
|
+
*/
|
|
8
|
+
const MAX = 8, NAME = /^[A-Za-z0-9_-]{1,32}$/, REACH = 10;
|
|
9
|
+
|
|
10
|
+
const vec = (v, limit) => (Array.isArray(v) && v.length === 3 && v.every(Number.isFinite) && v.every(x => Math.abs(x) <= limit) ? v.map(x => Math.round(x * 1e4) / 1e4) : null);
|
|
11
|
+
const dir = v => { const u = vec(v, 1e3), n = u && Math.hypot(...u); return n > 1e-6 ? u.map(x => Math.round((x / n) * 1e4) / 1e4) : null; };
|
|
12
|
+
|
|
13
|
+
/** The repaired sockets of a record, or null for none (invalid entries are dropped). */
|
|
14
|
+
function normalizeObjectGrasp(value) {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [name, s] of Object.entries(value)) {
|
|
18
|
+
if (Object.keys(out).length >= MAX || !NAME.test(name) || !s || typeof s !== "object") continue;
|
|
19
|
+
const at = vec(s.at, REACH);
|
|
20
|
+
if (!at) continue;
|
|
21
|
+
out[name] = { at };
|
|
22
|
+
for (const k of ["axis", "from", "thumb"]) { const d = dir(s[k]); if (d) out[name][k] = d; }
|
|
23
|
+
}
|
|
24
|
+
return Object.keys(out).length ? out : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The grasp field of a stored record (normalizeSceneObject): kept when it has a socket. */
|
|
28
|
+
export function objectGraspFields(record) {
|
|
29
|
+
const grasp = normalizeObjectGrasp(record?.grasp);
|
|
30
|
+
return grasp ? { grasp } : {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The grasp part of an object.update patch: the sockets replace the record's own; `grasp: null` removes them. Only a change is returned. */
|
|
34
|
+
export function objectGraspPatch(object, patch) {
|
|
35
|
+
if (patch?.grasp === undefined) return {};
|
|
36
|
+
const next = patch.grasp === null ? null : normalizeObjectGrasp(patch.grasp);
|
|
37
|
+
return JSON.stringify(next) === JSON.stringify(object.grasp ?? null) ? {} : { grasp: next };
|
|
38
|
+
}
|
package/src/object-look.js
CHANGED
|
@@ -194,3 +194,5 @@ export function withShadowCapWarnings(result, objects) {
|
|
|
194
194
|
const warnings = shadowCapWarnings(objects, result?.affectedIds ?? []);
|
|
195
195
|
return warnings.length ? { ...result, warnings: [...(result.warnings ?? []), ...warnings] } : result;
|
|
196
196
|
}
|
|
197
|
+
// A record's grasp sockets ride the same normalize and patch paths (object-grasp.js).
|
|
198
|
+
export { objectGraspFields, objectGraspPatch } from "./object-grasp.js";
|
package/src/object-travel.js
CHANGED
|
@@ -115,12 +115,15 @@ export function trackLevel(id, frame, take) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* True while a compiled take holds the record in a hand (a grabbed cup)
|
|
119
|
-
*
|
|
118
|
+
* True while a compiled take holds the record in a hand (a grabbed cup), or the record rides a
|
|
119
|
+
* moving body after its release (`world`, take.js): its track is in world space then, so the
|
|
120
|
+
* carry from its own parents (the table it stood on) is cut, like `attach`.
|
|
120
121
|
*/
|
|
121
122
|
function held(id, frame, take) {
|
|
122
123
|
const track = take?.objectTracks?.get?.(id);
|
|
123
|
-
|
|
124
|
+
if (!track) return false;
|
|
125
|
+
const at = trackFrame(track, frame);
|
|
126
|
+
return track.hold?.[at] === 1 || track.world?.[at] === 1;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/** The record's own route sample (or compiled track) as a matrix, or null when it does not travel. */
|