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/docs.js
CHANGED
|
@@ -22,6 +22,6 @@ export const MOTION_ACTION_HELP = {
|
|
|
22
22
|
+ "A carried object takes pos [x, y, z] and rot [x, y, z, w], or aim [x, y, z] (its +Y) and roll?: its base's world pose (a group: its root), grab..release only (objects.<id>.keys); the hand follows. "
|
|
23
23
|
+ "A hinge takes angle (degrees) or rot (its turn about the axis). remove: true deletes the key at frame. Refused where the take neither holds nor hinges it (drag it then). "
|
|
24
24
|
+ "Answers keyed and the motion.compile output; REACH \"<id>: n cm off its keys\": what the hand missed.",
|
|
25
|
-
report: "Page through the parts of a motion.compile or motion.qa answer that did not fit (output.more): part is groups (the default), timeline or probes, from the item index to start at; "
|
|
25
|
+
report: "Page through the parts of a motion.compile or motion.qa answer that did not fit (output.more): specId names the answer (or characterId: its installed take's); part is groups (the default), timeline or probes, from the item index to start at; "
|
|
26
26
|
+ "answers { specId, part, from, items, next } (next = the index to ask for next, null at the end). Changes nothing.",
|
|
27
27
|
};
|
package/src/motion3/fix/index.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// self arms out of the rest of the body on its fitted skin: the elbow turns about the
|
|
15
15
|
// shoulder-wrist line, the hand is pushed out along the skin (self.js)
|
|
16
16
|
// retime speed spikes get more frames, borrowed from the neighbouring keys
|
|
17
|
+
// stance a standIdle stance whose legs, feet or hips are in a scene body slides out of it, the least
|
|
18
|
+
// distance (skills/stance.js; applied when the skills expand, reported in observations.placed)
|
|
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
|
|
17
21
|
import { lowerHips, armReach } from "./reach.js";
|
|
18
22
|
import { lockFeet } from "./slide.js";
|
|
19
23
|
import { holdTurns } from "./turn.js";
|
|
@@ -22,14 +26,14 @@ import { pushOut } from "./push.js";
|
|
|
22
26
|
import { retime } from "./retime.js";
|
|
23
27
|
import { selfClear } from "./self.js";
|
|
24
28
|
|
|
25
|
-
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime"];
|
|
29
|
+
const FIX_KEYS = ["reach", "slide", "penetration", "self", "retime", "stance", "grasp"];
|
|
26
30
|
export { FIX_KEYS };
|
|
27
31
|
|
|
28
32
|
export function fixOptions(fix) {
|
|
29
|
-
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false };
|
|
33
|
+
if (fix === false) return { reach: false, slide: false, penetration: 0, self: false, retime: false, stance: false, grasp: false };
|
|
30
34
|
const f = fix ?? {};
|
|
31
35
|
const pen = f.penetration === true ? 0.02 : Math.min(0.05, Math.max(0, Number(f.penetration) || 0));
|
|
32
|
-
return { reach: f.reach !== false, slide: f.slide !== false, penetration: pen, self: f.self !== false, retime: f.retime === true };
|
|
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 };
|
|
33
37
|
}
|
|
34
38
|
|
|
35
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
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// What a hand holds weighs on its arm (#816). The load on a hand is the held object's mass (mass.js) x G x the
|
|
2
|
+
// horizontal distance from its shoulder to its wrist (N m), read on the solved frames:
|
|
3
|
+
// sag a carry whose load can reach LIGHT N m (its mass at the arm's full reach) lowers the wrist target of a
|
|
4
|
+
// plain IK hand (a hand_k target: not a fit, whose passes re-solve it, nor a keyed carry, whose keys lead
|
|
5
|
+
// it) by min(SAG_MAX, SAG_PER_KG x mass) and draws it as far in toward the shoulder, so the elbow bends
|
|
6
|
+
// more; eased in over the settle from the grab and out the same before the release (seat.js SETTLE)
|
|
7
|
+
// observe output.observations.load: each hand that carries something, { object, mass, maxNm, at, frames }
|
|
8
|
+
// LOAD "<side> holds <id> at <x> N m": past QA_LIMITS-style ONE_HAND N m with no second hand on the object; its
|
|
9
|
+
// hint asks for the other hand, and when the right hand holds, a patch adds a left reachGrip that arrives at
|
|
10
|
+
// the grab on the object's face nearest the left shoulder (second-hand.js holds only a right-hand carry)
|
|
11
|
+
// fix.grasp false turns all three off: the take of before.
|
|
12
|
+
import { add, sub, scale, norm, apply, T, len } from "./compiler/math.js";
|
|
13
|
+
import { SIDES } from "./compiler/pose.js";
|
|
14
|
+
import { armReach } from "./compiler/anchors.js";
|
|
15
|
+
import { isCarried } from "./carried.js";
|
|
16
|
+
import { massOf } from "./mass.js";
|
|
17
|
+
import { handOf, spanOf, partsOf, objectPoseAt } from "./object-pose.js";
|
|
18
|
+
import { SETTLE, restOf } from "./seat.js";
|
|
19
|
+
|
|
20
|
+
const REACH = 10, G = 9.81, LIGHT = 2, SAG_MAX = 0.06, SAG_PER_KG = 0.006, ONE_HAND = 25, SECOND = 0.5;
|
|
21
|
+
export const LOAD_UNITS = "load: what each hand carries, kg, and its most N m (mass x 9.81 x the shoulder-to-wrist distance across) at f; frames [first, last] held";
|
|
22
|
+
|
|
23
|
+
const graspOn = spec => spec?.fix !== false && spec?.fix?.grasp !== false;
|
|
24
|
+
const ease = t => { const u = Math.min(1, Math.max(0, t)); return u * u * (3 - 2 * u); };
|
|
25
|
+
|
|
26
|
+
/** The carries of spec, [id, entry, hand] (hand "l" | "r"). */
|
|
27
|
+
const carries = spec => Object.entries(spec.objects ?? {}).filter(([, o]) => isCarried(o) && spec.props?.[o.prop]).map(([id, o]) => [id, o, spec.props[o.prop].hand]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The world offset (m) the wrist target of hand k takes at frame f for what it carries, or null: ctx is the compile
|
|
31
|
+
* context (its spec, scene, sk, carrySpan), s the frame's pose so far, target the wrist target, free the share of the
|
|
32
|
+
* arm on a plain target.
|
|
33
|
+
*/
|
|
34
|
+
export function sagOf(ctx, k, f, s, target, free) {
|
|
35
|
+
const { spec, scene, sk } = ctx;
|
|
36
|
+
if (!graspOn(spec) || !(free > 1e-3)) return null;
|
|
37
|
+
for (const [id, o, hand] of carries(spec)) {
|
|
38
|
+
if (hand !== k || o.keys?.length) continue;
|
|
39
|
+
const mass = massOf(spec, scene, id), [grab, release] = ctx.carrySpan(o);
|
|
40
|
+
if (f < grab || f > release || mass * G * armReach(sk, k) < LIGHT) continue;
|
|
41
|
+
const w = ease((f - grab) / SETTLE) * ease((release - f) / SETTLE) * free, drop = Math.min(SAG_MAX, SAG_PER_KG * mass) * w;
|
|
42
|
+
if (!(drop > 0)) return null;
|
|
43
|
+
const toShoulder = sub(s.P[sk.index[`${SIDES[k]}Arm`]], target);
|
|
44
|
+
const across = len([toShoulder[0], 0, toShoulder[2]]) > 1e-6 ? norm([toShoulder[0], 0, toShoulder[2]]) : [0, 0, 0];
|
|
45
|
+
return add([0, -drop, 0], scale(across, drop));
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** N m on hand k on pose s holding `mass` kg. */
|
|
51
|
+
const nm = (sk, s, k, mass) => { const d = sub(s.P[sk.index[`${SIDES[k]}Hand`]], s.P[sk.index[`${SIDES[k]}Arm`]]); return mass * G * Math.hypot(d[0], d[2]); };
|
|
52
|
+
|
|
53
|
+
/** fn({ id, o, k, mass, f, Nm }) for each carry on each frame of out its hand holds it (grasp physics on). */
|
|
54
|
+
function eachHeld(sk, out, fn) {
|
|
55
|
+
const { spec, scene } = out.ctx, f0 = out.f0 ?? 0;
|
|
56
|
+
if (!graspOn(spec)) return;
|
|
57
|
+
const list = carries(spec).map(([id, o, k]) => ({ id, o, k, mass: massOf(spec, scene, id) }));
|
|
58
|
+
if (!list.length) return;
|
|
59
|
+
out.frames.forEach(({ s }, i) => {
|
|
60
|
+
const f = i + f0;
|
|
61
|
+
for (const c of list) if (handOf(out, c.id, f) === c.k) fn({ ...c, f, Nm: nm(sk, s, c.k, c.mass) });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** observeLoad(sk, out) -> { hand_l?, hand_r? } (each { object, mass, maxNm, at, frames: [first, last] }) or null. */
|
|
66
|
+
export function observeLoad(sk, out) {
|
|
67
|
+
const res = {};
|
|
68
|
+
eachHeld(sk, out, ({ id, k, mass, f, Nm }) => {
|
|
69
|
+
const row = res[`hand_${k}`] ??= { object: id, mass: Math.round(mass * 1000) / 1000, maxNm: 0, at: f, frames: [f, f] };
|
|
70
|
+
if (Nm > row.maxNm) { row.maxNm = Nm; row.at = f; }
|
|
71
|
+
row.frames[1] = f;
|
|
72
|
+
});
|
|
73
|
+
for (const row of Object.values(res)) row.maxNm = Math.round(row.maxNm * 10) / 10;
|
|
74
|
+
return Object.keys(res).length ? res : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True when the hand other than k is on carry id (an object anchor on it or a prop anchor on its prop) at f. */
|
|
78
|
+
function secondOn(out, id, o, k, f) {
|
|
79
|
+
const j = k === "r" ? "l" : "r", { spec, scene } = out.ctx, parts = new Set([id, ...partsOf(scene, id)]);
|
|
80
|
+
return Object.entries(spec.anchors ?? {}).some(([name, a]) => (out.baked?.[`w_${name}_${j}`]?.[f] ?? 0) > SECOND
|
|
81
|
+
&& ((a.kind === "object" && parts.has(a.object)) || (a.kind === "prop" && a.prop === o.prop)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** QA LOAD issues: a hand carrying past ONE_HAND N m with no second hand on the object. */
|
|
85
|
+
export function loadChecks(sk, out) {
|
|
86
|
+
const issues = [];
|
|
87
|
+
eachHeld(sk, out, ({ id, o, k, f, Nm }) => {
|
|
88
|
+
if (Nm > ONE_HAND && !secondOn(out, id, o, k, f)) issues.push({ kind: "LOAD", msg: `${SIDES[k].toLowerCase()} holds ${id} at ${Nm.toFixed(0)} N m`, frame: f, value: Math.round(Nm * 10) / 10, limit: ONE_HAND });
|
|
89
|
+
});
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The anchor point (object local) for the left hand on carry id at frame f: the centre of the face nearest the left
|
|
95
|
+
* shoulder, of the faces other than the bottom and the one the right palm is on (the box's other side, as two hands hold it).
|
|
96
|
+
*/
|
|
97
|
+
function otherFace(out, id, f) {
|
|
98
|
+
const { sk, scene } = out.ctx, s = out.frames[f - (out.f0 ?? 0)]?.s, pose = objectPoseAt(out, id, f), rest = restOf(scene, id);
|
|
99
|
+
const part = scene.objects[partsOf(scene, id)[0]] ?? scene.objects[id];
|
|
100
|
+
if (!s || !pose || !part?.half) return null;
|
|
101
|
+
const toLocal = p => apply(T(pose.R), sub(p, pose.pos)), c = apply(T(rest.R), sub(add(part.pos, apply(part.R, [0, part.half[1], 0])), rest.pos));
|
|
102
|
+
const palm = toLocal(scale(add(s.P[sk.index.RightHand], s.P[sk.index.RightHandMiddle1]), 0.5)), shoulder = toLocal(s.P[sk.index.LeftArm]);
|
|
103
|
+
const faces = [0, 1, 2].flatMap(i => [-1, 1].map(sg => { const p = c.slice(); p[i] += sg * part.half[i]; return { p, i, sg }; })).filter(x => !(x.i === 1 && x.sg < 0));
|
|
104
|
+
const palmFace = faces.reduce((m, x) => (Math.abs(palm[x.i] - x.p[x.i]) < Math.abs(palm[m.i] - m.p[m.i]) ? x : m));
|
|
105
|
+
const best = faces.filter(x => x !== palmFace).reduce((m, x) => (len(sub(x.p, shoulder)) < len(sub(m.p, shoulder)) ? x : m));
|
|
106
|
+
return best.p.map(x => Math.round(x * 1000) / 1000);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const loadHints = {
|
|
110
|
+
/** LOAD(X, g) -> { cause, hint, fix? }: the other hand on it; a patch when the right hand holds. */
|
|
111
|
+
LOAD(X, g) {
|
|
112
|
+
const [, side, id] = /^(left|right) holds (\S+) at/.exec(g.subject) ?? [], o = X.spec?.objects?.[id];
|
|
113
|
+
if (!o || !X.out) return null;
|
|
114
|
+
const k = side === "left" ? "l" : "r", other = k === "r" ? "left" : "right", [grab] = spanOf(X.out, o), kg = massOf(X.spec, X.out.ctx.scene, id);
|
|
115
|
+
const plain = { cause: `objects.${id} (${kg.toFixed(1)} kg)`, hint: `${id} weighs ${kg.toFixed(1)} kg: ${g.worst.toFixed(0)} N m on the ${side} arm at f${g.worstFrame} (one hand holds ${ONE_HAND}); put the ${other} hand on it too (a reachGrip on an object anchor on ${id} from f${grab}), or carry it closer to the body` };
|
|
116
|
+
if (k !== "r") return plain;
|
|
117
|
+
const local = otherFace(X.out, id, Math.max(grab, Math.min(g.worstFrame, spanOf(X.out, o)[1])));
|
|
118
|
+
if (!local) return plain;
|
|
119
|
+
const name = `${id}Other`;
|
|
120
|
+
// a big face takes the flat palm against it (a power grip's thumb would go into the box)
|
|
121
|
+
const anchor = { kind: "object", object: id, local, grasp: "palm" };
|
|
122
|
+
return { ...plain, fix: { ops: user => [user.anchors ? { op: "add", path: `/anchors/${name}`, value: anchor } : { op: "add", path: "/anchors", value: { [name]: anchor } },
|
|
123
|
+
{ op: "add", path: "/use/-", value: { skill: "reachGrip", f0: Math.max(0, grab - REACH), args: { anchor: name, side: "l", reach: Math.min(REACH, grab) || 1, hold: Math.max(1, spanOf(X.out, o)[1] - grab), release: 1 } } }] } };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// What a scene object weighs, kg, for the arm that holds it (#816). The spec says it (spec.objects[id].mass), else a
|
|
2
|
+
// small sphere is a ball (BALL kg: a baseball is 0.145), else its parts' volumes at DENSITY (wood, a filled box). A
|
|
3
|
+
// group weighs what its bodies do. Never a scene record field: motion-scene entries are saved with a take (shapes.js).
|
|
4
|
+
import { partsOf } from "./object-pose.js";
|
|
5
|
+
import { shapeOf } from "./shapes.js";
|
|
6
|
+
|
|
7
|
+
const BALL = 0.15, BALL_SIZE = 0.12, DENSITY = 400, MIN = 0.02, MAX = 60;
|
|
8
|
+
|
|
9
|
+
function volumeOf(entry) {
|
|
10
|
+
const s = shapeOf(entry), h = entry.half;
|
|
11
|
+
if (s.kind === "sphere") return (4 / 3) * Math.PI * s.r ** 3;
|
|
12
|
+
if (s.kind === "cylinder") return Math.PI * s.r * s.r * s.h;
|
|
13
|
+
return 8 * h[0] * h[1] * h[2];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function massOf(spec, scene, id) {
|
|
17
|
+
const given = spec?.objects?.[id]?.mass;
|
|
18
|
+
if (Number.isFinite(given)) return given;
|
|
19
|
+
const parts = partsOf(scene, id).map(k => scene.objects[k]);
|
|
20
|
+
if (parts.length === 1 && shapeOf(parts[0]).kind === "sphere" && 2 * shapeOf(parts[0]).r < BALL_SIZE) return BALL;
|
|
21
|
+
const kg = parts.reduce((sum, e) => sum + volumeOf(e) * DENSITY, 0);
|
|
22
|
+
return Math.min(MAX, Math.max(MIN, kg));
|
|
23
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// The object itself is still drawn in the hand (object-pose.js): the residual is what the arm
|
|
12
12
|
// and the wrist could not reach, and QA reports it (qa/key-checks.js).
|
|
13
13
|
import { isCarried, carriedPose, compose, relative } from "./carried.js";
|
|
14
|
+
import { seatsInHand, seatOffset, settleAt, between as slide } from "./seat.js";
|
|
14
15
|
import { groupsOf } from "./scene.js";
|
|
15
16
|
import { restState } from "./compiler/skeleton.js";
|
|
16
17
|
import { add, scale, sub, apply, T, matFromQuat, quatFromMat } from "./compiler/math.js";
|
|
@@ -110,7 +111,8 @@ export const keyedEntries = spec => Object.entries(spec.objects ?? {}).filter(([
|
|
|
110
111
|
export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
111
112
|
const { sk, spec, scene } = ctx, held = keyedEntries(spec);
|
|
112
113
|
if (!held.length) return null;
|
|
113
|
-
const
|
|
114
|
+
const seated = new Set(held.map(([id]) => id).filter(id => seatsInHand(spec, spec.objects[id], scene, id)).map(id => spec.objects[id]));
|
|
115
|
+
const palmOf = (o, s) => carriedPose(sk, spec, s, { prop: o.prop }, seated.has(o)), attached = new WeakMap(), attaching = new Set();
|
|
114
116
|
const restAt = (id, f) => ctx.restingAt?.(id, f) ?? scene?.objects?.[id] ?? groupsOf(scene)[id];
|
|
115
117
|
// a keyed carry of a thin body (a cup, a can, a bar) whose grip is free (no at/along, no orient: false, no grasp fit, no keyed
|
|
116
118
|
// wrist at the grab) keeps the grip its keyed path holds most comfortably (grip-choice.js), solved on the plain frames with the
|
|
@@ -136,22 +138,25 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
136
138
|
const cache = attached.get(out.baked), o = spec.objects[id];
|
|
137
139
|
if (cache.has(id)) return cache.get(id);
|
|
138
140
|
const picked = chosen(id);
|
|
139
|
-
if (picked && picked.name !== "today") {
|
|
141
|
+
if (picked && picked.name !== "today") { const a = seated.has(o) ? seatOffset(sk, spec, scene, id, o, picked.offset) : picked.offset; cache.set(id, a); cache.set(`${id}|taken`, picked.offset); return a; }
|
|
140
142
|
const rest = scene?.objects?.[id] ?? groupsOf(scene)[id], group = !scene?.objects?.[id] && !!rest;
|
|
141
143
|
const keeps = o.at === undefined && o.along === undefined && (group || o.grab !== undefined);
|
|
142
144
|
// the grab frame solved without the keys: a left hand anchored on id reads it there (object-pose.js), where this offset puts it
|
|
143
145
|
attaching.add(id);
|
|
144
146
|
let s;
|
|
145
147
|
try { s = keeps ? solvePlain(spanIn(out, o)[0]).s : restState(sk); } finally { attaching.delete(id); }
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
+
const taken = relative(palmOf(o, s), keeps ? ctx.restingAt?.(id, spanIn(out, o)[0]) ?? { pos: rest.pos, R: rest.R } : carriedPose(sk, spec, s, o));
|
|
149
|
+
const a = keeps && seated.has(o) ? seatOffset(sk, spec, scene, id, o, taken) : taken;
|
|
150
|
+
cache.set(id, a); cache.set(`${id}|taken`, taken);
|
|
148
151
|
return a;
|
|
149
152
|
};
|
|
153
|
+
// on the grasp path a keyed carry's hand slides from where it took the object into its seat over the settle (seat.js)
|
|
154
|
+
const attachAt = (id, f) => { const a = attach(id), t = attached.get(out.baked)?.get(`${id}|taken`); return t && t !== a && f !== undefined ? slide(t, a, settleAt(spanIn(out, spec.objects[id])[0], f)) : a; };
|
|
150
155
|
const carryOn = (id, s) => compose(palmOf(spec.objects[id], s), attach(id));
|
|
151
156
|
const strainOn = (id, s, pose, opts) => { const o = spec.objects[id]; return holdCost(sk, s, SIDES[spec.props[o.prop].hand], compose(pose, invert(attach(id))), palmOf(o, s), opts); };
|
|
152
157
|
const heldAt = (id, o, f) => {
|
|
153
158
|
const curve = curveIn(out, id), w = leadWeight(curve, spanIn(out, o), f);
|
|
154
|
-
return w > 0 ? { w, palm: compose(keyedPose(curve, f), invert(
|
|
159
|
+
return w > 0 ? { w, palm: compose(keyedPose(curve, f), invert(attachAt(id, f))), measure: s => palmOf(o, s), comfort: o.keys.some(k => k.lean > 0) } : null;
|
|
155
160
|
};
|
|
156
161
|
// the elbow's planned path over the frames the keys lead (hold-plan.js), once per baked channels and time map
|
|
157
162
|
const plans = new WeakMap();
|
|
@@ -263,5 +268,5 @@ export function holdHook(ctx, out, solvePlain, solveLed) {
|
|
|
263
268
|
const firstKey = id => { const t = Math.min(...spec.objects[id].keys.map(k => k.f)); return out.timeMap ? Math.round(out.timeMap(t)) : t; };
|
|
264
269
|
// the grips chosen for keyed carries (observations.wrist reads them): [{ id, hand, name, cost, today }]
|
|
265
270
|
const grips = () => held.map(([id, o]) => { const p = chosen(id); return p && { id, hand: spec.props[o.prop].hand, name: p.name, cost: p.cost, today: p.today }; }).filter(Boolean);
|
|
266
|
-
return { lead, attach, attaching: id => attaching.has(id), plainFrame: solvePlain, carryOn, strainOn, span: id => spanIn(out, spec.objects[id]), firstKey, grips };
|
|
271
|
+
return { lead, attach, attachAt, attaching: id => attaching.has(id), plainFrame: solvePlain, carryOn, strainOn, span: id => spanIn(out, spec.objects[id]), firstKey, grips };
|
|
267
272
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
// Carried objects are OBSTACLES at every frame, at their pose that frame (qa/object-checks.js);
|
|
27
27
|
// a follow entry is none.
|
|
28
28
|
import { isCarried, isFollow, carriedPose, compose, relative, movesRecord } from "./carried.js";
|
|
29
|
+
import { seatsInHand, seatOffset, settleAt, between } from "./seat.js";
|
|
29
30
|
import { followPose } from "./follow.js";
|
|
30
31
|
import { groupsOf } from "./scene.js";
|
|
31
32
|
import { solveFrame } from "./compiler/compile.js";
|
|
@@ -131,9 +132,16 @@ function carriedOn(out, id, o, f, s) {
|
|
|
131
132
|
if (!keepsOffset(scene, id, o)) return carriedPose(sk, spec, s, o);
|
|
132
133
|
// keyed (object-keys.js): the grab-frame offset solved without the keys, which the hand followed. While that
|
|
133
134
|
// grab frame is solved (a hand anchored on the object reads it), the object is where the offset puts it: at rest.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
const seat = seatsInHand(spec, o, scene, id);
|
|
136
|
+
if (o.keys?.length && out.ctx.hold) {
|
|
137
|
+
if (out.ctx.hold.attaching(id)) return restPose(scene, id);
|
|
138
|
+
const held = compose(carriedPose(sk, spec, s, { prop: o.prop }, seat), out.ctx.hold.attachAt(id, f)), grab = spanOf(out, o)[0];
|
|
139
|
+
return seat ? between(restingAt(out, id, grab) ?? restPose(scene, id), held, settleAt(grab, f)) : held;
|
|
140
|
+
}
|
|
141
|
+
const palm = st => carriedPose(sk, spec, st, { prop: o.prop }, seat), grab = spanOf(out, o)[0];
|
|
142
|
+
const taken = relative(palm(grab === f ? s : frameAt(out, grab).s), restingAt(out, id, grab) ?? restPose(scene, id));
|
|
143
|
+
// on the grasp path the object eases into its seat against the palm (seat.js)
|
|
144
|
+
return seat ? between(compose(palm(s), taken), compose(palm(s), seatOffset(sk, spec, scene, id, o, taken)), settleAt(grab, f)) : compose(palm(s), taken);
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/** Pose of the record a carried entry moves, cached on the solved frame. */
|
package/src/motion3/pipeline.js
CHANGED
|
@@ -27,6 +27,7 @@ import { canonicalGoals } from "./compiler/validate-goals.js";
|
|
|
27
27
|
import { heldReadings, HELD_UNITS } from "./qa/key-checks.js";
|
|
28
28
|
import { observeNear, NEAR_UNITS } from "./qa/near.js";
|
|
29
29
|
import { observeWrist, WRIST_UNITS } from "./qa/wrist-obs.js";
|
|
30
|
+
import { observeLoad, LOAD_UNITS } from "./load.js";
|
|
30
31
|
import { planWalks, applyWalkPlans } from "./walk-plan.js";
|
|
31
32
|
import { relaxSpec } from "./lenient.js";
|
|
32
33
|
|
|
@@ -90,11 +91,15 @@ export function compileSpec(sent, { sk, scene }, opts = {}) {
|
|
|
90
91
|
const near = observeNear(sk, out), wrist = observeWrist(sk, out);
|
|
91
92
|
if (near) Object.assign(observations, { units: `${observations.units}; ${NEAR_UNITS}`, near });
|
|
92
93
|
if (wrist) Object.assign(observations, { units: `${observations.units}; ${WRIST_UNITS}`, wrist });
|
|
94
|
+
const load = observeLoad(sk, out);
|
|
95
|
+
if (load) Object.assign(observations, { units: `${observations.units}; ${LOAD_UNITS}`, load });
|
|
93
96
|
// a skill that paced its beats to fit a short span says so where every answer keeps it (C7 tier 1)
|
|
94
97
|
const paced = used.filter(u => u.paced).map(u => `${u.skill} paced ${u.paced.toFixed(2)} to fit f${u.f0}..f${u.end}`);
|
|
95
98
|
if (paced.length) observations.paced = paced;
|
|
96
99
|
const cut = used.filter(u => u.cut).map(u => `${u.skill}: ${u.cut}`);
|
|
97
100
|
if (cut.length) observations.cut = cut;
|
|
101
|
+
const placed = used.filter(u => u.placed).map(u => `${u.skill} f${u.f0}: ${u.placed.why}: at [${u.placed.at.join(", ")}], heading ${u.placed.heading}`);
|
|
102
|
+
if (placed.length) observations.placed = placed;
|
|
98
103
|
if (plans.length) observations.planned = plans.map(p => p.note);
|
|
99
104
|
const timeline = timelineOf(out, { used, spec: expanded }), probes = asked && runProbes(out, asked, { spec: expanded, sk, scene, notes });
|
|
100
105
|
return { out, qa, observations, spec: resolved, expanded, used, fixed: out.fixed, search, timeline, ...(probes ? { probes } : {}), ...(notes.length ? { notes } : {}),
|
|
@@ -8,9 +8,11 @@ import { motionHints } from "./hints-motion.js";
|
|
|
8
8
|
import { objectHints } from "./hints-object.js";
|
|
9
9
|
import { goalHints } from "./hints-goal.js";
|
|
10
10
|
import { wristHints } from "./wrist-checks.js";
|
|
11
|
+
import { loadHints } from "../load.js";
|
|
12
|
+
import { graspHints } from "./hints-grasp.js";
|
|
11
13
|
import { attachPatches } from "./patch.js";
|
|
12
14
|
|
|
13
|
-
const HINTS = { ...contactHints, ...motionHints, ...objectHints, ...goalHints, ...wristHints };
|
|
15
|
+
const HINTS = { ...contactHints, ...motionHints, ...objectHints, ...goalHints, ...wristHints, ...loadHints, ...graspHints };
|
|
14
16
|
|
|
15
17
|
function keyTable(out) {
|
|
16
18
|
const byCh = new Map();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Does the hand hold what it carries (#816)? The COLLIDE check excuses the holding hand and forearm (object-checks.js)
|
|
2
|
+
// and nothing judged the fingers, so a ball riding the finger middles, through them, scored QA 0 (session 7f15b4ae).
|
|
3
|
+
// On the physical-grasp path: a carry with no at/along (it rests in the hand) or naming a carry grasp, on grab..release,
|
|
4
|
+
// and a fit on an object anchor with grasp while it holds (its weight and its fingers' grip at least HELD: the reach has
|
|
5
|
+
// arrived and the skill has the hand closed; the object
|
|
6
|
+
// still rests where it stands, so SUPPORT does not judge it). A carry pinned with at/along and no
|
|
7
|
+
// grasp type (a cigarette, a lighter) keeps its preset grip and is not judged here. With fix.grasp false the carries it
|
|
8
|
+
// would seat are judged as they are held (the preset grip; qa/hints-grasp.js turns grasp back on); fix: false judges none.
|
|
9
|
+
// The fingers are phalanx capsules
|
|
10
|
+
// (PHALANX m) against the held object's shapes (shapes.js: a ball is round, not its box):
|
|
11
|
+
// FINGER_PEN "<Bone> into <id>": a phalanx's skin deeper than PEN in it
|
|
12
|
+
// FINGER_GAP "<side> fingers off <id>": fewer than 3 of the 4 fingertips within GAP of its surface (pinch: the thumb
|
|
13
|
+
// and the index); the thumb and two fingers hold too (a tripod: a small ball the ring and little fingers do
|
|
14
|
+
// not reach)
|
|
15
|
+
// SUPPORT "<id> not supported": nothing touching it from below holds its centre over its contacts (within
|
|
16
|
+
// SUPPORT_TOL across), no thumb presses against the fingers (OPPOSE deg between their contact normals)
|
|
17
|
+
// and the contacts do not enclose it (their normals no more than ENCLOSE deg apart)
|
|
18
|
+
import { add, scale, dot, norm, apply } from "../compiler/math.js";
|
|
19
|
+
import { isCarried } from "../carried.js";
|
|
20
|
+
import { spanOf, partsOf, objectPoseAt } from "../object-pose.js";
|
|
21
|
+
import { SETTLE, seatsInHand } from "../seat.js";
|
|
22
|
+
import { shapeOf, normalAt, closestPoint, capsuleDepth } from "../shapes.js";
|
|
23
|
+
|
|
24
|
+
const PHALANX = 0.009, PALM = 0.022, PEN = 0.004, GAP = 0.008, HELD = 0.995, SUPPORT_TOL = 0.01, OPPOSE = 120, ENCLOSE = 150, BELOW = -0.3, ARRIVED = 0.03;
|
|
25
|
+
const FINGERS = ["Index", "Middle", "Ring", "Pinky"], SIDE = { l: "Left", r: "Right" };
|
|
26
|
+
|
|
27
|
+
/** The grasp a held object takes when the spec names none: a sphere's hand cups it, a cylinder's wraps it. */
|
|
28
|
+
function defaultGrasp(scene, id) {
|
|
29
|
+
const parts = partsOf(scene, id), kind = parts.length === 1 ? shapeOf(scene.objects[parts[0]]).kind : "box";
|
|
30
|
+
return kind === "sphere" ? "spherical" : kind === "cylinder" ? "power" : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The held carries and grasp fits, each { id, k (hand), grasp, from, to } in absolute frames, with the frames a fit weighs in. */
|
|
34
|
+
export function heldSpans(out) {
|
|
35
|
+
const { spec: asked, scene } = out.ctx, spans = [], spec = asked.fix?.grasp === false ? { ...asked, fix: { ...asked.fix, grasp: true } } : asked;
|
|
36
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) {
|
|
37
|
+
// the carries grasp physics seats (with fix.grasp false: the ones it would)
|
|
38
|
+
if (!isCarried(o) || !seatsInHand(spec, o, scene, id)) continue;
|
|
39
|
+
const k = spec.props?.[o.prop]?.hand, [from, to] = spanOf(out, o);
|
|
40
|
+
// judged once the object has settled into the hand (seat.js): the grab itself is the transition
|
|
41
|
+
if (k) spans.push({ id, k, grasp: o.grasp ?? defaultGrasp(scene, id), from: from + SETTLE + 1, to });
|
|
42
|
+
}
|
|
43
|
+
for (const fit of spec.fits ?? []) {
|
|
44
|
+
const a = spec.anchors?.[fit.anchor];
|
|
45
|
+
// a rim socket (a steering wheel) keeps its rim grip: the fingers do not close on the disc the scene draws (finger-close.js)
|
|
46
|
+
if (a?.kind !== "object" || !a.grasp || out.ctx.sockets?.get(fit.anchor)?.rim) continue;
|
|
47
|
+
const ch = `w_${fit.anchor}_${fit.side}`, f0 = out.f0 ?? 0, fingers = out.baked?.[`fingers_${fit.side}`];
|
|
48
|
+
const grip = f => Object.entries(fingers?.[f] ?? {}).reduce((acc, [p, v]) => acc + (p === "relaxed" ? 0 : v || 0), 0);
|
|
49
|
+
const held = f => (out.baked?.[ch]?.[f] ?? 0) >= HELD && grip(f) >= HELD;
|
|
50
|
+
spans.push({ id: a.object, k: fit.side, grasp: a.grasp, from: f0, to: f0 + out.frames.length - 1, held, rests: true });
|
|
51
|
+
}
|
|
52
|
+
return spans;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entriesAt = (out, id, f) => partsOf(out.ctx.scene, id).map(p => ({ ...out.ctx.scene.objects[p], ...objectPoseAt(out, p, f) }));
|
|
56
|
+
|
|
57
|
+
function handCapsules(sk, s, k) {
|
|
58
|
+
const S = SIDE[k], P = j => s.P[sk.index[`${S}Hand${j}`]], out = [{ bone: `${S}Hand`, finger: "palm", a: s.P[sk.index[`${S}Hand`]], b: P("Middle1"), r: PALM }];
|
|
59
|
+
for (const f of [...FINGERS, "Thumb"]) for (let i = 1; i <= 3; i++) out.push({ bone: `${S}Hand${f}${i}`, finger: f, tip: i === 3, a: P(`${f}${i}`), b: P(`${f}${i + 1}`), r: PHALANX });
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function contacts(capsules, entries) {
|
|
64
|
+
return capsules.map(c => {
|
|
65
|
+
let best = { depth: -Infinity };
|
|
66
|
+
for (const e of entries) { const d = capsuleDepth(e, c.a, c.b, c.r); if (d > best.depth) best = { depth: d, entry: e }; }
|
|
67
|
+
const mid = scale(add(c.a, c.b), 0.5), at = best.entry && closestPoint(best.entry, mid);
|
|
68
|
+
return { ...c, depth: best.depth, at, normal: best.entry && normalAt(best.entry, mid) };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hullDistance(p, pts) {
|
|
73
|
+
if (!pts.length) return Infinity;
|
|
74
|
+
const q = [p[0], p[2]], xs = pts.map(v => [v[0], v[2]]);
|
|
75
|
+
const segDist = (a, b) => { const ab = [b[0] - a[0], b[1] - a[1]], t = Math.max(0, Math.min(1, ((q[0] - a[0]) * ab[0] + (q[1] - a[1]) * ab[1]) / ((ab[0] ** 2 + ab[1] ** 2) || 1))); return Math.hypot(q[0] - a[0] - t * ab[0], q[1] - a[1] - t * ab[1]); };
|
|
76
|
+
const hull = monotoneHull(xs);
|
|
77
|
+
if (hull.length >= 3 && hull.every((a, i) => { const b = hull[(i + 1) % hull.length]; return (b[0] - a[0]) * (q[1] - a[1]) - (b[1] - a[1]) * (q[0] - a[0]) >= 0; })) return 0;
|
|
78
|
+
return Math.min(...hull.map((a, i) => segDist(a, hull[(i + 1) % hull.length])));
|
|
79
|
+
}
|
|
80
|
+
function monotoneHull(points) {
|
|
81
|
+
const p = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]), cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
82
|
+
if (p.length < 3) return p;
|
|
83
|
+
const lower = [], upper = [];
|
|
84
|
+
for (const v of p) { while (lower.length >= 2 && cross(lower.at(-2), lower.at(-1), v) <= 0) lower.pop(); lower.push(v); }
|
|
85
|
+
for (const v of p.reverse()) { while (upper.length >= 2 && cross(upper.at(-2), upper.at(-1), v) <= 0) upper.pop(); upper.push(v); }
|
|
86
|
+
return [...lower.slice(0, -1), ...upper.slice(0, -1)];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function frameIssues(sk, s, h, entries, push, f) {
|
|
90
|
+
const touch = contacts(handCapsules(sk, s, h.k), entries), side = SIDE[h.k].toLowerCase();
|
|
91
|
+
// a reach whose hand never got there is REACH's and CONTACT's to report, not the fingers'
|
|
92
|
+
if (h.rests && touch[0].depth < -ARRIVED) return;
|
|
93
|
+
for (const c of touch) if (c.finger !== "palm" && c.depth > PEN) push("FINGER_PEN", `${c.bone} into ${h.id}`, f, c.depth, PEN);
|
|
94
|
+
const tipOn = finger => touch.some(c => c.finger === finger && c.tip && c.depth >= -GAP);
|
|
95
|
+
const fingersOn = FINGERS.filter(tipOn).length, thumb = tipOn("Thumb");
|
|
96
|
+
// a pinch holds between the thumb and the index; a small ball in a tripod (the thumb and two fingers); else three fingers
|
|
97
|
+
const gripped = h.grasp === "pinch" ? thumb && tipOn("Index") : fingersOn >= 3 || (thumb && fingersOn >= 2);
|
|
98
|
+
if (!gripped) push("FINGER_GAP", `${side} fingers off ${h.id}`, f, fingersOn + (thumb ? 1 : 0), h.grasp === "pinch" ? 2 : 3);
|
|
99
|
+
const on = touch.filter(c => c.depth >= -GAP && c.normal);
|
|
100
|
+
const c0 = scale(entries.reduce((acc, e) => add(acc, add(e.pos, apply(e.R, [0, e.half[1], 0]))), [0, 0, 0]), 1 / entries.length);
|
|
101
|
+
const under = on.filter(c => c.normal[1] <= BELOW).map(c => c.at);
|
|
102
|
+
const thumbN = on.filter(c => c.finger === "Thumb").map(c => c.normal), fingerN = on.filter(c => FINGERS.includes(c.finger)).map(c => c.normal);
|
|
103
|
+
const mean = v => (v.length ? norm(v.reduce(add, [0, 0, 0])) : null), tn = mean(thumbN), fn = mean(fingerN);
|
|
104
|
+
const opposed = tn && fn && (Math.acos(Math.max(-1, Math.min(1, dot(tn, fn)))) * 180) / Math.PI >= OPPOSE;
|
|
105
|
+
const angle = (a, b) => (Math.acos(Math.max(-1, Math.min(1, dot(a, b)))) * 180) / Math.PI;
|
|
106
|
+
const enclosed = on.some((a, i) => on.slice(i + 1).some(b => angle(a.normal, b.normal) >= ENCLOSE));
|
|
107
|
+
if (!h.rests && !opposed && !enclosed && hullDistance(c0, under) > SUPPORT_TOL) push("SUPPORT", `${h.id} not supported`, f, Math.min(1, hullDistance(c0, under)), SUPPORT_TOL);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** FINGER_PEN, FINGER_GAP and SUPPORT issues of every held span (frames index out.frames, as every check's). */
|
|
111
|
+
export function graspChecks(sk, out) {
|
|
112
|
+
// fix.grasp false is judged too: the hand that keeps its preset grip is what its hint turns grasp back on for
|
|
113
|
+
if (out.ctx.spec.fix === false) return [];
|
|
114
|
+
const issues = [], f0 = out.f0 ?? 0, push = (kind, msg, i, value, limit) => issues.push({ kind, msg, frame: i, value: +value.toFixed(4), limit });
|
|
115
|
+
for (const h of heldSpans(out)) {
|
|
116
|
+
for (let f = Math.max(h.from, f0); f <= Math.min(h.to, f0 + out.frames.length - 1); f++) {
|
|
117
|
+
if (h.held && !h.held(f)) continue;
|
|
118
|
+
const entries = entriesAt(out, h.id, f);
|
|
119
|
+
if (entries.length) frameIssues(sk, out.frames[f - f0].s, h, entries, push, f - f0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return issues;
|
|
123
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Hints for the grasp kinds (#816, qa/grasp-checks.js, skills/catch.js): { cause, hint, fix? } as the other hints-*.js.
|
|
2
|
+
// FINGER_GAP fix.grasp false: the fingers stopped at the preset; fix /fix/grasp true. Else the hand toward the object
|
|
3
|
+
// FINGER_PEN fix.grasp false: the same fix. Else the hand out of it along its surface normal at the phalanx
|
|
4
|
+
// SUPPORT the carry names the grasp its shape takes (sphere: spherical, cylinder: power, else palm); with
|
|
5
|
+
// fix.grasp false, grasp back on first
|
|
6
|
+
// CATCH catchObject's f0 earlier by the frames the hand came late (the ball passed the catch point that much
|
|
7
|
+
// before the grab)
|
|
8
|
+
// LOAD's hint is load.js's (the second hand).
|
|
9
|
+
import { moveHint } from "./hints-contact.js";
|
|
10
|
+
import { add, scale, sub, len } from "../compiler/math.js";
|
|
11
|
+
import { shapeOf, normalAt, signedDistance } from "../shapes.js";
|
|
12
|
+
import { partsOf, restingAt } from "../object-pose.js";
|
|
13
|
+
|
|
14
|
+
const MARGIN = 0.005;
|
|
15
|
+
const graspOff = X => X.spec?.fix?.grasp === false;
|
|
16
|
+
const turnOn = { cause: "spec.fix.grasp false: the fingers stopped at the preset curl", hint: "turn grasp physics on (fix.grasp true): the fingers close until they touch and the thing rests in the hand", fix: { path: "/fix/grasp", value: true } };
|
|
17
|
+
|
|
18
|
+
/** The held object's part entries at frame f, as drawn. */
|
|
19
|
+
function heldAt(X, id, f) {
|
|
20
|
+
const { scene } = X.out.ctx, at = X.out.objectsAt?.(X.rel(f)) ?? {};
|
|
21
|
+
return partsOf(scene, id).map(p => ({ ...scene.objects[p], ...(X.out.ctx.objectPose?.(p, f) ?? at[p] ?? {}) }));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const graspHints = {
|
|
25
|
+
FINGER_GAP(X, g) {
|
|
26
|
+
if (graspOff(X)) return turnOn;
|
|
27
|
+
const [, side, id] = /^(left|right) fingers off (\S+)$/.exec(g.subject) ?? [];
|
|
28
|
+
if (!id || !X.out) return null;
|
|
29
|
+
const k = side === "left" ? "l" : "r", S = side === "left" ? "Left" : "Right", f = g.worstFrame, { s } = X.fr(f);
|
|
30
|
+
const palm = scale(add(s.P[X.I[`${S}Hand`]], s.P[X.I[`${S}HandMiddle1`]]), 0.5), parts = heldAt(X, id, f);
|
|
31
|
+
if (!parts.length) return null;
|
|
32
|
+
const near = parts.reduce((m, e) => (signedDistance(e, palm) < signedDistance(m, palm) ? e : m)), gap = signedDistance(near, palm);
|
|
33
|
+
return moveHint(X, `hand_${k}`, f, scale(normalAt(near, palm), -Math.max(0, gap - 0.03)), ` (onto ${id}: the fingers reach it from nearer)`);
|
|
34
|
+
},
|
|
35
|
+
FINGER_PEN(X, g) {
|
|
36
|
+
if (graspOff(X)) return turnOn;
|
|
37
|
+
const [, bone, id] = /^(\S+) into (\S+)$/.exec(g.subject) ?? [];
|
|
38
|
+
if (!id || !X.out || X.I[bone] === undefined) return null;
|
|
39
|
+
const k = bone.startsWith("Left") ? "l" : "r", f = g.worstFrame, p = X.fr(f).s.P[X.I[bone]], parts = heldAt(X, id, f);
|
|
40
|
+
if (!parts.length) return null;
|
|
41
|
+
const into = parts.reduce((m, e) => (signedDistance(e, p) < signedDistance(m, p) ? e : m));
|
|
42
|
+
return moveHint(X, `hand_${k}`, f, scale(normalAt(into, p), g.worst - g.limit + MARGIN), ` (out of ${id})`);
|
|
43
|
+
},
|
|
44
|
+
SUPPORT(X, g) {
|
|
45
|
+
if (graspOff(X)) return turnOn;
|
|
46
|
+
const id = /^(\S+) not supported$/.exec(g.subject)?.[1], o = X.spec?.objects?.[id];
|
|
47
|
+
if (!o || !X.out) return null;
|
|
48
|
+
const part = X.out.ctx.scene.objects[partsOf(X.out.ctx.scene, id)[0]], kind = part ? shapeOf(part).kind : "box";
|
|
49
|
+
const grasp = kind === "sphere" ? "spherical" : kind === "cylinder" ? "power" : "palm";
|
|
50
|
+
if (o.grasp === grasp) return { cause: `objects.${id}.grasp ${grasp}`, hint: `${id} rests on the fingers only: hold it nearer the palm (the hand lower under it) or with both hands` };
|
|
51
|
+
return { cause: `objects.${id} (no grasp for a ${kind})`, hint: `${id}: set objects.${id}.grasp to "${grasp}" (how a ${kind} is held)`, fix: { path: `/objects/${id}/grasp`, value: grasp } };
|
|
52
|
+
},
|
|
53
|
+
CATCH(X, g) {
|
|
54
|
+
const [, id] = /^(\S+) missed by/.exec(g.subject) ?? [], o = X.spec?.objects?.[id];
|
|
55
|
+
if (!o || !X.out) return null;
|
|
56
|
+
const grab = g.worstFrame, target = X.spec.anchors?.[`${id}Catch0`]?.p;
|
|
57
|
+
if (!target) return { cause: `objects.${id}.grab`, hint: `${id}: the hand is ${(g.worst * 100).toFixed(1)} cm from it at f${grab}; reach it earlier` };
|
|
58
|
+
// the frame the flight was at the catch point: the hand came that many frames after it
|
|
59
|
+
let best = null;
|
|
60
|
+
for (let f = Math.max(0, grab - 30); f < grab; f++) {
|
|
61
|
+
const at = restingAt(X.out, id, f)?.pos;
|
|
62
|
+
if (at && (!best || len(sub(at, target)) < best.d)) best = { f, d: len(sub(at, target)) };
|
|
63
|
+
}
|
|
64
|
+
const lead = best ? grab - best.f : 0, hint = `${id}: the hand gets there ${lead} frame(s) after it passed (${(g.worst * 100).toFixed(1)} cm off at f${grab}); start catchObject ${lead} frame(s) earlier`;
|
|
65
|
+
if (!(lead > 0)) return { cause: `use catchObject ${id}`, hint };
|
|
66
|
+
return { cause: `use catchObject ${id} f0`, hint, fix: { ops: user => {
|
|
67
|
+
const i = (user.use ?? []).findIndex(u => u.skill === "catchObject" && u.args?.object === id);
|
|
68
|
+
return i < 0 ? null : [{ op: "replace", path: `/use/${i}/f0`, value: Math.max(0, user.use[i].f0 - lead) }];
|
|
69
|
+
} } };
|
|
70
|
+
},
|
|
71
|
+
};
|
package/src/motion3/qa/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { generalChecks, QA_LIMITS } from "./checks.js";
|
|
|
7
7
|
import { styleChecks, floatChecks } from "./style.js";
|
|
8
8
|
import { specChecks } from "./spec-checks.js";
|
|
9
9
|
import { objectChecks } from "./object-checks.js";
|
|
10
|
+
import { graspChecks } from "./grasp-checks.js";
|
|
11
|
+
import { loadChecks } from "../load.js";
|
|
10
12
|
import { goalChecks } from "./goals.js";
|
|
11
13
|
import { wristIssues } from "./wrist-checks.js";
|
|
12
14
|
import { peopleChecks } from "./people-checks.js";
|
|
@@ -35,7 +37,7 @@ function summarizeIssues(issues) {
|
|
|
35
37
|
export function runMotionQa(sk, out, spec, opts = {}) {
|
|
36
38
|
const checks = spec.checks ?? {}, goals = goalChecks(out, spec.goals ?? []);
|
|
37
39
|
const issues = [...generalChecks(sk, out, { allow: checks.allow, limits: checks.limits, mouthPoints: checks.mouthPoints }), ...specChecks(out, checks),
|
|
38
|
-
...objectChecks(sk, out, spec), ...floatChecks(out, checks),
|
|
40
|
+
...objectChecks(sk, out, spec), ...graspChecks(sk, out), ...loadChecks(sk, out), ...floatChecks(out, checks),
|
|
39
41
|
...styleChecks(sk, out, checks, { ...QA_LIMITS, ...checks.limits }), ...wristIssues(sk, out), ...peopleChecks(sk, out, checks), ...goals.issues];
|
|
40
42
|
if (out.f0) for (const i of issues) i.frame += out.f0;
|
|
41
43
|
const groups = summarizeIssues(issues);
|