cozyclay 3.0.2 → 3.1.0
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 +95 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +22 -56
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
- package/dist/assets/app-B2BpfCl2.js +5088 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-B887n77J.js +1 -0
- package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
- package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
- package/dist/assets/task-board-BKaM7d-9.js +14 -0
- package/dist/assets/workflow-B8Dhzexj.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/package.json +3 -3
- package/src/App.jsx +50 -281
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-task-scopes.js +69 -0
- package/src/bus-task-targets.js +48 -0
- package/src/bus-tasks.js +181 -0
- package/src/command-bus.js +15 -40
- 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-compile.js +51 -27
- package/src/commands/motion3.js +13 -7
- package/src/commands/project.js +1 -1
- package/src/commands/shared.js +9 -0
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/anchors.js +33 -5
- package/src/motion3/compiler/compile.js +27 -4
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/grip.js +9 -3
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/socket-grip.js +34 -10
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/fix/index.js +7 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +5 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +21 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +11 -10
- package/src/motion3/schema/types.js +5 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/basic.js +61 -13
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +13 -4
- package/src/motion3/skills/info.js +14 -1
- package/src/motion3/skills/reach-human.js +139 -0
- package/src/motion3/skills/stance.js +92 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/motion3/sockets.js +21 -4
- package/src/motion3/views.js +28 -5
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +4 -7
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +3 -7
- package/src/scenes.js +4 -5
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +1 -1
- package/src/studio-error-codes.js +1 -1
- package/src/studio-motion-visual.js +4 -4
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +75 -42
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +11 -25
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +7 -14
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-DbKG0NgD.js +0 -6
- package/dist/assets/app-34i-iUy1.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DYpa2nvO.js +0 -5
- package/dist/assets/first-shot-handoff-CrGZ3n_O.js +0 -1
- package/dist/assets/landing-mq5bnqFE.js +0 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-dUhurYUc.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
package/src/motion3/carried.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { palmAxes } from "./compiler/limbs.js";
|
|
9
9
|
import { SIDES } from "./compiler/pose.js";
|
|
10
10
|
import { add, scale, norm, sub, dot, cross, apply, mul, T, axisRot, I3 } from "./compiler/math.js";
|
|
11
|
+
import { bonePoint } from "./compiler/skeleton.js";
|
|
11
12
|
|
|
12
13
|
export const isCarried = o => typeof o?.prop === "string";
|
|
13
14
|
/** A follow entry { follow: <ref>, offset?, aim?, visible?, level? } (C16, follow.js): the record rides a point of the body. */
|
|
@@ -23,9 +24,16 @@ export function propOrigin(sk, spec, s, o) {
|
|
|
23
24
|
return scale(prop.origin.map(b => s.P[sk.index[b]]).reduce((a, p) => add(a, p), [0, 0, 0]), 1 / prop.origin.length);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
const
|
|
27
|
+
/** The prop origin carried by its hand's bone on pose s, where the bind pose has it against the hand: closing fingers do not move it (#816). */
|
|
28
|
+
function handOrigin(sk, prop, s) {
|
|
29
|
+
const h = sk.index[`${SIDES[prop.hand]}Hand`], P = sk.restWorldPos;
|
|
30
|
+
const mean = scale(prop.origin.map(b => P[sk.index[b]]).reduce((a, p) => add(a, p), [0, 0, 0]), 1 / prop.origin.length);
|
|
31
|
+
return bonePoint(s, h, apply(T(sk.restWorldRot[h]), sub(mean, P[h])));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** World { pos, R } of a carried record on pose s (at/along offset, PR #14); `fixed`: the origin rides the hand bone (seat.js). */
|
|
35
|
+
export function carriedPose(sk, spec, s, o, fixed = false) {
|
|
36
|
+
const ax = palmAxes(sk, s, SIDES[spec.props[o.prop].hand]), origin = fixed ? handOrigin(sk, spec.props[o.prop], s) : propOrigin(sk, spec, s, o);
|
|
29
37
|
const palm = v => add(scale(ax.d, v[0]), add(scale(ax.l, v[1]), scale(ax.n, v[2])));
|
|
30
38
|
const y = norm(palm(o.along ?? [0, 0, 1]));
|
|
31
39
|
let x = sub(ax.d, scale(y, dot(ax.d, y)));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// One motion compile as a job of plain data, the same in the compile worker (compile-worker.js) and on
|
|
2
2
|
// the calling thread (commands/motion3-compile.js, where no worker runs: Node, tests): the rig by model
|
|
3
|
-
// name, the motion scene (scene.js motionScene of the editor's objects: boxes
|
|
3
|
+
// name, the motion scene (scene.js motionScene of the editor's objects: oriented boxes; shapes.js reads a sphere or a cylinder round from them), the spec as sent, and
|
|
4
4
|
// what to do with it (probes, checks, lenient, repair, bake). The answer is plain data and typed arrays
|
|
5
5
|
// only (the compiler's out holds closures), so it crosses a worker boundary: QA, observations, the stored
|
|
6
6
|
// spec, and the baked take when asked.
|
|
@@ -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";
|
|
@@ -34,8 +35,12 @@ import { fitWeight } from "../fix/self.js";
|
|
|
34
35
|
import { holdHook } from "../object-keys.js";
|
|
35
36
|
import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
|
|
36
37
|
import { objectPoseAt, handOf, restingAt, frameAt, spanOf } from "../object-pose.js";
|
|
38
|
+
import { heldFor } from "./finger-close.js";
|
|
39
|
+
import { sagOf } from "../load.js";
|
|
37
40
|
|
|
38
41
|
const smooth = x => x * x * (3 - 2 * x);
|
|
42
|
+
// how far past the straight arm a grasp fit's wrist target may end before it counts as wound up (solveFrame), m
|
|
43
|
+
const REACH_SLACK = 0.02;
|
|
39
44
|
|
|
40
45
|
/**
|
|
41
46
|
* Scene boxes by frame. A body moves with its own spec.objects entry or its nearest group's (a
|
|
@@ -133,6 +138,10 @@ function hooksFor(ctx, c, f, inFit = false, lead = true, fitSide = null) {
|
|
|
133
138
|
afterArm: (k, s, diag) => measureProps(ctx, k, s, diag),
|
|
134
139
|
// a fit's passes only place the wrist; the final pose (and a plain one) turns the hand onto the body
|
|
135
140
|
grip: inFit ? undefined : (k, s, diag) => gripHand(ctx, sk, s, SIDES[k], k, c, f, diag),
|
|
141
|
+
// a carry's weight lowers a plain IK hand's target, on the final pose only (load.js)
|
|
142
|
+
sag: inFit ? undefined : (k, s, target) => sagOf(ctx, k, f, s, target, (1 - fitWeight(ctx.spec, c, k)) * Math.min(1, Math.max(0, c[`armw_${k}`] ?? 0))),
|
|
143
|
+
// fingers close on what the hand holds, on the final pose only (finger-close.js)
|
|
144
|
+
heldShapes: inFit || !ctx.grasp ? undefined : (k, s) => heldFor(ctx, c, f, k, s),
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -146,15 +155,25 @@ export function solveFrame(ctx, c0, f, lead = true) {
|
|
|
146
155
|
if (!(c[wch] > 1e-4)) continue;
|
|
147
156
|
const full = { ...c, [wch]: 1, [`armw_${fit.side}`]: 1 };
|
|
148
157
|
for (const ch of Object.keys(full)) if (ch !== wch && new RegExp(`^w_.+_${fit.side}$`).test(ch)) full[ch] = 0;
|
|
149
|
-
|
|
158
|
+
// each pass adds what the effector still misses; an arm at full reach misses the same each pass, so on a grasp's fit
|
|
159
|
+
// 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
|
|
160
|
+
// few cm out of reach). A grasp fit whose wrist target ends past the straight arm (REACH_SLACK) aims instead at the
|
|
161
|
+
// anchor by the hand's own wrist-to-effector offset of its best pass: the arm reaches straight for it and its
|
|
162
|
+
// residual (REACH) is what the effector misses. Other fits keep their passes as they were (their takes are pinned).
|
|
163
|
+
let corr = [0, 0, 0], best = null, last = null;
|
|
164
|
+
const grasp = graspAnchor(ctx.spec, fit.anchor), shoulder = ctx.sk.index[`${SIDES[fit.side]}Arm`], wristAt = ctx.sk.index[`${SIDES[fit.side]}Hand`];
|
|
150
165
|
for (let i = 0; i < 6; i++) {
|
|
151
166
|
full[`fit_${fit.anchor}_${fit.side}`] = corr;
|
|
152
167
|
const { s, diag } = pose(ctx.sk, full, hooksFor(ctx, full, f, true, lead, fit.side));
|
|
153
168
|
const eff = effectorPoint(ctx.sk, s, diag, fit.effector);
|
|
154
169
|
const goal = add(resolveAnchor(ctx, fit.anchor, s, diag, full, fit.side), fit.offset ?? [0, 0, 0]);
|
|
155
170
|
if (!eff) break;
|
|
156
|
-
|
|
171
|
+
const miss = sub(goal, eff), err = len(miss);
|
|
172
|
+
if (grasp && (!best || err < best.err)) best = { err, hand: sub(s.P[wristAt], eff) };
|
|
173
|
+
last = { goal, shoulder: s.P[shoulder] };
|
|
174
|
+
corr = add(corr, miss);
|
|
157
175
|
}
|
|
176
|
+
if (grasp && best && last && len(sub(add(last.goal, corr), last.shoulder)) > armReach(ctx.sk, fit.side) + REACH_SLACK) corr = best.hand;
|
|
158
177
|
c[`fit_${fit.anchor}_${fit.side}`] = corr;
|
|
159
178
|
}
|
|
160
179
|
const { s, diag } = pose(ctx.sk, c, hooksFor(ctx, c, f, false, lead));
|
|
@@ -173,7 +192,7 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
|
|
|
173
192
|
if (!(frameCount >= 1 && frameCount <= 20000)) throw new Error("MOTION_SPEC_INVALID: frames must be 1..20000");
|
|
174
193
|
const fix = fixOptions(spec.fix);
|
|
175
194
|
const [a, b] = opts.window ? [Math.max(0, opts.window[0]), Math.min(frameCount - 1, opts.window[1])] : [0, frameCount - 1];
|
|
176
|
-
const ctx = { sk, spec, scene, softReach: fix.reach, push: { l: new Map(), r: new Map() }, swivel: { l: new Map(), r: new Map() }, skinPush: { l: new Map(), r: new Map() }, keepOut: fix.self ? wristBody(sk) : null };
|
|
195
|
+
const ctx = { sk, spec, scene, grasp: fix.grasp, softReach: fix.reach, push: { l: new Map(), r: new Map() }, swivel: { l: new Map(), r: new Map() }, skinPush: { l: new Map(), r: new Map() }, keepOut: fix.self ? wristBody(sk) : null };
|
|
177
196
|
const out = { ctx, fps: spec.fps ?? 30, frameCount, f0: a, frames: [] };
|
|
178
197
|
// object anchors read the pose refs read (object-pose.js), on the frame being solved
|
|
179
198
|
ctx.objectPose = (id, f, now) => objectPoseAt(out, id, f, now);
|
|
@@ -183,11 +202,15 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
|
|
|
183
202
|
// a prop anchor outside its hand's carry reads the carry's grab (release) frame (anchors.js propOnCarry)
|
|
184
203
|
ctx.carrySpan = o => spanOf(out, o);
|
|
185
204
|
ctx.solvedAt = f => frameAt(out, f);
|
|
205
|
+
// a reach's grasp is planned on its first full-weight frame (socket-grip.js plannedGrasp)
|
|
206
|
+
ctx.weightAt = (ch, f) => out.baked?.[ch]?.[f];
|
|
207
|
+
ctx.frameCount = frameCount;
|
|
186
208
|
const engine = {
|
|
187
209
|
rebake(keys) {
|
|
188
210
|
out.keys = keys;
|
|
189
211
|
out.baked = bakeChannels(keys, frameCount, flowOptions(spec.flow));
|
|
190
212
|
ctx.socketTurns = new Map();
|
|
213
|
+
ctx.graspPlans = new Map();
|
|
191
214
|
steadyHead(out.baked, keys, frameCount);
|
|
192
215
|
// a held pose breathes and shifts its weight (idle.js): offsets solveFrame adds, contacts and out.baked stay as keyed
|
|
193
216
|
ctx.idle = idleLayer(out.baked, { spec, sk, frameCount });
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Fingers close on what the hand holds (#816). A preset curls each phalanx a fixed angle, which fits one size: on a
|
|
2
|
+
// ball the index and middle sank 3 cm into it while the little finger stayed in the air (session 7f15b4ae). After the
|
|
3
|
+
// arm, the hold and the elbow's swivel have placed the hand (pose.js), each finger closes phalanx by phalanx, from the
|
|
4
|
+
// knuckle out: the joint turns further about the axis it curls on until the rest of the finger first touches a held
|
|
5
|
+
// shape (shapes.js; its skin within TOUCH m of the surface), never past the fist's curl; a finger that cannot reach it
|
|
6
|
+
// keeps the preset's curl (closing on air would curl it into whatever lies under the palm). The thumb keeps the preset's
|
|
7
|
+
// turn at its base and flexes its outer joints the same way. Found by bisection (STEPS evaluations a joint) on the joints' positions,
|
|
8
|
+
// then turned once; weighted by how far the hand holds (a fit's weight), so the fingers close over the reach and open
|
|
9
|
+
// on the release. A joint the preset curled into the body straightens when that leaves the finger shallower; a
|
|
10
|
+
// joint whose sub-chain already touches stays, and the next joint out closes on: the finger wraps the body.
|
|
11
|
+
import { add, sub, scale, norm, cross, apply, axisRot } from "./math.js";
|
|
12
|
+
import { rotateSubtree } from "./skeleton.js";
|
|
13
|
+
import { palmAxes, fingerCurl, fistCurl } from "./limbs.js";
|
|
14
|
+
import { capsuleDepth } from "../shapes.js";
|
|
15
|
+
import { partsOf } from "../object-pose.js";
|
|
16
|
+
import { seatsInHand } from "../seat.js";
|
|
17
|
+
|
|
18
|
+
const PHALANX = 0.009, TOUCH = 0.002, STEPS = 8, THUMB_FLEX = 70;
|
|
19
|
+
const FINGERS = ["Index", "Middle", "Ring", "Pinky"];
|
|
20
|
+
// the hand stays open for most of a reach and closes as it arrives: from OPEN_UNTIL of the fit's weight to CLOSED_AT
|
|
21
|
+
const OPEN_UNTIL = 0.6, CLOSED_AT = 0.9;
|
|
22
|
+
const closing = w => { const t = Math.min(1, Math.max(0, (w - OPEN_UNTIL) / (CLOSED_AT - OPEN_UNTIL))); return t * t * (3 - 2 * t); };
|
|
23
|
+
|
|
24
|
+
function chainDepth(points, entries) {
|
|
25
|
+
let depth = -Infinity;
|
|
26
|
+
for (let i = 0; i + 1 < points.length; i++) for (const e of entries) depth = Math.max(depth, capsuleDepth(e, points[i], points[i + 1], PHALANX));
|
|
27
|
+
return depth;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The turn (deg, 0..limit) about unit `axis` at `pivot` at which the chain first touches: 0 when it already does, null when it never does. */
|
|
31
|
+
function touchTurn(points, pivot, axis, limit, entries) {
|
|
32
|
+
if (limit <= 0 || chainDepth(points, entries) >= -TOUCH) return 0;
|
|
33
|
+
const at = deg => { const R = axisRot(axis, deg); return points.map(p => add(pivot, apply(R, sub(p, pivot)))); };
|
|
34
|
+
if (chainDepth(at(limit), entries) < -TOUCH) return null;
|
|
35
|
+
let lo = 0, hi = limit;
|
|
36
|
+
for (let i = 0; i < STEPS; i++) { const mid = (lo + hi) / 2; if (chainDepth(at(mid), entries) >= -TOUCH) hi = mid; else lo = mid; }
|
|
37
|
+
return hi;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The turn (deg, 0..limit) back about `axis` that takes a chain inside the body just out of it (0 when it is out, or when
|
|
41
|
+
* no opening leaves it shallower). */
|
|
42
|
+
function clearTurn(points, pivot, axis, limit, entries) {
|
|
43
|
+
if (chainDepth(points, entries) < -TOUCH) return 0;
|
|
44
|
+
const at = deg => { const R = axisRot(axis, -deg); return points.map(p => add(pivot, apply(R, sub(p, pivot)))); };
|
|
45
|
+
// a chain that stays in the body however far it opens opens only if that leaves it shallower
|
|
46
|
+
if (chainDepth(at(limit), entries) >= -TOUCH) return chainDepth(at(limit), entries) < chainDepth(points, entries) ? limit : 0;
|
|
47
|
+
let lo = 0, hi = limit;
|
|
48
|
+
for (let i = 0; i < STEPS; i++) { const mid = (lo + hi) / 2; if (chainDepth(at(mid), entries) < -TOUCH) hi = mid; else lo = mid; }
|
|
49
|
+
return hi;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Turns joint j by the touching turn about `axis` (or `limit` when it never touches, unless `air` is false), weighted; true when it touched. */
|
|
53
|
+
function closeJoint(sk, s, j, chain, axis, limit, entries, weight, air = true) {
|
|
54
|
+
const turn = touchTurn(chain.map(i => s.P[i]), s.P[j], axis, limit, entries), deg = turn ?? (air ? limit : 0);
|
|
55
|
+
if (deg > 0) rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(axis, deg * weight));
|
|
56
|
+
return turn !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* closeOnShape(sk, s, side, { entries, weight, preset }): the hand on pose s ("Left" | "Right") closes on the shape
|
|
61
|
+
* entries (motion-scene entries at this frame) by `weight` 0..1; `preset` is the hand's finger weights (its fingers_<k>
|
|
62
|
+
* channel), from which each joint may close as far as the fist's curl. Answers { touched: the fingers that reached it }.
|
|
63
|
+
*/
|
|
64
|
+
export function closeOnShape(sk, s, side, { entries: held, stops = [], weight, preset }) {
|
|
65
|
+
if (!held?.length || !(weight > 0)) return { touched: [] };
|
|
66
|
+
// a finger stops on what it holds or on what that rests against (a table under a ball)
|
|
67
|
+
const entries = [...held, ...stops];
|
|
68
|
+
const sign = side === "Left" ? -1 : 1, J = name => sk.index[`${side}Hand${name}`], touched = [];
|
|
69
|
+
for (const f of FINGERS) {
|
|
70
|
+
const joints = [1, 2, 3, 4].map(i => J(`${f}${i}`));
|
|
71
|
+
if (joints.some(j => j === undefined)) continue;
|
|
72
|
+
let tip = false;
|
|
73
|
+
// a finger the preset curled into the body straightens, joint by joint, when the straight finger is shallower (a
|
|
74
|
+
// finger lying across a box would go deeper); each joint then closes until the finger touches
|
|
75
|
+
const pts = joints.map(i => s.P[i]), l0 = scale(palmAxes(sk, s, side).l, sign);
|
|
76
|
+
const straight = [0, 1, 2].reduce((p, k) => { const R = axisRot(l0, -fingerCurl(preset, f, k)); return p.map((q, i) => (i > k ? add(p[k], apply(R, sub(q, p[k]))) : q)); }, pts);
|
|
77
|
+
const opens = chainDepth(pts, held) >= -TOUCH && chainDepth(straight, held) < chainDepth(pts, held);
|
|
78
|
+
for (let k = 0; k < 3; k++) {
|
|
79
|
+
const axis = scale(palmAxes(sk, s, side).l, sign), curl = fingerCurl(preset, f, k), open = opens ? curl : 0;
|
|
80
|
+
if (open > 0) rotateSubtree(sk, s, joints[k], s.P[joints[k]].slice(), axisRot(axis, -open * weight));
|
|
81
|
+
tip = closeJoint(sk, s, joints[k], joints.slice(k), axis, Math.max(0, fistCurl(f, k) - curl + open), entries, weight, false);
|
|
82
|
+
}
|
|
83
|
+
if (tip) touched.push(f);
|
|
84
|
+
}
|
|
85
|
+
const thumb = [1, 2, 3, 4].map(i => J(`Thumb${i}`));
|
|
86
|
+
if (thumb.every(j => j !== undefined)) {
|
|
87
|
+
// the thumb keeps the preset's turn at its base (wrapThumb); its two outer joints back out of the body when the
|
|
88
|
+
// preset wrapped them into it, then flex until the thumb touches (one that cannot reach it keeps the preset's flex)
|
|
89
|
+
const { n } = palmAxes(sk, s, side);
|
|
90
|
+
let reached = false;
|
|
91
|
+
for (const k of [1, 2]) {
|
|
92
|
+
const axis = scale(norm(cross(norm(sub(s.P[thumb[k + 1]], s.P[thumb[k]])), n)), sign);
|
|
93
|
+
const out = clearTurn(thumb.slice(k).map(i => s.P[i]), s.P[thumb[k]], axis, THUMB_FLEX, entries);
|
|
94
|
+
if (out > 0) rotateSubtree(sk, s, thumb[k], s.P[thumb[k]].slice(), axisRot(axis, -out * weight));
|
|
95
|
+
reached = closeJoint(sk, s, thumb[k], thumb.slice(k), axis, THUMB_FLEX, entries, weight, false);
|
|
96
|
+
}
|
|
97
|
+
if (reached) touched.push("Thumb");
|
|
98
|
+
}
|
|
99
|
+
return { touched };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What hand k closes on at frame f with channels c: the object of its heaviest fit on an object anchor with grasp, while
|
|
104
|
+
* that object is not in its hand yet (a carry's hand holds it by its seat, seat.js), as { entries, weight, preset }; null
|
|
105
|
+
* when it reaches for nothing.
|
|
106
|
+
*/
|
|
107
|
+
export function heldFor(ctx, c, f, k, s) {
|
|
108
|
+
let best = null;
|
|
109
|
+
for (const fit of ctx.spec.fits ?? []) {
|
|
110
|
+
const a = ctx.spec.anchors?.[fit.anchor], w = c[`w_${fit.anchor}_${k}`] ?? 0;
|
|
111
|
+
if (fit.side !== k || a?.kind !== "object" || !a.grasp || !(w > 1e-4) || (best && best.weight >= w) || ctx.objectHand(a.object, f) === k) continue;
|
|
112
|
+
// a disc's rim socket (a steering wheel) is a ring the scene draws as a solid disc: the hand keeps its rim grip
|
|
113
|
+
if (ctx.sockets?.get(fit.anchor)?.rim) continue;
|
|
114
|
+
const at = ctx.objectsAt(f), entries = partsOf(ctx.scene, a.object).map(p => at[p]).filter(Boolean);
|
|
115
|
+
// as far as the skill has the hand closing (reachGrip keys its grip late in the reach: skills/reach-human.js)
|
|
116
|
+
const grip = Math.min(1, Object.entries(c[`fingers_${k}`] ?? {}).reduce((acc, [p, v]) => acc + (p === "relaxed" ? 0 : v || 0), 0));
|
|
117
|
+
if (entries.length) best = { entries, weight: closing(w) * grip, preset: c[`fingers_${k}`] };
|
|
118
|
+
}
|
|
119
|
+
return best ?? carriedFor(ctx, c, f, k, s);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A carry on the grasp path hand k holds at frame f, at its seat on pose s (seat.js), as { entries, weight, preset }. */
|
|
123
|
+
function carriedFor(ctx, c, f, k, s) {
|
|
124
|
+
const { spec, scene } = ctx;
|
|
125
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) {
|
|
126
|
+
if (!seatsInHand(spec, o, scene, id) || spec.props?.[o.prop]?.hand !== k || !s) continue;
|
|
127
|
+
// the hand holds it from the grab on (the object eases into its seat meanwhile, settleAt)
|
|
128
|
+
const [grab, release] = ctx.carrySpan(o), weight = 1;
|
|
129
|
+
// while the keyed offset is being taken (its grab frame solved without the keys) the hand has nothing seated yet
|
|
130
|
+
if (f < grab || f > release || ctx.hold?.attaching(id)) continue;
|
|
131
|
+
// where it is on this pose (object-pose.js: easing into its seat over the settle, as it is drawn), not the full seat
|
|
132
|
+
const entries = partsOf(scene, id).map(p => ({ ...scene.objects[p], ...ctx.objectPose(p, f, { s }) }));
|
|
133
|
+
const near = Object.entries(ctx.objectsAt(f)).filter(([key, e]) => !partsOf(scene, id).includes(key) && e.half && !e.carried && entries.some(h => Math.hypot(...h.pos.map((v, i) => v - e.pos[i])) < 0.25 + Math.hypot(...e.half))).map(([, e]) => e);
|
|
134
|
+
if (entries.length) return { entries, stops: near, weight, preset: c[`fingers_${k}`] };
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -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;
|
|
@@ -158,12 +158,27 @@ const FINGER_PRESETS = {
|
|
|
158
158
|
grip: { Index: [55, 60, 35], Middle: [60, 62, 35], Ring: [62, 62, 35], Pinky: [65, 62, 35], Thumb: [15, 10, 8] },
|
|
159
159
|
cigarette: { Index: [8, 10, 6], Middle: [10, 12, 8], Ring: [60, 75, 45], Pinky: [65, 80, 45], Thumb: [35, 25, 20] },
|
|
160
160
|
point: { Index: [0, 0, 0], Middle: [80, 95, 60], Ring: [85, 95, 60], Pinky: [85, 95, 60], Thumb: [40, 30, 30] },
|
|
161
|
+
spherical: { Index: [35, 40, 25], Middle: [38, 42, 25], Ring: [40, 42, 25], Pinky: [42, 44, 25], Thumb: [15, 10, 8] },
|
|
161
162
|
};
|
|
163
|
+
// A ball's fingers fan out round it (#816): each finger turns about the palm normal at its knuckle, degrees toward the
|
|
164
|
+
// thumb side (+), by preset weight; the index leads the thumb round one side, the little finger the other.
|
|
165
|
+
const SPREAD = { spherical: { Index: 9, Middle: 2, Ring: -5, Pinky: -12 } };
|
|
166
|
+
|
|
167
|
+
/** Degrees phalanx k (0..2) of finger f curls at preset weights `weights` (blended over relaxed). */
|
|
168
|
+
export function fingerCurl(weights, f, k) {
|
|
169
|
+
const base = FINGER_PRESETS.relaxed;
|
|
170
|
+
return base[f][k] + Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * ((FINGER_PRESETS[p]?.[f]?.[k] ?? base[f][k]) - base[f][k]), 0);
|
|
171
|
+
}
|
|
172
|
+
/** The fist's curl of phalanx k of finger f: as far as a finger closes. */
|
|
173
|
+
export const fistCurl = (f, k) => FINGER_PRESETS.fist[f][k];
|
|
162
174
|
|
|
163
175
|
/** Blend presets by weight over `relaxed` and curl every finger toward the palm. */
|
|
164
176
|
export function fingers(sk, s, side, weights) {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
177
|
+
const sign = side === "Left" ? -1 : 1, curl = (f, k) => fingerCurl(weights, f, k);
|
|
178
|
+
for (const f of ["Index", "Middle", "Ring", "Pinky"]) {
|
|
179
|
+
const spread = Object.entries(weights ?? {}).reduce((acc, [p, w]) => acc + (w || 0) * (SPREAD[p]?.[f] ?? 0), 0), j = sk.index[`${side}Hand${f}1`];
|
|
180
|
+
if (spread && j !== undefined) rotateSubtree(sk, s, j, s.P[j].slice(), axisRot(palmAxes(sk, s, side).n, spread));
|
|
181
|
+
}
|
|
167
182
|
for (const f of ["Index", "Middle", "Ring", "Pinky", "Thumb"]) {
|
|
168
183
|
for (let k = 0; k < 3; k++) {
|
|
169
184
|
const j = sk.index[`${side}Hand${f}${k + 1}`];
|
|
@@ -181,7 +196,7 @@ export function fingers(sk, s, side, weights) {
|
|
|
181
196
|
// palm and across it, then flexed at its two joints about their own axes (bone x palm normal). The fist's thumb lies over
|
|
182
197
|
// the index and middle fingers' middle phalanges; the grip's closes around a handle against the index finger. Searched on
|
|
183
198
|
// both rigs within the joints' ranges (thumb tip to the index's middle phalanx: fist 8.4 -> 3.7 cm Y Bot, 6.9 -> 2.4 X Bot).
|
|
184
|
-
const THUMB_WRAP = { fist: [-70, -5, 5, 80], grip: [-30, -30, 30, 10] };
|
|
199
|
+
const THUMB_WRAP = { fist: [-70, -5, 5, 80], grip: [-30, -30, 30, 10], spherical: [-40, -20, 20, 10] };
|
|
185
200
|
function wrapThumb(sk, s, side, [oppose, across, flex2, flex3]) {
|
|
186
201
|
const sign = side === "Left" ? -1 : 1, J = k => sk.index[`${side}HandThumb${k}`];
|
|
187
202
|
if (J(1) === undefined) return;
|
|
@@ -6,6 +6,7 @@ import { add, sub, scale, norm, len, apply, mul, rx, ry, rz } from "./math.js";
|
|
|
6
6
|
import { restState, rotateSubtree } from "./skeleton.js";
|
|
7
7
|
import { twoBone, orientFoot, wrist, fingers, limitFold, askedFold, swivelElbow, ELBOW_FOLD, KNEE_FOLD } from "./limbs.js";
|
|
8
8
|
import { holdHand } from "./hold.js";
|
|
9
|
+
import { closeOnShape } from "./finger-close.js";
|
|
9
10
|
import { clampWrist } from "./wrist-clamp.js";
|
|
10
11
|
import { ELBOW_REST } from "./arm-orient.js";
|
|
11
12
|
import { wristBody, bodyDistance } from "../body.js";
|
|
@@ -147,6 +148,9 @@ export function pose(sk, c, hooks = {}) {
|
|
|
147
148
|
if (hooks.keepOut) target = hooks.keepOut(k, s, target);
|
|
148
149
|
const push = hooks.push?.(k);
|
|
149
150
|
if (push) target = add(target, push);
|
|
151
|
+
// what the hand carries weighs it down (load.js)
|
|
152
|
+
const sag = hooks.sag?.(k, s, target);
|
|
153
|
+
if (sag) target = add(target, sag);
|
|
150
154
|
// the self fix's push rides the IK as far as the arm is on it (an arm coming in from FK would jump)
|
|
151
155
|
const skinPush = hooks.skinPush?.(k);
|
|
152
156
|
if (skinPush) target = add(target, scale(skinPush, turnWeight(w)));
|
|
@@ -171,6 +175,8 @@ export function pose(sk, c, hooks = {}) {
|
|
|
171
175
|
// the turn keeps the hand's world turn; a wrist it takes past its limits stops there and the hand turns with the elbow
|
|
172
176
|
// (wrist-clamp.js), unless keys lead what the hand holds: that hand keeps the object's grip (hold.js limits its wrist)
|
|
173
177
|
if (turn) { swivelElbow(sk, s, side, turn * turnWeight(w)); if (!held) clampWrist(sk, s, side); }
|
|
178
|
+
const grasped = hooks.heldShapes?.(k, s);
|
|
179
|
+
if (grasped) diag[`closed_${k}`] = closeOnShape(sk, s, side, grasped).touched;
|
|
174
180
|
hooks.afterArm?.(k, s, diag);
|
|
175
181
|
}
|
|
176
182
|
return { s, diag };
|
|
@@ -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
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// of the body. A free entry { keys?, fall?, visible?, level? } (C17: no prop, no follow, no hinge field) moves a body or a
|
|
8
8
|
// group on its own: world pose keys over the whole take, and gravity falls; a carry takes fall too, before its grab
|
|
9
9
|
// or from its release on. Fields: schema/fields.js OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS. See validate.js.
|
|
10
|
-
import { isVec3, fmt, didYouMean } from "./vocab.js";
|
|
10
|
+
import { isVec3, fmt, didYouMean, GRASPS } from "./vocab.js";
|
|
11
11
|
import { objectOk } from "./validate-common.js";
|
|
12
12
|
import { OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS } from "../schema/fields.js";
|
|
13
13
|
import { isSwitch, isFollow, movesRecord } from "../carried.js";
|
|
@@ -15,7 +15,7 @@ import { isFree } from "../free-motion.js";
|
|
|
15
15
|
import { checkRef } from "../refs.js";
|
|
16
16
|
import { partsOf } from "../object-pose.js";
|
|
17
17
|
|
|
18
|
-
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce?, tumble? }";
|
|
18
|
+
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce?, tumble?, v? }";
|
|
19
19
|
const isQuat = q => Array.isArray(q) && q.length === 4 && q.every(Number.isFinite) && Math.abs(Math.hypot(...q) - 1) <= 0.01;
|
|
20
20
|
const isDir = v => isVec3(v) && Math.hypot(...v) > 1e-9;
|
|
21
21
|
|
|
@@ -94,10 +94,23 @@ function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
|
94
94
|
}
|
|
95
95
|
if (fl.onto !== undefined) objectOk(err, scene, `${fat}.onto`, fl.onto, { groups: true });
|
|
96
96
|
if (fl.bounce !== undefined && !(Number.isFinite(fl.bounce) && fl.bounce >= 0 && fl.bounce <= 0.6)) err(`${fat}.bounce must be 0..0.6 (the share of its landing speed it bounces back with; 0 lands dead), got ${fmt(fl.bounce)}`);
|
|
97
|
+
if (fl.v !== undefined && !((carry && fl.v === "hand") || isVec3(fl.v))) err(`${fat}.v must be [vx, vy, vz] m/s (the velocity it leaves with)${carry ? " or \"hand\" (the velocity the hand gives it at the release)" : ""}, got ${fmt(fl.v)}`);
|
|
97
98
|
if (fl.tumble !== undefined && typeof fl.tumble !== "boolean") err(`${fat}.tumble must be true (it rolls to rest on a stable face, the default) or false (it drops straight down as it was), got ${fmt(fl.tumble)}`);
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/** A grasp that starts like the given one (4+ letters: "sphere" -> spherical), else the nearest by edit distance. */
|
|
103
|
+
const prefixLen = (a, b) => { let i = 0; while (i < a.length && a[i] === b[i]) i += 1; return i; };
|
|
104
|
+
function graspHint(given) {
|
|
105
|
+
const text = String(given), best = GRASPS.reduce((a, b) => (prefixLen(b, text) > prefixLen(a, text) ? b : a));
|
|
106
|
+
return prefixLen(best, text) >= 4 ? `; did you mean "${best}"?` : didYouMean(text, GRASPS);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** mass (kg): what the object weighs, for the arm that holds it (#816). */
|
|
110
|
+
function massOk(err, at, o) {
|
|
111
|
+
if (o.mass !== undefined && !(Number.isFinite(o.mass) && o.mass >= 0.01 && o.mass <= 60)) err(`${at}.mass must be kg in 0.01..60, got ${fmt(o.mass)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
101
114
|
/** visible and level name channels (C15): visible shows the record above 0.5, level scales its light, glow and effect rate. */
|
|
102
115
|
function channelsOk(err, at, o) {
|
|
103
116
|
if (o.visible !== undefined && (typeof o.visible !== "string" || !o.visible)) err(`${at}.visible must name a channel (shown above 0.5)`);
|
|
@@ -110,6 +123,7 @@ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, un
|
|
|
110
123
|
// a switch and a follow take any record (a light, an effect); the others a body or a group
|
|
111
124
|
if ((kind !== "switch" && kind !== "follow") || !scene?.others?.[id]) objectOk(err, scene, at, id, { groups: true });
|
|
112
125
|
for (const f of Object.keys(o ?? {})) if (!fields.includes(f) && !(kind === "hinge" && (f === "keys" || f === "fall"))) unknown(`${at} (${kind}) has unknown field "${f}"${didYouMean(f, fields)}; fields: ${fields.join(", ")}`);
|
|
126
|
+
massOk(err, at, o);
|
|
113
127
|
if (kind === "follow") {
|
|
114
128
|
validateFollow(spec, err, { sk, scene }, at, o);
|
|
115
129
|
channelsOk(err, at, o);
|
|
@@ -129,6 +143,7 @@ export function validateObjects(spec, err, { sk, scene, frames = spec.frames, un
|
|
|
129
143
|
if (kind === "carry") {
|
|
130
144
|
if (!spec.props?.[o.prop]) err(`${at}.prop "${o.prop}" is not in spec.props`);
|
|
131
145
|
for (const f of ["at", "along"]) if (o[f] !== undefined && !isVec3(o[f])) err(`${at}.${f} must be [d, l, n] along the palm axes, got ${fmt(o[f])}`);
|
|
146
|
+
if (o.grasp !== undefined && !GRASPS.includes(o.grasp)) err(`${at}.grasp must be one of ${GRASPS.join(", ")}${graspHint(o.grasp)}, got ${fmt(o.grasp)}`);
|
|
132
147
|
channelsOk(err, at, o);
|
|
133
148
|
if (frameOk(err, `${at}.grab`, o.grab, frames) && frameOk(err, `${at}.release`, o.release, frames)
|
|
134
149
|
&& o.grab !== undefined && o.release !== undefined && !(o.grab < o.release)) err(`${at}: grab (${o.grab}) must come before release (${o.release})`);
|
|
@@ -107,8 +107,9 @@ function validateChecks(spec, err, { sk, scene, frames, unknown = err, past = ""
|
|
|
107
107
|
each("allow");
|
|
108
108
|
for (const name of Object.keys(c.limits ?? {})) if (!(name in QA_LIMITS)) unknown(`checks.limits.${name} is not a QA limit${didYouMean(name, Object.keys(QA_LIMITS))}`);
|
|
109
109
|
const fix = spec.fix;
|
|
110
|
-
if (fix !== undefined && fix !== false && (typeof fix !== "object" || fix === null)) err(
|
|
110
|
+
if (fix !== undefined && fix !== false && (typeof fix !== "object" || fix === null)) err(`spec.fix must be false or { ${FIX_KEYS.join(", ")} }`);
|
|
111
111
|
else for (const k of Object.keys(fix || {})) if (!FIX_KEYS.includes(k)) unknown(`spec.fix.${k} is not a fix${didYouMean(k, FIX_KEYS)} (fixes: ${FIX_KEYS.join(", ")})`);
|
|
112
|
+
if (fix && fix.grasp !== undefined && typeof fix.grasp !== "boolean") err(`spec.fix.grasp must be true (fingers close on what a hand holds, the default) or false (the preset grip, the object at its grab-frame offset), got ${fmt(fix.grasp)}`);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/** opts = { sk, scene, frames, lenient, unknown: err for an unknown field (a no-op when lenient), past: the
|
|
@@ -10,13 +10,13 @@ export const NUM_CHANNELS = new Set(["yaw", "pitch", "roll", "spine_yaw", "spine
|
|
|
10
10
|
export const MAP_CHANNELS = new Set(sided(["fingers"]));
|
|
11
11
|
// Channels the skills key (standIdle, walks): valid in a spec, left out of motion.describe so a first spec need not read them.
|
|
12
12
|
export const SKILL_CHANNELS = new Set(["arm_hang"]);
|
|
13
|
-
export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point"];
|
|
13
|
+
export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point", "spherical"];
|
|
14
14
|
/** The grasps an object anchor takes a hand by (../sockets.js), and the finger preset each closes the hand to by default. */
|
|
15
|
-
export const GRASPS = ["power", "pinch", "hook", "palm"];
|
|
16
|
-
export const GRASP_FINGERS = { power: "grip", pinch: "cigarette", hook: "grip", palm: "relaxed" };
|
|
15
|
+
export const GRASPS = ["power", "pinch", "hook", "palm", "spherical"];
|
|
16
|
+
export const GRASP_FINGERS = { power: "grip", pinch: "cigarette", hook: "grip", palm: "relaxed", spherical: "spherical" };
|
|
17
17
|
/** Where the held thing sits in the hand, [d, l, n] on the palm axes from the finger middles (n toward the palm; m): a bar in a
|
|
18
18
|
* power grip or a hook lies across the palm under the finger middles, a pinch is held at the fingertips, a flat palm presses with its centre. */
|
|
19
|
-
export const GRASP_POINTS = { power: [0, 0, 0.02], hook: [0, 0, 0.02], pinch: [0.025, 0, 0.01], palm: [-0.045, 0, 0.015] };
|
|
19
|
+
export const GRASP_POINTS = { power: [0, 0, 0.02], hook: [0, 0, 0.02], pinch: [0.025, 0, 0.01], palm: [-0.045, 0, 0.015], spherical: [-0.015, 0, 0.035] };
|
|
20
20
|
|
|
21
21
|
// [min, max, convention] for numeric channels whose values have a hard range.
|
|
22
22
|
const RANGES = [
|
package/src/motion3/describe.js
CHANGED
|
@@ -103,8 +103,8 @@ function describeSkill(name) {
|
|
|
103
103
|
const MEANT = { topics: ["objects", "channels"], skills: ["reachGrip"] };
|
|
104
104
|
/** Topics no round-2 session needed for a first spec: essentials names their parts, the full topic has the fields. */
|
|
105
105
|
const SHALLOW = ["checks"];
|
|
106
|
-
/** Skills essentials leaves to topic skills: a flip
|
|
107
|
-
const FIRST_SPEC_SKIP = ["flip"];
|
|
106
|
+
/** Skills essentials leaves to topic skills: a flip, a cartwheel or a body thrown by a hit is no first spec's need, and essentials has no room. */
|
|
107
|
+
const FIRST_SPEC_SKIP = ["flip", "hitReaction", "throwObject", "catchObject"];
|
|
108
108
|
|
|
109
109
|
/** A shared shape whose fields are mostly a skill's args (walk, walkTo): its own fields and `...<skill> args`. */
|
|
110
110
|
function shapeLine(d) {
|