cozyclay 3.1.6 → 3.2.1
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 +145 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +25 -17
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/openai-compatible-provider.mjs +24 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/{app-CEP7jzXB.css → app-CnDkB5WO.css} +1 -1
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/compile-worker-D1pl6G7x.js +6 -0
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +3 -1
- package/src/App.jsx +42 -75
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/index.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/physics.js +21 -0
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +13 -14
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/center-of-mass.js +5 -23
- package/src/motion/contact-project.js +143 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/fix-collisions.js +1 -1
- package/src/motion/ik-drag.js +93 -1
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion/timeline.css +12 -0
- package/src/motion/timeline.jsx +2 -2
- package/src/motion3/compile-job.js +3 -2
- package/src/motion3/compiler/compile.js +3 -3
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/compiler/spring.js +95 -0
- package/src/motion3/compiler/validate-objects.js +18 -2
- package/src/motion3/compiler/validate-parts.js +1 -0
- package/src/motion3/compiler/vocab.js +1 -1
- package/src/motion3/fix/index.js +8 -4
- package/src/motion3/fix/person.js +110 -0
- package/src/motion3/free-motion.js +49 -11
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/object-pose.js +44 -2
- package/src/motion3/people.js +16 -1
- package/src/motion3/physics-frames.js +72 -0
- package/src/motion3/physics-trail.js +45 -0
- package/src/motion3/physics-view.js +79 -0
- package/src/motion3/pipeline.js +5 -1
- package/src/motion3/qa/ballistic.js +67 -0
- package/src/motion3/qa/com.js +140 -0
- package/src/motion3/qa/ground.js +10 -3
- package/src/motion3/qa/hints-contact.js +21 -7
- package/src/motion3/qa/hints-motion.js +38 -1
- package/src/motion3/qa/key-checks.js +5 -4
- package/src/motion3/qa/patch.js +32 -4
- package/src/motion3/qa/people-checks.js +1 -1
- package/src/motion3/qa/spring-pivot.js +23 -0
- package/src/motion3/qa/style.js +13 -30
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/schema/fields.js +4 -2
- package/src/motion3/schema/topics.js +15 -11
- package/src/motion3/schema/types.js +13 -9
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/physics-overlay-settings.js +41 -0
- package/src/physics-overlay.js +152 -0
- package/src/physics-overlay.jsx +54 -0
- package/src/physics-toggle.jsx +20 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +15 -28
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +53 -84
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-DwXhsJXz.js +0 -1
- package/dist/assets/app-BLS1Zaru.js +0 -5144
- package/dist/assets/compile-worker-or7djI2v.js +0 -6
- package/dist/assets/motion3-compile-Du7UXrWn.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +0 -13
- package/dist/assets/task-board-CkLZGPkv.js +0 -15
- package/dist/assets/workflow-DJwOUzqJ.js +0 -179
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The ballistic arc a FLOAT run should follow (qa/style.js floatChecks reads the run, this file
|
|
2
|
+
// says how to fix it). Off the ground the hips ride a gravity parabola: anchored at the frame
|
|
3
|
+
// just before takeoff (A = a - 1) and the frame just after landing (B = b + 1), with gravity
|
|
4
|
+
// fixed (g = 9.81 / fps^2 m per frame^2), the launch speed v0 is the one closed form that makes
|
|
5
|
+
// the parabola pass through both anchors:
|
|
6
|
+
// y(f) = yA + v0 (f - A) - g/2 (f - A)^2, v0 = (yB - yA) / T + g T / 2, T = B - A.
|
|
7
|
+
// The fix is that parabola minus the hips height, per frame, vertical only. No anchor (the run
|
|
8
|
+
// touches either end of the take) or a correction past MAX_FIX (the run is too long for the
|
|
9
|
+
// height it has: the arc would throw the body half a metre or more) means no fix; past MAX_FIX
|
|
10
|
+
// arcPrescription gives the apex the run needs, or the run the hips' own apex fits.
|
|
11
|
+
// Pure: numbers in, numbers out.
|
|
12
|
+
|
|
13
|
+
/** A correction past this (m) is not a fix: the run needs fewer frames, not a higher arc. */
|
|
14
|
+
export const MAX_FIX = 0.5;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ballisticArc(y, a, b, g) -> { from, to, v0, delta, worst } | null
|
|
18
|
+
* @param {ArrayLike<number>} y hips height per frame (index = frame of the same take)
|
|
19
|
+
* @param {number} a first airborne frame (index into y)
|
|
20
|
+
* @param {number} b last airborne frame (index into y)
|
|
21
|
+
* @param {number} g gravity in m per frame^2 (9.81 / fps^2)
|
|
22
|
+
* @returns delta[i] = fitted - actual height at frame a + i; worst = the frame (index into y)
|
|
23
|
+
* of the largest |delta|; from/to = the anchor frames; v0 = the launch speed (m per frame)
|
|
24
|
+
*/
|
|
25
|
+
export function ballisticArc(y, a, b, g, maxFix = MAX_FIX) {
|
|
26
|
+
if (!Number.isInteger(a) || !Number.isInteger(b) || b < a || !(g > 0)) return null;
|
|
27
|
+
const A = a - 1, B = b + 1;
|
|
28
|
+
if (A < 0 || B > y.length - 1) return null;
|
|
29
|
+
const yA = y[A], yB = y[B], T = B - A;
|
|
30
|
+
if (!Number.isFinite(yA) || !Number.isFinite(yB)) return null;
|
|
31
|
+
const v0 = (yB - yA) / T + g * T / 2, delta = [];
|
|
32
|
+
let worst = a;
|
|
33
|
+
for (let f = a; f <= b; f++) {
|
|
34
|
+
if (!Number.isFinite(y[f])) return null;
|
|
35
|
+
const t = f - A, d = yA + v0 * t - g / 2 * t * t - y[f];
|
|
36
|
+
delta.push(d);
|
|
37
|
+
if (Math.abs(d) > Math.abs(delta[worst - a])) worst = f;
|
|
38
|
+
}
|
|
39
|
+
if (Math.abs(delta[worst - a]) > maxFix) return null;
|
|
40
|
+
return { from: A, to: B, v0, delta, worst };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The frames (indexes into y) at 1/4, 1/2 and 3/4 of the way between the anchors, each once,
|
|
45
|
+
* inside the run [a, b] of arc.
|
|
46
|
+
*/
|
|
47
|
+
export function arcFrames(arc) {
|
|
48
|
+
const { from: A, to: B } = arc, a = A + 1, b = B - 1, out = [];
|
|
49
|
+
for (const q of [0.25, 0.5, 0.75]) {
|
|
50
|
+
const f = Math.min(b, Math.max(a, Math.round(A + (B - A) * q)));
|
|
51
|
+
if (!out.includes(f)) out.push(f);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* arcPrescription(y, a, b, g) -> { off, frames, apex, height, fits } | null: for an arc past MAX_FIX, the numbers that
|
|
58
|
+
* fix the run instead. A flight of N frames (anchor to anchor) peaks g N^2 / 8 above takeoff (apex, g per frame^2:
|
|
59
|
+
* 9.81 (N / fps)^2 / 8); the hips' own apex h above takeoff fits N = sqrt(8 h / g) frames (fits, rounded; 0 when the
|
|
60
|
+
* hips never rise). off = the arc minus the hips at the worst frame. Null when there is no anchor or the arc fits.
|
|
61
|
+
*/
|
|
62
|
+
export function arcPrescription(y, a, b, g) {
|
|
63
|
+
const arc = ballisticArc(y, a, b, g, Infinity);
|
|
64
|
+
if (!arc || Math.abs(arc.delta[arc.worst - a]) <= MAX_FIX) return null;
|
|
65
|
+
const frames = arc.to - arc.from, height = Math.max(...Array.from(arc.delta, (_, i) => y[a + i])) - y[arc.from];
|
|
66
|
+
return { off: arc.delta[arc.worst - a], frames, apex: g * frames * frames / 8, height, fits: height > 0 ? Math.round(Math.sqrt(8 * height / g)) : 0 };
|
|
67
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Balance measurements on one frame's pose: the mass-weighted centre of mass of the body and the
|
|
2
|
+
// support polygon of the feet on the ground. QA's BALANCE check and observations.balance
|
|
3
|
+
// (style.js) and the BALANCE hint (hints-contact.js) all read them from here, so the issue, the
|
|
4
|
+
// observation and the fix agree on the same point and the same polygon.
|
|
5
|
+
// centre of mass the segment-mass-weighted average of the segment midpoints (SEGMENT_FRACTIONS),
|
|
6
|
+
// from the frame's joint positions; a skeleton missing a segment bone falls back
|
|
7
|
+
// to the average of the hips, the spine and the head.
|
|
8
|
+
// support polygon the 2D convex hull (x, z) of the sole points (heel, ball, tip) of the feet on
|
|
9
|
+
// the ground (ground.js): one foot's points when only one is down. While the hips
|
|
10
|
+
// travel every foot counts (a step is about to land).
|
|
11
|
+
// margin how far the centre of mass's ground projection sits inside the polygon (m, the
|
|
12
|
+
// distance to its nearest edge), < 0 outside it (minus the distance to it).
|
|
13
|
+
import { groundSole } from "./ground.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Segment mass fractions of total body mass (Dempster, WADC TR 55-159, 1955, as tabulated by
|
|
17
|
+
* Winter, "Biomechanics and Motor Control of Human Movement"). The trunk (0.497) is split into
|
|
18
|
+
* pelvis (0.142) and thorax+abdomen (0.355); the hand rides on the forearm and the foot on the
|
|
19
|
+
* shank. Each entry's fraction sits at the midpoint of its two joints (bone names without a rig
|
|
20
|
+
* prefix). The fractions sum to 1. src/motion/center-of-mass.js reads the same table.
|
|
21
|
+
*/
|
|
22
|
+
export const SEGMENT_FRACTIONS = [
|
|
23
|
+
{ id: "pelvis", start: "Hips", end: "Spine1", mass: 0.142 },
|
|
24
|
+
{ id: "torso", start: "Spine1", end: "Neck", mass: 0.355 },
|
|
25
|
+
{ id: "head", start: "Neck", end: "Head", mass: 0.081 },
|
|
26
|
+
{ id: "leftUpperArm", start: "LeftArm", end: "LeftForeArm", mass: 0.028 },
|
|
27
|
+
{ id: "leftForeArmHand", start: "LeftForeArm", end: "LeftHand", mass: 0.022 },
|
|
28
|
+
{ id: "rightUpperArm", start: "RightArm", end: "RightForeArm", mass: 0.028 },
|
|
29
|
+
{ id: "rightForeArmHand", start: "RightForeArm", end: "RightHand", mass: 0.022 },
|
|
30
|
+
{ id: "leftThigh", start: "LeftUpLeg", end: "LeftLeg", mass: 0.100 },
|
|
31
|
+
{ id: "leftShankFoot", start: "LeftLeg", end: "LeftFoot", mass: 0.061 },
|
|
32
|
+
{ id: "rightThigh", start: "RightUpLeg", end: "RightLeg", mass: 0.100 },
|
|
33
|
+
{ id: "rightShankFoot", start: "RightLeg", end: "RightFoot", mass: 0.061 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/** The joints whose plain average stands in for the centre of mass on a skeleton missing a segment bone. */
|
|
37
|
+
const TRUNK = ["Hips", "Spine1", "Spine2", "Head"];
|
|
38
|
+
|
|
39
|
+
/** Mass-weighted centre of mass [x, y, z] of pose s (world joint positions s.P), or null when sk lacks a segment bone. */
|
|
40
|
+
export function centerOfMass(sk, s) {
|
|
41
|
+
const I = sk.index, c = [0, 0, 0];
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const { start, end, mass } of SEGMENT_FRACTIONS) {
|
|
44
|
+
const a = s.P[I[start]], b = s.P[I[end]];
|
|
45
|
+
if (I[start] === undefined || I[end] === undefined || !a || !b) return null;
|
|
46
|
+
for (let k = 0; k < 3; k++) c[k] += mass * (a[k] + b[k]) / 2;
|
|
47
|
+
total += mass;
|
|
48
|
+
}
|
|
49
|
+
return c.map(v => v / total);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** centerOfMass, or the average of the hips, the spine and the head where the skeleton cannot give it. */
|
|
53
|
+
export function bodyCentre(sk, s) {
|
|
54
|
+
const com = centerOfMass(sk, s);
|
|
55
|
+
if (com) return com;
|
|
56
|
+
const js = TRUNK.filter(j => sk.index[j] !== undefined);
|
|
57
|
+
return [0, 1, 2].map(k => js.reduce((acc, j) => acc + s.P[sk.index[j]][k], 0) / js.length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
61
|
+
|
|
62
|
+
/** Convex hull of 2D points [x, z], counter-clockwise in (x, z), collinear points dropped (monotone chain). */
|
|
63
|
+
export function convexHull(points) {
|
|
64
|
+
const p = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
65
|
+
if (p.length < 3) return p;
|
|
66
|
+
const half = list => {
|
|
67
|
+
const h = [];
|
|
68
|
+
for (const q of list) {
|
|
69
|
+
while (h.length >= 2 && cross(h[h.length - 2], h[h.length - 1], q) <= 1e-12) h.pop();
|
|
70
|
+
h.push(q);
|
|
71
|
+
}
|
|
72
|
+
h.pop();
|
|
73
|
+
return h;
|
|
74
|
+
};
|
|
75
|
+
return [...half(p), ...half([...p].reverse())];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Nearest point to c on segment a-b (2D). */
|
|
79
|
+
function onSegment(c, a, b) {
|
|
80
|
+
const d = [b[0] - a[0], b[1] - a[1]], l2 = d[0] * d[0] + d[1] * d[1];
|
|
81
|
+
const t = l2 > 0 ? Math.max(0, Math.min(1, ((c[0] - a[0]) * d[0] + (c[1] - a[1]) * d[1]) / l2)) : 0;
|
|
82
|
+
return [a[0] + t * d[0], a[1] + t * d[1]];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Where 2D point c ([x, z]) sits against the convex polygon of points: { margin, near }. near is
|
|
87
|
+
* the nearest point of the polygon's boundary; margin is the distance to it, > 0 inside the
|
|
88
|
+
* polygon, < 0 outside (a polygon of one or two points has no inside).
|
|
89
|
+
*/
|
|
90
|
+
export function supportMargin(c, points) {
|
|
91
|
+
const H = convexHull(points);
|
|
92
|
+
let near = H[0], best = Infinity;
|
|
93
|
+
for (let i = 0; i < (H.length > 1 ? H.length : 1); i++) {
|
|
94
|
+
const q = H.length > 1 ? onSegment(c, H[i], H[(i + 1) % H.length]) : H[0];
|
|
95
|
+
const d = Math.hypot(q[0] - c[0], q[1] - c[1]);
|
|
96
|
+
if (d < best) { best = d; near = q; }
|
|
97
|
+
}
|
|
98
|
+
const inside = H.length >= 3 && H.every((a, i) => cross(a, H[(i + 1) % H.length], c) >= 0);
|
|
99
|
+
return { margin: inside ? best : -best, near };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Balance at frame index i of out (out.frames[i]): "air" with no foot on the ground, else
|
|
104
|
+
* { margin, com, near, feet }: the centre of mass (bodyCentre), its margin against the support
|
|
105
|
+
* polygon (supportMargin of its ground projection) and the polygon's nearest point near ([x, z]).
|
|
106
|
+
* feet: the sole points [x, z] that made the polygon; ground: the ground height (groundSole) each
|
|
107
|
+
* of them was measured from, in the same order. Declared supports are style.js's.
|
|
108
|
+
*/
|
|
109
|
+
export function balanceAt(out, i) {
|
|
110
|
+
const F = out.frames, P = F[i].s.P, I = out.ctx.sk.index;
|
|
111
|
+
const moving = i > 0 && i < F.length - 1 && Math.hypot(F[i + 1].s.P[I.Hips][0] - F[i - 1].s.P[I.Hips][0], F[i + 1].s.P[I.Hips][2] - F[i - 1].s.P[I.Hips][2]) / 2 > 0.006;
|
|
112
|
+
const feet = [], ground = [];
|
|
113
|
+
for (const S of ["Left", "Right"]) {
|
|
114
|
+
const sp = groundSole(out, i, S);
|
|
115
|
+
if (!moving && Math.min(sp.heel[1], sp.ball[1]) >= 0.02) continue;
|
|
116
|
+
for (const key of ["heel", "ball", "tip"]) { feet.push([sp[key][0], sp[key][2]]); ground.push(sp.ground[key]); }
|
|
117
|
+
}
|
|
118
|
+
if (!feet.length) return "air";
|
|
119
|
+
const com = bodyCentre(out.ctx.sk, F[i].s), { margin, near } = supportMargin([com[0], com[2]], feet);
|
|
120
|
+
return { margin, com, near, feet, ground, facing: facing(P, I) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The body's horizontal forward [x, z] from the hip axis (left hip minus right hip, turned a quarter about up). */
|
|
124
|
+
function facing(P, I) {
|
|
125
|
+
const sx = P[I.LeftUpLeg][0] - P[I.RightUpLeg][0], sz = P[I.LeftUpLeg][2] - P[I.RightUpLeg][2], l = Math.hypot(sx, sz) || 1;
|
|
126
|
+
return [-sz / l, sx / l];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Words for where the centre of mass is off the support polygon: "0.12 m ahead of the feet
|
|
131
|
+
* (toward +Z)". b = balanceAt (outside: margin < 0). Ahead, behind, left and right are the
|
|
132
|
+
* body's (its hips); the axis in brackets is the world's.
|
|
133
|
+
*/
|
|
134
|
+
export function offsetWords(b) {
|
|
135
|
+
const o = [b.com[0] - b.near[0], b.com[2] - b.near[1]], [fx, fz] = b.facing;
|
|
136
|
+
const ahead = o[0] * fx + o[1] * fz, left = o[0] * fz - o[1] * fx;
|
|
137
|
+
const side = Math.abs(ahead) >= Math.abs(left) ? (ahead >= 0 ? "ahead of" : "behind") : (left >= 0 ? "left of" : "right of");
|
|
138
|
+
const axis = Math.abs(o[0]) >= Math.abs(o[1]) ? `${o[0] >= 0 ? "+" : "-"}X` : `${o[1] >= 0 ? "+" : "-"}Z`;
|
|
139
|
+
return `${(-b.margin).toFixed(2)} m ${side} the feet (toward ${axis})`;
|
|
140
|
+
}
|
package/src/motion3/qa/ground.js
CHANGED
|
@@ -52,9 +52,16 @@ export function groundY(out, i, p) {
|
|
|
52
52
|
return g;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* solePoints of frame index i with y the height above the ground under each point (x, z are world);
|
|
57
|
+
* ground: { heel, ball, tip }, the ground heights those were measured from.
|
|
58
|
+
*/
|
|
56
59
|
export function groundSole(out, i, side) {
|
|
57
|
-
const sp = solePoints(out.ctx.sk, out.frames[i].s, side);
|
|
58
|
-
for (const key of ["heel", "ball", "tip"])
|
|
60
|
+
const sp = solePoints(out.ctx.sk, out.frames[i].s, side), ground = {};
|
|
61
|
+
for (const key of ["heel", "ball", "tip"]) {
|
|
62
|
+
ground[key] = groundY(out, i, sp[key]);
|
|
63
|
+
sp[key] = [sp[key][0], sp[key][1] - ground[key], sp[key][2]];
|
|
64
|
+
}
|
|
65
|
+
sp.ground = ground;
|
|
59
66
|
return sp;
|
|
60
67
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// other limb. Each answers a world move (direction x amount) for whatever drives that part.
|
|
3
3
|
import { add, sub, scale, norm, dot, len, clamp, apply, ry, T } from "../compiler/math.js";
|
|
4
4
|
import { bonePoint } from "../compiler/skeleton.js";
|
|
5
|
-
import { CAPSULES, segPoints, sdBox, boxNormal, headDepth, segDist,
|
|
5
|
+
import { CAPSULES, segPoints, sdBox, boxNormal, headDepth, segDist, ankleY } from "./geometry.js";
|
|
6
|
+
import { balanceAt, offsetWords } from "./com.js";
|
|
6
7
|
import { vec, m3 } from "./fmt.js";
|
|
7
8
|
import { kneeFoldHint, kneeWhy } from "./hints-knee.js";
|
|
8
9
|
import { objectFrame } from "../compiler/anchors.js";
|
|
@@ -95,10 +96,22 @@ function probes(X, f, nm) {
|
|
|
95
96
|
return Object.entries(diag.props?.[nm.slice(5)] ?? {}).filter(([k, p]) => Array.isArray(p) && k !== "origin").map(([, p]) => p);
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/** The most a hand's reachGrip world anchor rises over a scene box's top it went into (#819: past ~3.5 cm the fingers miss the thing). */
|
|
100
|
+
const GRIP_LIFT = 0.035;
|
|
101
|
+
/** A hand whose reachGrip world anchor sits too low over a scene box's top: that anchor rises by what the hit lacks. */
|
|
102
|
+
function gripLift(X, g, part, id, need) {
|
|
103
|
+
const f = g.worstFrame, k = sideOf(part), box = X.out.objectsAt(X.rel(f))[id], d = X.hand(k, f);
|
|
104
|
+
const grip = d.anchor && X.keys(d.ch).some(key => / reachGrip$/.test(key.src));
|
|
105
|
+
if (!/^hand_[lr]$/.test(part) || !box || X.spec.objects?.[id] || !grip || X.spec.anchors?.[d.anchor]?.kind !== "world" || !(need > 0 && need <= GRIP_LIFT)) return null;
|
|
106
|
+
return deepest(X, f, part, box).n[1] > 0.5 ? moveHint(X, part, f, [0, need, 0], ` (over ${id}'s top)`) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
export const contactHints = {
|
|
99
110
|
COLLIDE(X, g) {
|
|
100
111
|
const [, part, id] = /^(\S+) x (.+)$/.exec(g.subject) ?? [];
|
|
101
112
|
if (/^foot_[lr]$/.test(part ?? "")) return footHint(X, g, part, id);
|
|
113
|
+
const lift = gripLift(X, g, part, id, g.worst - g.limit + MARGIN);
|
|
114
|
+
if (lift) return lift;
|
|
102
115
|
const r = boxHint(X, g, part, id, g.worst - g.limit + MARGIN);
|
|
103
116
|
if (r && /^(hand|farm)_/.test(part) && !X.spec.fix?.penetration && g.worst - g.limit <= 0.05) {
|
|
104
117
|
r.hint += "; or fix.penetration: 0.02 pushes shallow hand hits out";
|
|
@@ -184,12 +197,13 @@ export const contactHints = {
|
|
|
184
197
|
fix: { channel: ch, f: g.worstFrame, value: clean } };
|
|
185
198
|
},
|
|
186
199
|
BALANCE(X, g) {
|
|
187
|
-
const f = g.worstFrame
|
|
200
|
+
const f = g.worstFrame;
|
|
188
201
|
if (g.subject === "no foot on the ground") return { cause: X.cause("root", f), hint: `at f${f} lower foot_l or foot_r onto the ground, the floor or the top of an object under it (the ankle stands ${ankleY(X.sk)} m above it), or declare checks.supports for the seat the hips rest on` };
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
202
|
+
// the mass-weighted centre of mass toward the support polygon's nearest point, to within limit - 1 cm of it (com.js)
|
|
203
|
+
const b = balanceAt(X.out, Math.max(0, Math.min(X.out.frames.length - 1, f - (X.out.f0 ?? 0))));
|
|
204
|
+
if (b === "air" || b.margin >= 0) return null;
|
|
205
|
+
const d = -b.margin, keep = Math.min(d, Math.max(0, (g.limit ?? 0.08) - 0.01)), t = (d - keep) / d;
|
|
206
|
+
const delta = [(b.near[0] - b.com[0]) * t, 0, (b.near[1] - b.com[2]) * t];
|
|
207
|
+
return { cause: X.cause("root", f), hint: `root: move ${vec(delta)} m at f${f} (centre of mass ${offsetWords(b)}, toward the nearest edge of the support polygon), or step a foot under it`, fix: { channel: "root", f, delta } };
|
|
194
208
|
},
|
|
195
209
|
};
|
|
@@ -13,6 +13,7 @@ import { vec, m3 } from "./fmt.js";
|
|
|
13
13
|
import { moveHint } from "./hints-contact.js";
|
|
14
14
|
import { kneeFoldHint, kneeWhy } from "./hints-knee.js";
|
|
15
15
|
import { bodyMove } from "./hints-goal.js";
|
|
16
|
+
import { ballisticArc, arcFrames, arcPrescription } from "./ballistic.js";
|
|
16
17
|
|
|
17
18
|
const LIMB = { Leg: ["UpLeg", "Leg", "Foot", "knee"], ForeArm: ["Arm", "ForeArm", "Hand", "elbow"] };
|
|
18
19
|
const sideOf = s => (s.startsWith("Left") ? "l" : "r");
|
|
@@ -63,6 +64,33 @@ function probe(X, f, tries, measure) {
|
|
|
63
64
|
return { best, base, c0 };
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** The ballistic arc of a FLOAT group's run (its subject names f<a>-f<b>): the worst frame, the correction at any frame, the keys that follow it; { presc } past MAX_FIX; null without anchors. */
|
|
68
|
+
function floatArc(X, g) {
|
|
69
|
+
const f0 = X.out.f0 ?? 0, [, a = g.first, b = g.last] = / f(\d+)-f(\d+)/.exec(g.subject)?.map(Number) ?? [];
|
|
70
|
+
const y = X.out.frames.map(fr => fr.s.P[X.I.Hips][1]), G = 9.81 / (X.out.fps ?? 24) ** 2, arc = ballisticArc(y, a - f0, b - f0, G);
|
|
71
|
+
if (!arc) { const p = arcPrescription(y, a - f0, b - f0, G); return p && { presc: { ...p, from: a - 1, to: b + 1 } }; }
|
|
72
|
+
const dyAt = f => Math.round((arc.delta[f - f0 - arc.from - 1] ?? 0) * 1e4) / 1e4, at = q => ({ f: q + f0, dy: dyAt(q + f0) });
|
|
73
|
+
return { ...at(arc.worst), from: arc.from + f0, to: arc.to + f0, dyAt, keys: arcFrames(arc).map(at) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Root keys moved up or down by the arc: the agent's root key within 2 frames moves by the arc at its own frame, else a key is added; the anchors get their own height unless a root key sits within 2 frames. */
|
|
77
|
+
function arcOps(X, userSpec, arc) {
|
|
78
|
+
const ops = Array.isArray(userSpec.keys) ? [] : [{ op: "add", path: "/keys", value: [] }], used = new Set(), r4 = v => Math.round(v * 1e4) / 1e4;
|
|
79
|
+
for (const { f, dy } of arc.keys) {
|
|
80
|
+
const i = (userSpec.keys ?? []).findIndex(k => Array.isArray(k?.set?.root) && k.set.root.length === 3 && Math.abs(k.f - f) <= 2 && k.f > arc.from && k.f < arc.to);
|
|
81
|
+
if (i >= 0 && used.has(i)) continue;
|
|
82
|
+
const r = i >= 0 ? userSpec.keys[i].set.root : X.fr(f).c.root, d = i >= 0 ? arc.dyAt(userSpec.keys[i].f) : dy;
|
|
83
|
+
if (!Array.isArray(r)) continue;
|
|
84
|
+
if (i >= 0) used.add(i);
|
|
85
|
+
const value = [r4(r[0]), r4(r[1] + d), r4(r[2])];
|
|
86
|
+
ops.push(i >= 0 ? { op: "replace", path: `/keys/${i}/set/root`, value } : { op: "add", path: "/keys/-", value: { f, set: { root: value } } });
|
|
87
|
+
}
|
|
88
|
+
// the anchors keep their height: without a root key near takeoff and landing the channel would ease into the arc early and hold its last key after
|
|
89
|
+
const near = f => (userSpec.keys ?? []).some(k => Array.isArray(k?.set?.root) && Math.abs(k.f - f) <= 2);
|
|
90
|
+
for (const f of [arc.from, arc.to]) if (!near(f) && Array.isArray(X.fr(f).c.root)) ops.push({ op: "add", path: "/keys/-", value: { f, set: { root: X.fr(f).c.root.map(r4) } } });
|
|
91
|
+
return ops;
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
const hipOrShoulder = (X, k, f, limb) => X.fr(f).s.P[X.I[`${k === "l" ? "Left" : "Right"}${LIMB[limb][0]}`]];
|
|
67
95
|
|
|
68
96
|
export const motionHints = {
|
|
@@ -149,7 +177,16 @@ export const motionHints = {
|
|
|
149
177
|
SPEED: (X, g) => speedHint(X, g, 1),
|
|
150
178
|
POP: (X, g) => speedHint(X, g, 0.5),
|
|
151
179
|
ACC: (X, g) => speedHint(X, g, 0.5),
|
|
152
|
-
FLOAT
|
|
180
|
+
FLOAT(X, g) {
|
|
181
|
+
const base = `over f${g.first}-f${g.last} lower the root or key foot_l/foot_r back onto the ground, the floor or the top of an object under the foot (the ankle stands ${ankleY(X.sk)} m above it); a jump needs the hips to rise and fall; a seat or ledge the hips rest on goes in checks.supports (a foot on an object's top needs none)`;
|
|
182
|
+
const arc = floatArc(X, g);
|
|
183
|
+
if (!arc) return { cause: X.cause("root", g.worstFrame), hint: base };
|
|
184
|
+
const p = arc.presc, m2 = v => (Math.round(v * 100) / 100).toFixed(2), other = p && p.fits >= 2 && p.fits !== p.frames;
|
|
185
|
+
if (p) return { cause: X.cause("root", g.worstFrame), hint: `hips held ${m2(Math.abs(p.off))} m ${p.off > 0 ? "below" : "above"} a ballistic arc over ${p.frames} frames (f${p.from}-f${p.to}): either ${p.apex > p.height ? "raise" : "lower"} the apex to ${m2(p.apex)} m above takeoff${other ? `, or ${p.fits < p.frames ? "shorten" : "lengthen"} the air run to ${p.fits} frames for this apex (${m2(p.height)} m)` : ""}; or ${base}` };
|
|
186
|
+
const { f, dy } = arc;
|
|
187
|
+
return { cause: X.cause("root", f), hint: `hips should be ${Math.round(Math.abs(dy) * 100)} cm ${dy < 0 ? "lower" : "higher"} at f${f} (ballistic arc from f${arc.from} to f${arc.to}): root ${vec([0, dy, 0])} m there${arc.keys.length > 1 ? ` (keys at ${arc.keys.map(k => `f${k.f} ${vec([0, k.dy, 0])}`).join(", ")} follow the arc)` : ""}; or ${base}`,
|
|
188
|
+
fix: { channel: "root", f, delta: [0, dy, 0], ops: userSpec => arcOps(X, userSpec, arc) } };
|
|
189
|
+
},
|
|
153
190
|
LENGTH: () => ({ cause: "compiler", hint: "a bone changed length: report the spec, this is a compiler fault" }),
|
|
154
191
|
CONTACT(X, g) {
|
|
155
192
|
const ck = (X.spec.checks?.contacts ?? []).find(c => g.subject.startsWith(`${c.name}:`)), f = g.worstFrame, i = X.rel(f);
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// wrist turn past its limit after the elbow swung and the forearm turned (lane K8: the whole arm
|
|
9
9
|
// follows the object, compiler/arm-orient.js).
|
|
10
10
|
// Measured, not judged (C16, output.observations.held): every key as drawn, so the agent sees how
|
|
11
|
-
// far each one is from what the hand can hold without a probe.
|
|
11
|
+
// far each one is from what the hand can hold without a probe. Both read the pose before a spring swings it
|
|
12
|
+
// (object-pose.js basePoseAt): the swing rides on the keys, it is not the hand falling short.
|
|
12
13
|
import { keyedEntries, curveIn, keyedPose } from "../object-keys.js";
|
|
13
|
-
import {
|
|
14
|
+
import { basePoseAt, partsOf } from "../object-pose.js";
|
|
14
15
|
import { relative } from "../carried.js";
|
|
15
16
|
import { groupsOf } from "../scene.js";
|
|
16
17
|
import { WRIST_LIMITS } from "../compiler/arm-orient.js";
|
|
@@ -36,7 +37,7 @@ export function keyIssues(out, L, push) {
|
|
|
36
37
|
const curve = curveIn(out, id), corners = cornersOf(scene, id);
|
|
37
38
|
if (!corners.length) continue;
|
|
38
39
|
for (let f = Math.max(f0, Math.ceil(curve.t[0])); f <= Math.min(f0 + out.frames.length - 1, Math.floor(curve.t[curve.t.length - 1])); f++) {
|
|
39
|
-
const want = keyedPose(curve, f), got =
|
|
40
|
+
const want = keyedPose(curve, f), got = basePoseAt(out, id, f);
|
|
40
41
|
const off = Math.max(...corners.map(c => dist(add(want.pos, apply(want.R, c)), add(got.pos, apply(got.R, c)))));
|
|
41
42
|
if (off > L.reach) push("REACH", `${id}: ${(off * 100).toFixed(1)} cm off its keys`, f - f0, off, L.reach);
|
|
42
43
|
}
|
|
@@ -75,7 +76,7 @@ export function heldReadings(out) {
|
|
|
75
76
|
// the keys as the compile holds them: a mouth key the compile leaned says so (object-keys.js faceLeaned)
|
|
76
77
|
const curve = curveIn(out, id), keys = [...(out.ctx.hold?.entry?.(id) ?? o).keys].sort((a, b) => a.f - b.f);
|
|
77
78
|
return [id, keys.map((k, i) => {
|
|
78
|
-
const got =
|
|
79
|
+
const got = basePoseAt(out, id, Math.round(curve.t[i])), y = [got.R[0][1], got.R[1][1], got.R[2][1]];
|
|
79
80
|
const want = k.aim ? norm(k.aim) : apply(matFromQuat(curve.q[i]), [0, 1, 0]), deg = r1(angle(want, y));
|
|
80
81
|
return { f: k.f, deg, cm: r1(dist(k.pos, got.pos) * 100), ...(deg >= 1 ? { got: y.map(r2) } : {}), ...(k.aim ? { rot: curve.rots[i] } : {}), ...(k.leaned ? { lean: k.lean } : {}) };
|
|
81
82
|
})];
|
package/src/motion3/qa/patch.js
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
// { path: "/anchors/cupHandle/p", delta: [dx, dy, dz] } move a spec value by delta
|
|
8
8
|
// { channel: "root", f, delta, hold: true } move the body that stands at f
|
|
9
9
|
// { ops: userSpec => [{ op, path, value }] } the operations for the agent's spec (a second hand: load.js)
|
|
10
|
+
// + candidates: [ops, ...] other ways to the same end, which compile repair alone can
|
|
11
|
+
// price: g.candidates (non-enumerable, never in the output)
|
|
10
12
|
// Each becomes g.patch = [{ op, path, value }] against opts.userSpec (the spec as the agent sent
|
|
11
13
|
// it, use[] not expanded): a user key that sets the channel within f +-2 -> replace
|
|
12
|
-
// /keys/<i>/set/<ch
|
|
14
|
+
// /keys/<i>/set/<ch> (a delta: with the agent's keys next to it that hold the same value, so a held
|
|
15
|
+
// pose moves whole instead of sliding from one end to the other); a standIdle stance moved sideways -> /use/<i>/args/at; otherwise add
|
|
13
16
|
// /keys/- with the frame's value moved. A path fix replaces the value, or adds it when only the
|
|
14
17
|
// last step is missing (or the top-level /fix object). A hold fix moves what placed the body
|
|
15
18
|
// at f by the delta's x and z: a standIdle args.at, a walkTo's length (firstStep with 2 steps,
|
|
@@ -50,6 +53,27 @@ function userKey(userSpec, ch, f) {
|
|
|
50
53
|
return best;
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/** a and b the same channel value (a number or a vector) within 1e-6. */
|
|
57
|
+
const same = (a, b) => (isNum(a) && isNum(b) ? Math.abs(a - b) <= 1e-6
|
|
58
|
+
: Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, j) => isNum(x) && isNum(b[j]) && Math.abs(x - b[j]) <= 1e-6));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* User key i and the agent's keys next to it in frame order (no other source's key of ch between) that set
|
|
62
|
+
* ch to the same value: the hold a delta moves. The order is X.keys(ch) when the context has it, else the
|
|
63
|
+
* agent's own keys by frame.
|
|
64
|
+
*/
|
|
65
|
+
function heldSpan(X, userSpec, ch, i) {
|
|
66
|
+
const keys = userSpec.keys, held = j => j >= 0 && same(keys[j]?.set?.[ch], keys[i].set[ch]);
|
|
67
|
+
const order = X?.keys ? X.keys(ch).map(k => Number(/^keys\[(\d+)\]$/.exec(k.src)?.[1] ?? -1))
|
|
68
|
+
: keys.map((k, j) => j).filter(j => keys[j]?.set && Object.hasOwn(keys[j].set, ch) && isNum(keys[j].f)).sort((a, b) => keys[a].f - keys[b].f);
|
|
69
|
+
const at = order.indexOf(i);
|
|
70
|
+
if (at < 0) return [i];
|
|
71
|
+
let a = at, b = at;
|
|
72
|
+
while (a > 0 && held(order[a - 1])) a -= 1;
|
|
73
|
+
while (b < order.length - 1 && held(order[b + 1])) b += 1;
|
|
74
|
+
return order.slice(a, b + 1);
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
/** A standIdle stance moved by the root's sideways delta: /use/<i>/args/at. */
|
|
54
78
|
function standPatch(X, userSpec, fix, near) {
|
|
55
79
|
const m = /^use\[(\d+)\] standIdle$/.exec(near?.src ?? ""), d = fix.delta;
|
|
@@ -105,8 +129,11 @@ function channelPatch(X, userSpec, fix) {
|
|
|
105
129
|
const to = v => (fix.value !== undefined ? fix.value : moved(v, fix.delta));
|
|
106
130
|
const i = userKey(userSpec, ch, f);
|
|
107
131
|
if (i >= 0) {
|
|
108
|
-
const
|
|
109
|
-
|
|
132
|
+
const ops = (fix.delta !== undefined ? heldSpan(X, userSpec, ch, i) : [i]).map(j => {
|
|
133
|
+
const value = to(userSpec.keys[j].set[ch]);
|
|
134
|
+
return value === undefined ? null : { op: "replace", path: pointer(["keys", j, "set", ch]), value: round4(value) };
|
|
135
|
+
});
|
|
136
|
+
return ops.includes(null) ? null : ops;
|
|
110
137
|
}
|
|
111
138
|
if (!X?.fr) return null;
|
|
112
139
|
const { prev, next } = X.bracket(ch, f);
|
|
@@ -152,8 +179,9 @@ export function attachPatches(groups, { X, userSpec } = {}) {
|
|
|
152
179
|
for (const g of groups) {
|
|
153
180
|
if (!g.fix) continue;
|
|
154
181
|
try {
|
|
155
|
-
const patch = patchOf(g.fix, X, userSpec);
|
|
182
|
+
const patch = patchOf(g.fix, X, userSpec), more = patch && (g.fix.candidates ?? []).map(ops => patchOf({ ops }, X, userSpec)).filter(Boolean);
|
|
156
183
|
if (patch) g.patch = patch;
|
|
184
|
+
if (more?.length > 1) Object.defineProperty(g, "candidates", { value: more, enumerable: false, configurable: true });
|
|
157
185
|
} catch {
|
|
158
186
|
// a fix the context cannot place (no frame, no key table) gets no patch
|
|
159
187
|
}
|
|
@@ -7,7 +7,7 @@ import { allowance, QA_LIMITS } from "./checks.js";
|
|
|
7
7
|
import { dist } from "../compiler/math.js";
|
|
8
8
|
|
|
9
9
|
/** Two bodies whose hips are this far apart (m) cannot touch: a body is under 1 m from its hips to any skin. */
|
|
10
|
-
const APART = 2.2;
|
|
10
|
+
export const APART = 2.2;
|
|
11
11
|
|
|
12
12
|
/** The person's capsule endpoints at timeline frame f: [part, A, B, r] with the joints read from P. */
|
|
13
13
|
export function personCapsules(p, f) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Measured, not judged (output.observations.spring): a carried spring object swings about its base
|
|
2
|
+
// (compiler/spring.js), so a base far from the hand puts the pivot out there and every arm swing and
|
|
3
|
+
// wrist turn shakes it harder (a bag carried by its far end: the drive about 1.9x). One line per carry
|
|
4
|
+
// whose base sits PIVOT_FAR m or more from its prop origin (the hand socket), measured mid-carry.
|
|
5
|
+
import { isCarried, propOrigin } from "../carried.js";
|
|
6
|
+
import { basePoseAt, frameAt, spanOf } from "../object-pose.js";
|
|
7
|
+
import { dist } from "../compiler/math.js";
|
|
8
|
+
|
|
9
|
+
/** How far from the hand a spring object's base may sit before the observation, m. */
|
|
10
|
+
const PIVOT_FAR = 0.15;
|
|
11
|
+
|
|
12
|
+
/** observations.spring: ["SPRING_PIVOT <id>: base is 0.30 m from the hand; ..."], or null when every spring carry pivots at the hand. */
|
|
13
|
+
export function springPivots(out) {
|
|
14
|
+
const { sk, spec } = out.ctx, lines = [];
|
|
15
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) {
|
|
16
|
+
if (!o?.spring || !isCarried(o) || !spec.props?.[o.prop]) continue;
|
|
17
|
+
const [grab, release] = spanOf(out, o), f = Math.round((grab + release) / 2), base = basePoseAt(out, id, f);
|
|
18
|
+
if (!base) continue;
|
|
19
|
+
const d = dist(base.pos, propOrigin(sk, spec, frameAt(out, f).s, o));
|
|
20
|
+
if (d >= PIVOT_FAR) lines.push(`SPRING_PIVOT ${id}: base is ${d.toFixed(2)} m from the hand; the swing pivots there, so put the base at the hand with at/along`);
|
|
21
|
+
}
|
|
22
|
+
return lines.length ? lines : null;
|
|
23
|
+
}
|
package/src/motion3/qa/style.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// QA error, and so is a FLOAT: the body off the ground without falling (floatChecks). A pop is a
|
|
9
9
|
// spike, not a large value: a hard but continuous acceleration (a swing's launch) is style.
|
|
10
10
|
// See docs/motion-api-v3.1.md C13.
|
|
11
|
-
import {
|
|
11
|
+
import { sub, len, dot, angle } from "../compiler/math.js";
|
|
12
12
|
import { CAPSULES, segPoints, sdBox } from "./geometry.js";
|
|
13
13
|
import { groundSole, groundY } from "./ground.js";
|
|
14
|
+
import { balanceAt } from "./com.js";
|
|
14
15
|
|
|
15
16
|
const SPEED_JOINTS = ["Hips", "Head", "LeftHand", "RightHand", "LeftFoot", "RightFoot", "LeftLeg", "RightLeg", "LeftForeArm", "RightForeArm"];
|
|
16
17
|
const CORE = new Set(["Hips", "Head"]);
|
|
@@ -81,28 +82,12 @@ function supported(out, f, supports) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
84
|
-
* Per frame: null where a declared support carries the body, "air" with
|
|
85
|
-
*
|
|
86
|
-
* the
|
|
87
|
-
*
|
|
85
|
+
* Per frame: null where a declared support carries the body, else balanceAt (com.js): "air" with
|
|
86
|
+
* no foot on the ground, or { margin, ... } with margin in metres: how far the mass-weighted
|
|
87
|
+
* centre of mass sits inside the support polygon of the feet on the ground, the floor or an
|
|
88
|
+
* object's top (ground.js; < 0: outside it).
|
|
88
89
|
*/
|
|
89
|
-
|
|
90
|
-
const sk = out.ctx.sk, I = sk.index, F = out.frames;
|
|
91
|
-
return F.map(({ s }, f) => {
|
|
92
|
-
if (supported(out, f, supports)) return null;
|
|
93
|
-
const { P } = s;
|
|
94
|
-
const moving = f > 0 && f < F.length - 1 && Math.hypot(F[f + 1].s.P[I.Hips][0] - F[f - 1].s.P[I.Hips][0], F[f + 1].s.P[I.Hips][2] - F[f - 1].s.P[I.Hips][2]) / 2 > 0.006;
|
|
95
|
-
const feet = [];
|
|
96
|
-
for (const S of ["Left", "Right"]) {
|
|
97
|
-
const sp = groundSole(out, f, S);
|
|
98
|
-
if (moving || Math.min(sp.heel[1], sp.ball[1]) < 0.02) feet.push(sp.heel, sp.ball, sp.tip);
|
|
99
|
-
}
|
|
100
|
-
if (!feet.length) return "air";
|
|
101
|
-
const com = scale(["Hips", "Spine1", "Spine2", "Head"].reduce((acc, j) => add(acc, P[I[j]]), [0, 0, 0]), 0.25);
|
|
102
|
-
const xs = feet.map(p => p[0]), zs = feet.map(p => p[2]);
|
|
103
|
-
return Math.min(com[0] - Math.min(...xs), Math.max(...xs) - com[0], com[2] - Math.min(...zs), Math.max(...zs) - com[2]);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
90
|
+
export const balanceFrames = (out, supports = []) => out.frames.map((_, f) => (supported(out, f, supports) ? null : balanceAt(out, f)));
|
|
106
91
|
|
|
107
92
|
/** True when a hand rests on the ground at frame f: its wrist or knuckles within HAND_DOWN of the ground under them. */
|
|
108
93
|
const HAND_DOWN = 0.06;
|
|
@@ -112,7 +97,7 @@ function handDown(out, f) {
|
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
/** Runs [a, b] (indexes of out.frames) with both soles 2 cm or more above the ground (ground.js), no hand on it and no declared support. */
|
|
115
|
-
function airRuns(out, supports = []) {
|
|
100
|
+
export function airRuns(out, supports = []) {
|
|
116
101
|
const n = out.frames.length, runs = [];
|
|
117
102
|
const air = out.frames.map((_, f) => !supported(out, f, supports)
|
|
118
103
|
&& ["Left", "Right"].every(S => { const sp = groundSole(out, f, S); return Math.min(sp.heel[1], sp.ball[1]) >= 0.02; }) && !handDown(out, f));
|
|
@@ -169,12 +154,10 @@ export function styleChecks(sk, out, checks, L) {
|
|
|
169
154
|
if (deg > L.knee) push("KNEE", `${bend[0] === "knee_l" ? "Left" : "Right"} knee bent ${deg.toFixed(0)}deg`, f, deg, L.knee);
|
|
170
155
|
}
|
|
171
156
|
});
|
|
172
|
-
if (on("balance"))
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
push("BALANCE", `centre of mass ${com[0].toFixed(2)},${com[2].toFixed(2)} outside the feet`, f, -m, L.balance);
|
|
177
|
-
}
|
|
157
|
+
if (on("balance")) balanceFrames(out, checks.supports).forEach((b, f) => {
|
|
158
|
+
if (b === "air") push("BALANCE", "no foot on the ground", f, 0, 0);
|
|
159
|
+
// one subject whatever the direction: a lean that swings from ahead to left stays one group; the hint says how far and which way
|
|
160
|
+
else if (b && -b.margin > L.balance) push("BALANCE", "centre of mass outside the feet", f, -b.margin, L.balance);
|
|
178
161
|
});
|
|
179
162
|
return issues;
|
|
180
163
|
}
|
|
@@ -192,7 +175,7 @@ export function observeMotion(sk, out, checks = {}) {
|
|
|
192
175
|
return [Math.round(arr[at] * k * 100) / 100, at + f0];
|
|
193
176
|
};
|
|
194
177
|
const peaks = Object.fromEntries(kinematics(sk, out).map(({ j, v, a }) => [j, { speed: peak(v, fps), accel: peak(a, fps * fps) }]).filter(([, p]) => p.speed[0] >= STILL));
|
|
195
|
-
const margins =
|
|
178
|
+
const margins = balanceFrames(out, checks.supports).map(b => (b && b !== "air" ? b.margin : b)), span = test => {
|
|
196
179
|
const fs = margins.map((m, f) => (test(m) ? f + f0 : -1)).filter(f => f >= 0);
|
|
197
180
|
return fs.length ? [fs[0], fs[fs.length - 1]] : null;
|
|
198
181
|
}, air = airRuns(out, checks.supports).slice(0, 4).map(([a, b]) => [a + f0, b + f0]);
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// (hint-check.js) offers a patch by. Better means all of
|
|
3
3
|
// less the summed severity (severity.js) drops
|
|
4
4
|
// goals no more goals fail, and every goal that held still holds
|
|
5
|
-
// problems no more issue groups than before
|
|
5
|
+
// problems no more issue groups than before, unless a HARD group is gone and every new group is SMALL: a second
|
|
6
|
+
// hand that ends a LOAD may leave a forearm grazing the box, which the next repair round can finish
|
|
6
7
|
// trade what got worse (a new group, or a group whose severity rose) costs at most half of what got better
|
|
7
8
|
// Logs: a hint patch applied as sent made the take worse 49% of the time, and repair moved the root and broke the arms;
|
|
8
9
|
// a lower sum alone let a change through that traded one failure for several new ones.
|
|
@@ -13,6 +14,9 @@ const sevOf = qa => groupsOf(qa).reduce((sum, g) => sum + (g.severity ?? 0), 0);
|
|
|
13
14
|
const heldOf = qa => (qa?.goals ?? []).filter(g => g.ok).map(g => g.name);
|
|
14
15
|
const failedOf = qa => (qa?.goals ?? []).filter(g => !g.ok).length;
|
|
15
16
|
const TRADE = 0.5;
|
|
17
|
+
// failures the take cannot keep (a hand that cannot hold its load), and the severity (severity.js) a new group may have
|
|
18
|
+
// for a change that ends one to pass with more groups: 5 is a few frames just past a limit, never a held failure
|
|
19
|
+
const HARD = new Set(["LOAD"]), SMALL = 5;
|
|
16
20
|
const r2 = x => Math.round(x * 100) / 100;
|
|
17
21
|
|
|
18
22
|
/** The group severities by key (severity.js groupKey). */
|
|
@@ -35,9 +39,10 @@ export function compareQa(next, base) {
|
|
|
35
39
|
worse.sort((x, y) => y.severity[1] - y.severity[0] - (x.severity[1] - x.severity[0]));
|
|
36
40
|
const sev = [r2(sevOf(base)), r2(sevOf(next))], problems = [groupsOf(base).length, groupsOf(next).length];
|
|
37
41
|
const lost = heldOf(base).filter(n => !heldOf(next).includes(n));
|
|
42
|
+
const ends = groupsOf(base).some(g => HARD.has(g.kind) && !b.has(groupKey(g))) && [...b].every(([k, v]) => a.has(k) || v <= SMALL);
|
|
38
43
|
const why = !(sevOf(next) < sevOf(base) - 1e-9) ? `summed severity ${sev[0]} -> ${sev[1]}`
|
|
39
44
|
: failedOf(next) > failedOf(base) || lost.length ? `goal ${lost.length ? `"${lost[0]}" no longer holds` : "failures rose"}`
|
|
40
|
-
: problems[1] > problems[0] ? `problems ${problems[0]} -> ${problems[1]}`
|
|
45
|
+
: problems[1] > problems[0] && !ends ? `problems ${problems[0]} -> ${problems[1]}`
|
|
41
46
|
: rise > TRADE * drop + 1e-9 ? `${worse[0]?.key} got worse (${worse[0]?.severity.join(" -> ")})` : "";
|
|
42
47
|
return { better: !why, why, sev, problems, worse: worse.slice(0, 3) };
|
|
43
48
|
}
|