cozyclay 3.0.3 → 3.1.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 +93 -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 +29 -62
- 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/plan-tool.mjs +114 -0
- package/bin/agent/script-tool.mjs +2 -0
- package/bin/agent/sculpt-sketch.mjs +190 -0
- package/bin/agent/session-store.mjs +4 -3
- package/bin/agent/sketch-code.mjs +151 -0
- package/bin/agent/sketch-sandbox.mjs +102 -0
- 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/agent/turn-frames.mjs +4 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/app-DmR7jBkz.js +5144 -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-BB3_1-EB.js +1 -0
- package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
- package/dist/assets/task-board-ErQvfoaz.js +14 -0
- package/dist/assets/workflow-zQM-EKW1.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/mcp/tool-handlers.mjs +2 -2
- package/package.json +5 -3
- package/src/App.jsx +51 -282
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/elements.js +4 -3
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/objects.js +1 -1
- package/src/commands/project.js +1 -1
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/material-look.js +189 -0
- package/src/material-presets.js +78 -0
- package/src/material-textures.js +157 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/fix/index.js +5 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +3 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-look.js +9 -5
- 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/primitive-look.js +38 -0
- 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 +24 -35
- package/src/render-passes.js +7 -1
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +5 -9
- package/src/scenes.js +4 -5
- package/src/sculpt-colours.js +65 -0
- package/src/sculpt-compare.js +13 -50
- package/src/sculpt-csg.js +112 -0
- package/src/sculpt-curve.js +123 -0
- package/src/sculpt-cutters.js +43 -0
- package/src/sculpt-detail.js +207 -0
- package/src/sculpt-geometry-loft-convex.js +100 -0
- package/src/sculpt-geometry-shapes.js +250 -0
- package/src/sculpt-geometry.js +33 -15
- package/src/sculpt-loft-rings.js +225 -0
- package/src/sculpt-noise.js +65 -0
- package/src/sculpt-part-fields.js +27 -0
- package/src/sculpt-recipe-read.js +65 -0
- package/src/sculpt-recipe.js +33 -75
- package/src/sculpt-run.js +1 -1
- package/src/sculpt-scatter-items.js +183 -0
- package/src/sculpt-scatter-shape.js +57 -0
- package/src/sculpt-scatter.js +124 -0
- package/src/sculpt-shapes-curved.js +211 -0
- package/src/sculpt-shapes-loft-convex.js +218 -0
- package/src/sculpt-shapes.js +138 -0
- package/src/sculpt-sketch-state.js +76 -0
- package/src/sculpt-sketch.jsx +124 -0
- package/src/sculpt-surface-fields.js +65 -0
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +6 -2
- 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 +79 -46
- package/src/workflow/AgentPlanCard.jsx +41 -0
- 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 +25 -36
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/agent-plan.css +93 -0
- package/src/workflow/agent-plan.js +59 -0
- 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 +10 -17
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
|
@@ -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,6 +91,8 @@ 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;
|
|
@@ -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);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// QA of scene objects the spec carries (C3 and 7.1), as plain issues { kind, msg, frame, value,
|
|
2
2
|
// limit } (frame indexes out.frames):
|
|
3
3
|
// GRAB "<id>: hand 12.0 cm from it at grab", frame = grab, limit = checks.limits.grab
|
|
4
|
+
// CATCH "<id> missed by 4.2 cm": a carry that takes it out of the air (a fall with v before the grab) is more than
|
|
5
|
+
// CATCH_GAP m from it at the grab (#816, catchObject)
|
|
4
6
|
// (default 0.03 m); the distance runs from the prop origin to the part boxes' surface
|
|
5
7
|
// at their scene pose (where the hand finds them). An object its visible channel shows
|
|
6
8
|
// only from grab on appears in the hand (a cigarette out of a pocket), and one grabbed
|
|
@@ -23,7 +25,7 @@ import { isCarried, propOrigin } from "../carried.js";
|
|
|
23
25
|
import { objectBoxesAt, partsOf, spanOf, restingAt } from "../object-pose.js";
|
|
24
26
|
import { add, sub, apply, dot, cross, len, dist, scale, norm } from "../compiler/math.js";
|
|
25
27
|
|
|
26
|
-
const NEAR_SUPPORT = 3;
|
|
28
|
+
const NEAR_SUPPORT = 3, CATCH_GAP = 0.03;
|
|
27
29
|
|
|
28
30
|
const centre = b => add(b.pos, apply(b.R, [0, b.half[1], 0]));
|
|
29
31
|
const col = (R, i) => [R[0][i], R[1][i], R[2][i]];
|
|
@@ -100,7 +102,10 @@ export function objectChecks(sk, out, spec) {
|
|
|
100
102
|
const [grab, release] = spanOf(out, o), k = spec.props?.[o.prop]?.hand, excused = new Set([`hand_${k}`, `farm_${k}`]);
|
|
101
103
|
if (o.grab !== undefined && grab > 0 && out.frames[grab - f0] && shownAt(out, o, grab - 1)) {
|
|
102
104
|
const gap = grabGap(out, id, grab);
|
|
103
|
-
|
|
105
|
+
// a carry that takes a thing out of the air (a fall with v before its grab: catchObject) misses it: CATCH
|
|
106
|
+
const flying = [o.fall ?? []].flat().some(fl => Array.isArray(fl.v) && fl.at < grab);
|
|
107
|
+
if (flying && gap > CATCH_GAP) push("CATCH", `${id} missed by ${(gap * 100).toFixed(1)} cm`, grab - f0, gap, CATCH_GAP);
|
|
108
|
+
else if (!flying && gap > L.grab) push("GRAB", `${id}: hand ${(gap * 100).toFixed(1)} cm from it at grab`, grab - f0, gap, L.grab);
|
|
104
109
|
}
|
|
105
110
|
const support = o.grab !== undefined ? supportOf(out, id, grab) : null;
|
|
106
111
|
const nearSupport = f => Math.abs(f - grab) <= NEAR_SUPPORT || (o.release !== undefined && Math.abs(f - release) <= NEAR_SUPPORT);
|
package/src/motion3/qa/patch.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// { path: "/fix/retime", value: true } set a spec path
|
|
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
|
+
// { ops: userSpec => [{ op, path, value }] } the operations for the agent's spec (a second hand: load.js)
|
|
9
10
|
// Each becomes g.patch = [{ op, path, value }] against opts.userSpec (the spec as the agent sent
|
|
10
11
|
// it, use[] not expanded): a user key that sets the channel within f +-2 -> replace
|
|
11
12
|
// /keys/<i>/set/<ch>; a standIdle stance moved sideways -> /use/<i>/args/at; otherwise add
|
|
@@ -131,7 +132,7 @@ function pathPatch(userSpec, fix) {
|
|
|
131
132
|
|
|
132
133
|
/** The patch for one fix, or null when it does not apply mechanically or changes nothing. */
|
|
133
134
|
function patchOf(fix, X, userSpec) {
|
|
134
|
-
const patch = fix.channel !== undefined ? channelPatch(X, userSpec, fix) : fix.path !== undefined ? pathPatch(userSpec, fix) : null;
|
|
135
|
+
const patch = fix.ops ? fix.ops(userSpec) : (fix.channel !== undefined ? channelPatch(X, userSpec, fix) : fix.path !== undefined ? pathPatch(userSpec, fix) : null);
|
|
135
136
|
if (!patch) return null;
|
|
136
137
|
try {
|
|
137
138
|
return JSON.stringify(applyPatch(userSpec, patch)) === JSON.stringify(userSpec) ? null : patch;
|
|
@@ -12,6 +12,8 @@ import { carried } from "../compiler/limbs.js";
|
|
|
12
12
|
import { resolveAnchor } from "../compiler/anchors.js";
|
|
13
13
|
import { CAPSULES, segPoints, sdBox } from "./geometry.js";
|
|
14
14
|
import { refDistance } from "../refs.js";
|
|
15
|
+
import { seatsInHand } from "../seat.js";
|
|
16
|
+
import { handOf } from "../object-pose.js";
|
|
15
17
|
|
|
16
18
|
export function pointOf(out, f, ref) {
|
|
17
19
|
const { s, diag, c } = out.frames[f], [kind, rest] = ref.split(/:(.*)/s);
|
|
@@ -22,6 +24,12 @@ export function pointOf(out, f, ref) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** The part a contact target "anchor:<name>" holds by when that anchor takes a grasp (its socket's owner, else its object), else null. */
|
|
27
|
+
/** The carry on the grasp path (seat.js) the hand of prop point `ref` holds at absolute frame f, or null (#816). */
|
|
28
|
+
function seatedIn(out, ref, f) {
|
|
29
|
+
const prop = /^prop:([^.]+)\./.exec(ref)?.[1], k = out.ctx.spec.props?.[prop]?.hand, { spec, scene } = out.ctx;
|
|
30
|
+
return k ? Object.keys(spec.objects ?? {}).find(id => seatsInHand(spec, spec.objects[id], scene, id) && handOf(out, id, f) === k) ?? null : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
function graspPart(out, ref) {
|
|
26
34
|
const [kind, name] = ref.split(/:(.*)/s), a = kind === "anchor" ? out.ctx.spec.anchors?.[name] : null;
|
|
27
35
|
if (a?.kind !== "object" || !a.grasp) return null;
|
|
@@ -46,7 +54,8 @@ export function specChecks(out, checks = {}) {
|
|
|
46
54
|
// on a grasp's anchor the fingers close on the held part wherever the grasp turned them round it: they are on it
|
|
47
55
|
// when they are on its surface (as goals' near on an object), not on one point of it (session 7f15b4ae: 0.4 cm
|
|
48
56
|
// on the cup, read as 5.3 cm from the anchor)
|
|
49
|
-
|
|
57
|
+
// and a hand that holds a thing resting in its palm (seat.js) has its fingers on that thing's surface
|
|
58
|
+
const held = graspPart(out, k.target) ?? seatedIn(out, k.point, f + (out.f0 ?? 0)), e = held ? refDistance(out, f, k.point, `object:${held}`) ?? len(sub(a, b)) : len(sub(a, b));
|
|
50
59
|
if (e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
|
|
51
60
|
}
|
|
52
61
|
for (const k of checks.orient ?? []) if (active(out, f, k.when)) {
|
|
@@ -15,10 +15,10 @@ export const ANCHOR_FIELDS = { world: ["p"], body: ["p"], bone: ["bone", "offset
|
|
|
15
15
|
export const PROP_FIELDS = ["hand", "origin", "points"];
|
|
16
16
|
export const FIT_FIELDS = ["side", "anchor", "effector", "offset", "orient"];
|
|
17
17
|
/** spec.objects[id]: a hinge or a carry (a body or a group id), see C3; carry keys: C14; a switch (any record, C15) moves nothing; a follow (any record, C16) rides a point of the body; a free entry (C17) moves by keys and falls. */
|
|
18
|
-
export const OBJECT_FIELDS = { hinge: ["pivot", "axis", "channel"], carry: ["prop", "at", "along", "visible", "level", "grab", "release", "keys", "fall"], switch: ["visible", "level"],
|
|
18
|
+
export const OBJECT_FIELDS = { hinge: ["pivot", "axis", "channel"], carry: ["prop", "at", "along", "visible", "level", "grab", "release", "keys", "fall", "grasp", "mass"], switch: ["visible", "level"],
|
|
19
19
|
follow: ["follow", "offset", "aim", "visible", "level"], free: ["keys", "fall", "visible", "level"] };
|
|
20
|
-
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces, whether it rolls to rest. */
|
|
21
|
-
export const FALL_FIELDS = ["at", "onto", "bounce", "tumble"];
|
|
20
|
+
/** One spec.objects[id].fall entry (C17): the frame it starts to fall, what it lands on, how much it bounces, whether it rolls to rest, the velocity it leaves with. */
|
|
21
|
+
export const FALL_FIELDS = ["at", "onto", "bounce", "tumble", "v"];
|
|
22
22
|
/** One spec.objects[id].keys entry (C14): the held object's world pose at frame f; aim (+ roll) in place of rot (C16). */
|
|
23
23
|
export const OBJECT_KEY_FIELDS = ["f", "pos", "rot", "aim", "roll", "lean"];
|
|
24
24
|
export const WALK_FIELDS = ["type", "f0", "steps", "step", "period", "swing", "halfWidth", "lift", "heading", "turnDistance", "pelvisHigh", "pelvisLow", "firstStep", "armSwing", "vary"];
|
|
@@ -48,7 +48,7 @@ export const EXAMPLES = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
const UNITS = `World metres, +Y up; ${CONVENTIONS.yaw}; the character's left is +X at yaw 0. Angles in degrees.`;
|
|
51
|
-
const NO_JUMP = "
|
|
51
|
+
const NO_JUMP = "No at/along: it rests in the palm, fingers closed on it. Throw/catch: throwObject, catchObject.";
|
|
52
52
|
/** The objects example's carry: a grip prop and the object it holds. */
|
|
53
53
|
const CARRY = { props: EXAMPLES.objects.props, objects: { cup: EXAMPLES.objects.objects.cup } };
|
|
54
54
|
|
|
@@ -70,19 +70,20 @@ export const NOTES = {
|
|
|
70
70
|
props: ["A prop is a set of points fixed to one hand, [d, l, n] along the palm axes; prop:<prop>.origin is the mean of its origin bones.",
|
|
71
71
|
"fits: the hand target moves so the effector (a prop point or the mean of joints), not the wrist, lands on the anchor.",
|
|
72
72
|
"A fit on a scene body also turns the hand about the forearm into a grip: a thin body (a can, a bar, a handle, a neck) across the palm, else thumb up; never past a comfortable forearm turn. A wrist you turn (wtwist/wflex) wins; fit.orient: false (reachGrip args.orient: false) turns it off.",
|
|
73
|
-
"reachGrip adds props.<anchor>Grip and its fit. Carry a scene object with objects.<id>.prop (topic objects)."
|
|
73
|
+
"reachGrip adds props.<anchor>Grip and its fit. Carry a scene object with objects.<id>.prop (topic objects).",
|
|
74
|
+
"Do not pin a carried thing on a knuckle point: without at/along it rests in the palm and the fingers close on it (fix.grasp). objects.<id>.grasp names how: power, pinch, hook, palm, spherical (a ball). objects.<id>.mass (kg) weighs on the arm; past one hand QA says LOAD and patches in the other hand."],
|
|
74
75
|
objects: ["A hinge turns the object (a body or a group) about pivot/axis by the angle channel your keys set (degrees).",
|
|
75
76
|
"A carry follows the prop's hand: before grab the scene pose, grab..release in the hand, after release where it was let go, riding a body under it that keys or a hinge move.",
|
|
76
|
-
|
|
77
|
+
"No grab: in hand from frame 0; no release: to the end. No at/along: it rests in the palm, fingers closed on it.",
|
|
77
78
|
"A group (Empty) id moves all its parts; at/along pin a group's root to the palm too (an object.createKit kit). A carried object is an obstacle for the body and the scene.",
|
|
78
79
|
"A switch { visible?, level? } (no prop, no hinge field) moves nothing: it shows or hides any record (a part, a light, an effect) and sets its level, 0..4 light, glow and effect strength "
|
|
79
80
|
+ "(an unkeyed level reads 1). Hiding a group hides its parts. A carry takes level too.",
|
|
80
|
-
"keys pose a carried object in the world while
|
|
81
|
-
+ "or aim [x, y, z]
|
|
82
|
-
+ `The object leads and the
|
|
81
|
+
"keys pose a carried object in the world while in hand: pos its base (a group: its root), rot a quaternion [x, y, z, w], "
|
|
82
|
+
+ "or aim [x, y, z] where its +Y points (a bat's barrel) plus roll degrees about it (none: the roll the arm holds most easily). "
|
|
83
|
+
+ `The object leads and the arm follows (elbow, forearm, then the wrist within ${Object.entries(WRIST_LIMITS).map(([k, [lo, hi]]) => `${k} ${lo}..${hi}`).join(", ")} deg; dev + toward the thumb); `
|
|
83
84
|
+ "a left hand on a prop anchor of it turns with it; "
|
|
84
|
-
+ `the hand eases in and out over ${KEY_BLEND} frames.
|
|
85
|
-
+ "
|
|
85
|
+
+ `the hand eases in and out over ${KEY_BLEND} frames. REACH "<id>: n cm off its keys" is what the hand could not reach; `
|
|
86
|
+
+ "observations.held measures each key as drawn.",
|
|
86
87
|
"free { keys?, fall? } moves what nobody holds: keys at any frame; fall drops it at frame at onto the still top below (onto: that one; else the floor) "
|
|
87
88
|
+ "to rest on a stable face, one small bounce (0: none). A carry falls before grab or after release.",
|
|
88
89
|
"A follow { follow: <ref>, offset?, aim?, visible?, level? } puts any record (an effect, a light, a part) on a body point every frame: "
|
|
@@ -86,7 +86,8 @@ export const FIT_TYPES = {
|
|
|
86
86
|
const OBJECT_KEY_TYPES = { f: int(), pos: ref("vec3"), rot: { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4, description: "quat x, y, z, w" },
|
|
87
87
|
aim: ref("vec3", "or rot: +Y dir"), roll: num("deg about aim"), lean: num("aim tilt", { minimum: 0, maximum: 45 }) };
|
|
88
88
|
|
|
89
|
-
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }), tumble: { type: "boolean" }
|
|
89
|
+
const FALL = { type: "object", properties: { at: int(), onto: str(), bounce: num(null, { default: 0.25, minimum: 0, maximum: 0.6 }), tumble: { type: "boolean" },
|
|
90
|
+
v: { oneOf: [{ $ref: "#/$defs/vec3" }, { const: "hand" }], description: "m/s" } }, required: ["at"], additionalProperties: false };
|
|
90
91
|
/** A free entry's keys and fall (C17) are a carry's, at any frame: said once, not twice. */
|
|
91
92
|
const AS_CARRY = { description: "as carry; keys at any frame" };
|
|
92
93
|
export const FREE_TYPES = { keys: AS_CARRY, fall: AS_CARRY };
|
|
@@ -98,6 +99,7 @@ export const OBJECT_TYPES = {
|
|
|
98
99
|
keys: { type: "array", items: { type: "object", properties: Object.fromEntries(OBJECT_KEY_FIELDS.map(f => [f, OBJECT_KEY_TYPES[f]])), required: ["f", "pos"], additionalProperties: false },
|
|
99
100
|
description: "world pose at f; hand follows" },
|
|
100
101
|
fall: { ...FALL, description: "or a list; topic objects" },
|
|
102
|
+
grasp: { enum: GRASPS }, mass: num("kg", { minimum: 0.01, maximum: 60 }),
|
|
101
103
|
follow: ref("ref"), offset: ref("vec3"), aim: { oneOf: [{ enum: ["face", "up"] }, { $ref: "#/$defs/vec3" }, { $ref: "#/$defs/ref" }], default: "up" },
|
|
102
104
|
};
|
|
103
105
|
|
|
@@ -141,6 +143,7 @@ export const FIX_TYPES = {
|
|
|
141
143
|
self: bool("arms kept out of the body's skin (wrists, elbows)", { default: true }),
|
|
142
144
|
retime: bool("speed spikes borrow frames from the neighbouring keys", { default: false }),
|
|
143
145
|
stance: bool("a standIdle stance in a scene body slides out of it (observations.placed)", { default: true }),
|
|
146
|
+
grasp: bool("fingers close on what the hand holds; it rests in the hand", { default: true }),
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
export const REF_DOCS = {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A carried object rests in the hand that holds it (#816). Without at/along it kept the transform it had against the
|
|
2
|
+
// prop origin, the finger middles, at the grab frame: session 7f15b4ae's ball rode 2.4 cm off them, the index and middle
|
|
3
|
+
// 3 cm inside it, and closing the fingers would move that origin with them. On the grasp path (a carry with no at/along,
|
|
4
|
+
// or naming a carry grasp) with fix.grasp on:
|
|
5
|
+
// frame the palm frame stands on the Hand bone: the prop origin where the bind pose has it against the hand, so
|
|
6
|
+
// closing fingers move nothing they hold (carried.js carriedPose `fixed`)
|
|
7
|
+
// seat the grab-frame offset slides along the palm normal until the palm and the knuckles just touch the object
|
|
8
|
+
// (their skin TOUCH m off it): found once per offset by bisection on the bind hand, no frame is solved for it
|
|
9
|
+
// settle over SETTLE frames from the grab the object eases from where it rests into its seat (smoothstep)
|
|
10
|
+
import { add, sub, scale, apply, T } from "./compiler/math.js";
|
|
11
|
+
import { restState } from "./compiler/skeleton.js";
|
|
12
|
+
import { SIDES } from "./compiler/pose.js";
|
|
13
|
+
import { carriedPose, compose, relative } from "./carried.js";
|
|
14
|
+
import { capsuleDepth, shapeOf } from "./shapes.js";
|
|
15
|
+
|
|
16
|
+
const TOUCH = 0.002, PALM = 0.022, KNUCKLE = 0.012, NEAR = 0.04, FAR = 0.15, STEPS = 12;
|
|
17
|
+
export const SETTLE = 4;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* True when carry entry o of body or group `id` rests in its hand: grasp physics on, and a carry grasp named, or no pinned
|
|
21
|
+
* at/along on one round body (a ball, a can: shapes.js). A box or a kit carried without a grasp keeps its grab-frame offset
|
|
22
|
+
* (a lantern hangs from its handle; sliding it onto the palm would push it off the fingers).
|
|
23
|
+
*/
|
|
24
|
+
export const seatsInHand = (spec, o, scene, id) => spec?.fix !== false && spec?.fix?.grasp !== false && !!o?.prop
|
|
25
|
+
&& (o.grasp !== undefined || (o.at === undefined && o.along === undefined && roundBody(scene, id)));
|
|
26
|
+
function roundBody(scene, id) {
|
|
27
|
+
const parts = Object.entries(scene?.objects ?? {}).filter(([k, e]) => k === id || e.ancestors?.includes(id));
|
|
28
|
+
return parts.length === 1 && shapeOf(parts[0][1]).kind !== "box";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const seats = new WeakMap();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The offset (in the palm frame of carry o's prop) that seats body or group `id` against the palm: `offset` moved along
|
|
35
|
+
* the palm normal until the palm (Hand to Middle1) and the knuckles (Index1 to Pinky1) touch it from outside.
|
|
36
|
+
*/
|
|
37
|
+
export function seatOffset(sk, spec, scene, id, o, offset) {
|
|
38
|
+
if (!seats.has(scene)) seats.set(scene, new Map());
|
|
39
|
+
const cache = seats.get(scene), key = `${id}|${o.prop}|${offset.pos.map(v => v.toFixed(6)).join(",")}`;
|
|
40
|
+
if (cache.has(key)) return cache.get(key);
|
|
41
|
+
const prop = spec.props[o.prop], side = SIDES[prop.hand], s0 = restState(sk), frame = carriedPose(sk, spec, s0, { prop: o.prop }, true);
|
|
42
|
+
const local = p => apply(T(frame.R), sub(p, frame.pos)), J = n => local(s0.P[sk.index[`${side}${n}`]]);
|
|
43
|
+
const hand = [[J("Hand"), J("HandMiddle1"), PALM], [J("HandIndex1"), J("HandPinky1"), KNUCKLE]];
|
|
44
|
+
const root = restOf(scene, id), parts = Object.entries(scene.objects).filter(([k, e]) => k === id || e.ancestors?.includes(id));
|
|
45
|
+
// slide along the palm normal (the frame's y column): out of the hand when the object is in it, in toward it when it
|
|
46
|
+
// floats off it, whichever way the depth falls; stop where the skin is TOUCH off it
|
|
47
|
+
const y = [0, 1, 0], depthAt = u => {
|
|
48
|
+
const at = { pos: add(offset.pos, scale(y, u)), R: offset.R };
|
|
49
|
+
let d = -Infinity;
|
|
50
|
+
for (const [, e] of parts) {
|
|
51
|
+
const entry = { ...e, ...compose(at, relative(root, e)) };
|
|
52
|
+
for (const [a, b, r] of hand) d = Math.max(d, capsuleDepth(entry, a, b, r));
|
|
53
|
+
}
|
|
54
|
+
return d;
|
|
55
|
+
};
|
|
56
|
+
// out of the hand is the palm's side: -n on the right hand, +n on the left (limbs.js palmAxes), the frame's y column n
|
|
57
|
+
const d0 = depthAt(0), outward = prop.hand === "l" ? 1 : -1, inside = d0 > -TOUCH;
|
|
58
|
+
// inside: the answer lies outward within FAR; floating: inward within NEAR (never past the touch)
|
|
59
|
+
let lo = 0, hi = inside ? FAR : NEAR;
|
|
60
|
+
const sign = inside ? outward : -outward;
|
|
61
|
+
if (inside || depthAt(sign * hi) > -TOUCH) {
|
|
62
|
+
for (let i = 0; i < STEPS; i++) { const mid = (lo + hi) / 2; if ((depthAt(sign * mid) > -TOUCH) === inside) lo = mid; else hi = mid; }
|
|
63
|
+
} else lo = 0;
|
|
64
|
+
const seated = { pos: add(offset.pos, scale(y, sign * (inside ? hi : lo))), R: offset.R };
|
|
65
|
+
cache.set(key, seated);
|
|
66
|
+
return seated;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The rest pose { pos, R } of a body or a group root (a group: its parts' ancestor pose). */
|
|
70
|
+
export function restOf(scene, id) {
|
|
71
|
+
const e = scene.objects[id];
|
|
72
|
+
if (e) return { pos: e.pos, R: e.R };
|
|
73
|
+
const g = Object.values(scene.objects).find(x => x.ancestors?.[0] === id);
|
|
74
|
+
return g?.ancestorPoses?.[0] ?? { pos: [0, 0, 0], R: [[1, 0, 0], [0, 1, 0], [0, 0, 1]] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How far into its seat a carried object is at absolute frame f (0 at the grab, 1 from SETTLE frames on). */
|
|
78
|
+
export function settleAt(grab, f) {
|
|
79
|
+
const t = Math.min(1, Math.max(0, (f - grab) / SETTLE));
|
|
80
|
+
return t * t * (3 - 2 * t);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The world pose between `from` and `to` (same turn: the seat only slides), t of the way. */
|
|
84
|
+
export const between = (from, to, t) => ({ pos: add(from.pos, scale(sub(to.pos, from.pos), t)), R: to.R });
|
package/src/motion3/settle.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// pivot on the support, paced by gravity: closed form and deterministic.
|
|
11
11
|
import { add, sub, scale, dot, cross, len, norm, mul, apply, axisRot, clamp } from "./compiler/math.js";
|
|
12
12
|
import { G } from "./free-motion.js";
|
|
13
|
+
import { flightTime } from "./flight.js";
|
|
13
14
|
|
|
14
15
|
/** The share of the drop height a landing spends tipping the object over an edge. */
|
|
15
16
|
export const TIP = 0.3;
|
|
@@ -54,17 +55,21 @@ export function spinOf(Ra, Rb, fps) {
|
|
|
54
55
|
return len(axis) > 0 ? scale(axis, rate) : [0, 0, 0];
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The fall through the air from `start` onto the support top: { tl (s), at(t) } with the centre on a parabola, turning at
|
|
60
|
+
* `spin`, leaving at v0 m/s (a throw, flight.js; zero: straight down, as before).
|
|
61
|
+
*/
|
|
62
|
+
export function airPath(s, start, spin, top, fps, v0 = [0, 0, 0]) {
|
|
63
|
+
const rate = len(spin), com0 = world(start, s.com), still = !v0.some(v => v !== 0);
|
|
60
64
|
const at = t => {
|
|
61
65
|
const R = rate > 0 ? mul(axisRot(spin, deg(rate * t)), start.R) : start.R;
|
|
62
|
-
|
|
66
|
+
const fall = still ? [0, -0.5 * G * t * t, 0] : [v0[0] * t, v0[1] * t - 0.5 * G * t * t, v0[2] * t];
|
|
67
|
+
return { pos: sub(add(com0, fall), apply(R, s.com)), R };
|
|
63
68
|
};
|
|
64
69
|
const h = Math.max(0, lowest(s, start) - top);
|
|
65
|
-
if (rate === 0) return { tl: Math.sqrt((2 * h) / G), at };
|
|
70
|
+
if (rate === 0) return { tl: still ? Math.sqrt((2 * h) / G) : flightTime(h, v0[1], G), at };
|
|
66
71
|
// spinning: the first sub-frame step whose lowest corner reaches the top, then halved down to the touch
|
|
67
|
-
const dt = 1 / (fps * 8), cap = Math.sqrt((2 * (h + len(sub(s.hi, s.lo)))) / G) + dt;
|
|
72
|
+
const dt = 1 / (fps * 8), cap = (still ? Math.sqrt((2 * (h + len(sub(s.hi, s.lo)))) / G) : flightTime(h + len(sub(s.hi, s.lo)), v0[1], G)) + dt;
|
|
68
73
|
let a = 0, b = dt;
|
|
69
74
|
while (b < cap && lowest(s, at(b)) > top) { a = b; b += dt; }
|
|
70
75
|
for (let i = 0; i < 24 && b - a > 1e-7; i++) { const m = (a + b) / 2; if (lowest(s, at(m)) > top) a = m; else b = m; }
|