cozyclay 3.0.3 → 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 +67 -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-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/project.js +1 -1
- 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/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- 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/fix/index.js +5 -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 +3 -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 +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -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/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- 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 +1 -1
- package/src/studio-elements.js +1 -1
- 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-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.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/body.js
CHANGED
|
@@ -3,11 +3,43 @@
|
|
|
3
3
|
// outside positive, metres) and, for the limbs, the skin radius along the bone. A limb collides as
|
|
4
4
|
// spheres on its bone axis (limbProbes); bodyDistance is the nearest skin of every other bone but
|
|
5
5
|
// the limb's own neighbours (SKIP), so an arm reads the torso, the head, the legs and the other arm.
|
|
6
|
-
import yBody from "./rigs/y-bot-tpose-body.json" with { type: "json" };
|
|
7
|
-
import xBody from "./rigs/x-bot-tpose-body.json" with { type: "json" };
|
|
8
6
|
import { add, sub, scale, apply, T, len, dot, clamp } from "./compiler/math.js";
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
// The fits are ~0.5 MB of base64 grids, most of the editor's first-load JavaScript were they imported: each
|
|
9
|
+
// is an asset file instead, read the first time a compile needs it. Node reads it from disk; a worker (or the
|
|
10
|
+
// editor, should a compile run before preloadBodies has it) reads it synchronously over HTTP, so bodyFor
|
|
11
|
+
// stays synchronous everywhere. The editor fetches both in the background after its first paint (main.jsx).
|
|
12
|
+
const BODIES = {
|
|
13
|
+
"y-bot-tpose": new URL("./rigs/y-bot-tpose-body.json", import.meta.url),
|
|
14
|
+
"x-bot-tpose": new URL("./rigs/x-bot-tpose-body.json", import.meta.url),
|
|
15
|
+
};
|
|
16
|
+
const fits = new Map();
|
|
17
|
+
|
|
18
|
+
function readFit(url) {
|
|
19
|
+
const fs = url.protocol === "file:" ? globalThis.process?.getBuiltinModule?.("node:fs") : null;
|
|
20
|
+
if (fs) return JSON.parse(fs.readFileSync(url, "utf8"));
|
|
21
|
+
const xhr = new XMLHttpRequest();
|
|
22
|
+
xhr.open("GET", url.href, false);
|
|
23
|
+
xhr.send();
|
|
24
|
+
if (xhr.status !== 200) throw new Error(`motion body fit ${url.pathname}: HTTP ${xhr.status}`);
|
|
25
|
+
return JSON.parse(xhr.responseText);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fitOf(model) {
|
|
29
|
+
if (!fits.has(model)) fits.set(model, readFit(BODIES[model]));
|
|
30
|
+
return fits.get(model);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Fetches every rig's fit in the background, so a compile on this thread never waits on one. */
|
|
34
|
+
export function preloadBodies() {
|
|
35
|
+
return Promise.all(Object.entries(BODIES).map(async ([model, url]) => {
|
|
36
|
+
if (fits.has(model)) return;
|
|
37
|
+
const response = await fetch(url);
|
|
38
|
+
if (!response.ok) throw new Error(`motion body fit ${url.pathname}: HTTP ${response.status}`);
|
|
39
|
+
const fit = await response.json();
|
|
40
|
+
if (!fits.has(model)) fits.set(model, fit);
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
11
43
|
/** Distance read outside a grid: far enough that no probe counts it as touching. */
|
|
12
44
|
const FAR = 0.1;
|
|
13
45
|
const cache = new Map();
|
|
@@ -28,9 +60,9 @@ function skipOf(sk, b) {
|
|
|
28
60
|
|
|
29
61
|
/** The fitted body of skeleton sk, or null for a rig without one. */
|
|
30
62
|
export function bodyFor(sk) {
|
|
31
|
-
|
|
32
|
-
if (!fit) return null;
|
|
63
|
+
if (!BODIES[sk.model]) return null;
|
|
33
64
|
if (cache.has(sk.model)) return cache.get(sk.model);
|
|
65
|
+
const fit = fitOf(sk.model);
|
|
34
66
|
const grids = Object.entries(fit.grids).map(([name, g]) => {
|
|
35
67
|
const half = g.dims.map(d => ((d - 1) * fit.step) / 2), centre = g.min.map((m, k) => m + half[k]);
|
|
36
68
|
return { b: sk.index[name], min: g.min, dims: g.dims, data: decode(g.data), centre, reach: len(half) };
|
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.
|
|
@@ -35,6 +35,8 @@ import { fitWeight } from "../fix/self.js";
|
|
|
35
35
|
import { holdHook } from "../object-keys.js";
|
|
36
36
|
import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
|
|
37
37
|
import { objectPoseAt, handOf, restingAt, frameAt, spanOf } from "../object-pose.js";
|
|
38
|
+
import { heldFor } from "./finger-close.js";
|
|
39
|
+
import { sagOf } from "../load.js";
|
|
38
40
|
|
|
39
41
|
const smooth = x => x * x * (3 - 2 * x);
|
|
40
42
|
// how far past the straight arm a grasp fit's wrist target may end before it counts as wound up (solveFrame), m
|
|
@@ -136,6 +138,10 @@ function hooksFor(ctx, c, f, inFit = false, lead = true, fitSide = null) {
|
|
|
136
138
|
afterArm: (k, s, diag) => measureProps(ctx, k, s, diag),
|
|
137
139
|
// a fit's passes only place the wrist; the final pose (and a plain one) turns the hand onto the body
|
|
138
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),
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -186,7 +192,7 @@ export function compileMotion(spec, { sk, scene }, opts = {}) {
|
|
|
186
192
|
if (!(frameCount >= 1 && frameCount <= 20000)) throw new Error("MOTION_SPEC_INVALID: frames must be 1..20000");
|
|
187
193
|
const fix = fixOptions(spec.fix);
|
|
188
194
|
const [a, b] = opts.window ? [Math.max(0, opts.window[0]), Math.min(frameCount - 1, opts.window[1])] : [0, frameCount - 1];
|
|
189
|
-
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 };
|
|
190
196
|
const out = { ctx, fps: spec.fps ?? 30, frameCount, f0: a, frames: [] };
|
|
191
197
|
// object anchors read the pose refs read (object-pose.js), on the frame being solved
|
|
192
198
|
ctx.objectPose = (id, f, now) => objectPoseAt(out, id, f, now);
|
|
@@ -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
|
+
}
|
|
@@ -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 };
|
|
@@ -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) {
|
package/src/motion3/fix/index.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// retime speed spikes get more frames, borrowed from the neighbouring keys
|
|
17
17
|
// stance a standIdle stance whose legs, feet or hips are in a scene body slides out of it, the least
|
|
18
18
|
// distance (skills/stance.js; applied when the skills expand, reported in observations.placed)
|
|
19
|
+
// grasp a holding hand's fingers close on what it holds and the object rests in the hand (#816);
|
|
20
|
+
// false keeps the preset grip and the grab-frame offset
|
|
19
21
|
import { lowerHips, armReach } from "./reach.js";
|
|
20
22
|
import { lockFeet } from "./slide.js";
|
|
21
23
|
import { holdTurns } from "./turn.js";
|
|
@@ -24,14 +26,14 @@ import { pushOut } from "./push.js";
|
|
|
24
26
|
import { retime } from "./retime.js";
|
|
25
27
|
import { selfClear } from "./self.js";
|
|
26
28
|
|
|
27
|
-
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance"];
|
|
29
|
+
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance", "grasp"];
|
|
28
30
|
export { FIX_KEYS };
|
|
29
31
|
|
|
30
32
|
export function fixOptions(fix) {
|
|
31
|
-
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false };
|
|
33
|
+
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false, grasp: false };
|
|
32
34
|
const f = fix ?? {};
|
|
33
35
|
const pen = f.penetration === true ? 0.02 : Math.min(0.05, Math.max(0, Number(f.penetration) || 0));
|
|
34
|
-
return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true, stance: f.stance !== false };
|
|
36
|
+
return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true, stance: f.stance !== false, grasp: f.grasp !== false };
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** The slide report with the flat feet whose turn waited (turn.js): turns { frames, max deg }. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// A fall that leaves with a velocity (#816): a thrown thing. fall.v is the velocity its root leaves with, m/s, or "hand"
|
|
2
|
+
// on a carry: the velocity the hand gives it at the release, its pose a frame after minus a frame before, x fps (the
|
|
3
|
+
// frames beforePose gives the spin from, free-motion.js afterRelease). Without v, and with v zero, a fall is as before.
|
|
4
|
+
// It flies on a parabola (G of free-motion.js) and lands on the first surface its path comes down onto: the highest top
|
|
5
|
+
// whose point under the path at that height lies on that body (closed form per top, no stepping).
|
|
6
|
+
import { add, scale, sub } from "./compiler/math.js";
|
|
7
|
+
|
|
8
|
+
const ZERO = [0, 0, 0];
|
|
9
|
+
|
|
10
|
+
/** The velocity fall leaves with from root pose `start` (`before`: its pose a frame earlier, a hand's release), m/s. */
|
|
11
|
+
export function fallVelocity(fall, start, before, fps) {
|
|
12
|
+
if (Array.isArray(fall?.v)) return fall.v;
|
|
13
|
+
if (fall?.v === "hand" && before) return scale(sub(start.pos, before.pos), fps);
|
|
14
|
+
return ZERO;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const moves = v0 => v0.some(v => v !== 0);
|
|
18
|
+
|
|
19
|
+
/** Seconds until a point h m above its landing, leaving upward at vy m/s, comes down to it (G m/s^2). */
|
|
20
|
+
export const flightTime = (h, vy, G) => (vy + Math.sqrt(vy * vy + 2 * G * Math.max(0, h))) / G;
|
|
21
|
+
|
|
22
|
+
/** The x/z drift of a flight t s in (at most tl s, where it lands). */
|
|
23
|
+
export const drift = (v0, t, tl) => { const u = Math.min(t, tl); return [v0[0] * u, 0, v0[2] * u]; };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* landingOf (free-motion.js) for a fall from `start` leaving at v0: { support, top, h } under where it comes down. `land`
|
|
27
|
+
* is landingOf(spec, scene, id, pose, onto), `tops` the heights of the bodies it may come down on, G gravity. Coming down,
|
|
28
|
+
* it meets the highest surface whose point under its path at that height is on that surface: each top from the highest
|
|
29
|
+
* down, the drift to that height, and the support there kept when it is that top (else the next lower; the floor last).
|
|
30
|
+
*/
|
|
31
|
+
export function landingWith(land, start, v0, G, tops = []) {
|
|
32
|
+
const here = land(start), low = here.top + here.h, at = top => ({ pos: add(start.pos, drift(v0, flightTime(low - top, v0[1], G), Infinity)), R: start.R });
|
|
33
|
+
for (const top of [...new Set(tops)].filter(t => t <= low).sort((a, b) => b - a)) {
|
|
34
|
+
const there = land(at(top));
|
|
35
|
+
if (Math.abs(there.top - top) < 1e-9) return there;
|
|
36
|
+
}
|
|
37
|
+
return land(at(0));
|
|
38
|
+
}
|
|
@@ -24,6 +24,7 @@ import { groupsOf } from "./scene.js";
|
|
|
24
24
|
import { curveOf, keyedPose } from "./object-keys.js";
|
|
25
25
|
import { add, apply, sub, T, quatFromMat, I3 } from "./compiler/math.js";
|
|
26
26
|
import { shapeOf, airPath, settlePath, settledAt, spinOf, spinEnergy, TIP } from "./settle.js";
|
|
27
|
+
import { fallVelocity, moves, flightTime, drift, landingWith } from "./flight.js";
|
|
27
28
|
|
|
28
29
|
/** Gravity, m/s^2. */
|
|
29
30
|
export const G = 9.8;
|
|
@@ -97,24 +98,26 @@ function shapeIn(scene, id) {
|
|
|
97
98
|
* a frame earlier (a release from the hand), for the spin it is let go with.
|
|
98
99
|
*/
|
|
99
100
|
function fallPlan(spec, scene, id, start, t0, fall, fps, before = null) {
|
|
100
|
-
|
|
101
|
+
// a throw (fall.v, flight.js) lands where it comes down
|
|
102
|
+
const v0 = fallVelocity(fall, start, before, fps), thrown = moves(v0), landAt = pose => landingOf(spec, scene, id, pose, fall.onto ?? null);
|
|
103
|
+
const land = thrown ? landingWith(landAt, start, v0, G, Object.values(scene.objects).map(topOf)) : landAt(start), shape = fall.tumble === false ? null : shapeIn(scene, id);
|
|
101
104
|
if (shape) {
|
|
102
|
-
const spin = before ? spinOf(before.R, start.R, fps) : [0, 0, 0], air = airPath(shape, start, spin, land.top, fps);
|
|
105
|
+
const spin = before ? spinOf(before.R, start.R, fps) : [0, 0, 0], air = airPath(shape, start, spin, land.top, fps, v0);
|
|
103
106
|
// a spin tips it the way its top moves (spin x up); a spin about the vertical tips nothing
|
|
104
107
|
const path = settlePath(shape, air.at(air.tl), land.top, TIP * land.h + spinEnergy(shape, spin), [-spin[2], 0, spin[0]]);
|
|
105
|
-
if (path.steps.length || spin.some(v => v)) {
|
|
108
|
+
if (path.steps.length || spin.some(v => v) || thrown) {
|
|
106
109
|
const at = f => { const t = (f - t0) / fps; return t <= 0 ? start : t <= air.tl ? air.at(t) : settledAt(path, t - air.tl); };
|
|
107
110
|
return { t0, land: t0 + air.tl * fps, end: t0 + (air.tl + path.time) * fps, h: land.h, support: land.support, top: land.top, at, rest: path.rest };
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
|
-
const h = land.h, tl = Math.sqrt(2 * h / G);
|
|
113
|
+
const h = land.h, tl = thrown ? flightTime(h, v0[1], G) : Math.sqrt(2 * h / G), off = drift(v0, Infinity, tl);
|
|
111
114
|
const e = Math.min(0.6, Math.max(0, fall.bounce ?? BOUNCE)), vb = Math.min(e * G * tl, Math.sqrt(2 * G * BOUNCE_MAX)), tb = 2 * vb / G;
|
|
112
|
-
const rest = { pos: [start.pos[0], start.pos[1] - h, start.pos[2]], R: start.R };
|
|
115
|
+
const rest = { pos: [start.pos[0] + off[0], start.pos[1] - h, start.pos[2] + off[2]], R: start.R };
|
|
113
116
|
const at = f => {
|
|
114
117
|
const t = (f - t0) / fps;
|
|
115
118
|
if (t <= 0) return start;
|
|
116
|
-
const dy = t <= tl ? -0.5 * G * t * t : t <= tl + tb ? -h + vb * (t - tl) - 0.5 * G * (t - tl) ** 2 : -h;
|
|
117
|
-
return { pos: [start.pos[0], start.pos[1] + Math.min(0, dy), start.pos[2]], R: start.R };
|
|
119
|
+
const dy = t <= tl ? (thrown ? v0[1] * t : 0) - 0.5 * G * t * t : t <= tl + tb ? -h + vb * (t - tl) - 0.5 * G * (t - tl) ** 2 : -h, d = drift(v0, t, tl);
|
|
120
|
+
return { pos: [start.pos[0] + d[0], start.pos[1] + (thrown ? dy : Math.min(0, dy)), start.pos[2] + d[2]], R: start.R };
|
|
118
121
|
};
|
|
119
122
|
return { t0, land: t0 + tl * fps, end: t0 + (tl + tb) * fps, h, support: land.support, top: land.top, at, rest };
|
|
120
123
|
}
|