cozyclay 3.0.2 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +22 -56
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/session-store.mjs +2 -2
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-DdDXMgZm.js +1 -0
- package/dist/assets/app-B2BpfCl2.js +5088 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-B887n77J.js +1 -0
- package/dist/assets/scene-asset-sync-COGOlF5T.css +1 -0
- package/dist/assets/scene-asset-sync-CQzEdsqu.js +13 -0
- package/dist/assets/task-board-BKaM7d-9.js +14 -0
- package/dist/assets/workflow-B8Dhzexj.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/package.json +3 -3
- package/src/App.jsx +50 -281
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-task-scopes.js +69 -0
- package/src/bus-task-targets.js +48 -0
- package/src/bus-tasks.js +181 -0
- package/src/command-bus.js +15 -40
- package/src/command-history.js +36 -0
- package/src/commands/cast.js +10 -3
- package/src/commands/motion.js +2 -1
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +13 -7
- package/src/commands/project.js +1 -1
- package/src/commands/shared.js +9 -0
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/anchors.js +33 -5
- package/src/motion3/compiler/compile.js +27 -4
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/grip.js +9 -3
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/socket-grip.js +34 -10
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/docs.js +1 -1
- package/src/motion3/fix/index.js +7 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +5 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +21 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +11 -10
- package/src/motion3/schema/types.js +5 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/basic.js +61 -13
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +13 -4
- package/src/motion3/skills/info.js +14 -1
- package/src/motion3/skills/reach-human.js +139 -0
- package/src/motion3/skills/stance.js +92 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/motion3/sockets.js +21 -4
- package/src/motion3/views.js +28 -5
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +4 -7
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +3 -7
- package/src/scenes.js +4 -5
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +3 -3
- package/src/studio-elements.js +1 -1
- package/src/studio-error-codes.js +1 -1
- package/src/studio-motion-visual.js +4 -4
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +75 -42
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +11 -25
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +7 -14
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-DbKG0NgD.js +0 -6
- package/dist/assets/app-34i-iUy1.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DYpa2nvO.js +0 -5
- package/dist/assets/first-shot-handoff-CrGZ3n_O.js +0 -1
- package/dist/assets/landing-mq5bnqFE.js +0 -1
- package/dist/assets/scene-asset-sync-7J-w5oNh.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-dUhurYUc.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
|
@@ -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;
|
|
@@ -11,6 +11,9 @@ import { sub, dot, norm, len } from "../compiler/math.js";
|
|
|
11
11
|
import { carried } from "../compiler/limbs.js";
|
|
12
12
|
import { resolveAnchor } from "../compiler/anchors.js";
|
|
13
13
|
import { CAPSULES, segPoints, sdBox } from "./geometry.js";
|
|
14
|
+
import { refDistance } from "../refs.js";
|
|
15
|
+
import { seatsInHand } from "../seat.js";
|
|
16
|
+
import { handOf } from "../object-pose.js";
|
|
14
17
|
|
|
15
18
|
export function pointOf(out, f, ref) {
|
|
16
19
|
const { s, diag, c } = out.frames[f], [kind, rest] = ref.split(/:(.*)/s);
|
|
@@ -20,6 +23,19 @@ export function pointOf(out, f, ref) {
|
|
|
20
23
|
throw new Error(`MOTION_SPEC_INVALID: check point "${ref}"`);
|
|
21
24
|
}
|
|
22
25
|
|
|
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
|
+
|
|
33
|
+
function graspPart(out, ref) {
|
|
34
|
+
const [kind, name] = ref.split(/:(.*)/s), a = kind === "anchor" ? out.ctx.spec.anchors?.[name] : null;
|
|
35
|
+
if (a?.kind !== "object" || !a.grasp) return null;
|
|
36
|
+
return out.ctx.sockets?.get(name)?.owner ?? a.object;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
const active = (out, f, when) => (Array.isArray(when) ? f + (out.f0 ?? 0) >= when[0] && f + (out.f0 ?? 0) <= when[1] : when ? (out.frames[f].c[when] ?? 0) > 0.99 : true);
|
|
24
40
|
|
|
25
41
|
function alongDir(out, f, along) {
|
|
@@ -35,7 +51,11 @@ export function specChecks(out, checks = {}) {
|
|
|
35
51
|
for (const k of checks.contacts ?? []) if (active(out, f, k.when)) {
|
|
36
52
|
const a = pointOf(out, f, k.point), b = pointOf(out, f, k.target);
|
|
37
53
|
if (!a || !b) continue;
|
|
38
|
-
|
|
54
|
+
// on a grasp's anchor the fingers close on the held part wherever the grasp turned them round it: they are on it
|
|
55
|
+
// when they are on its surface (as goals' near on an object), not on one point of it (session 7f15b4ae: 0.4 cm
|
|
56
|
+
// on the cup, read as 5.3 cm from the anchor)
|
|
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));
|
|
39
59
|
if (e > (k.tol ?? 0.01)) push("CONTACT", `${k.name}: ${(e * 100).toFixed(1)} cm apart`, f, e, k.tol ?? 0.01);
|
|
40
60
|
}
|
|
41
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
|
|
|
@@ -65,24 +65,25 @@ export const NOTES = {
|
|
|
65
65
|
anchors: ["An anchor is a named point a hand can go to: key w_<anchor>_l / w_<anchor>_r from 0 to 1 with armw_<side> 1 (the IK arm).",
|
|
66
66
|
"An anchor name may not end in _l or _r.", "object anchors (a body or a group; local in its own axes) follow the object where it is (hinges, carries): put the other hand on a carried object with one; the carrying hand's own anchor stays where it took it. prop anchors are a point of a prop the other hand holds.",
|
|
67
67
|
"segment with reach slides from t toward tMin until the arm reaches it.",
|
|
68
|
-
"An object anchor with grasp (power, pinch, hook, palm) takes the hand by a socket of its object and the compiler turns the hand: power and hook put the knuckle line along the socket's axis with the palm on it, pinch holds at the fingertips, palm presses flat on the nearest face. socket names one (object.grasp sockets the user or you stored, or a thin part by its id: a bar, a grip, a magazine); without it the socket nearest local, or the shoulder. Two hands on one carried object: each hand an object anchor with grasp on its part (a rifle's grip and magazine). reachGrip on such an anchor closes the fingers to the grasp. Never key the wrist for it; a wrist you key (wtwist/wflex) wins.",
|
|
68
|
+
"An object anchor with grasp (power, pinch, hook, palm) takes the hand by a socket of its object and the compiler turns the hand: power and hook put the knuckle line along the socket's axis with the palm on it, pinch holds at the fingertips, palm presses flat on the nearest face. socket names one (object.grasp sockets the user or you stored, or a thin part by its id: a bar, a grip, a magazine; a disc, a steering wheel, is its rim, held where nearest the shoulder, and the hand stays there as it moves); without it the socket nearest local, or the shoulder. Two hands on one carried object: each hand an object anchor with grasp on its part (a rifle's grip and magazine). reachGrip on such an anchor closes the fingers to the grasp. Never key the wrist for it; a wrist you key (wtwist/wflex) wins.",
|
|
69
69
|
"person { person: <character id>, bone, offset? } is another character's joint where its installed take has it that frame (a hand on a shoulder, a push on the chest): install that character's take first; reachGrip on it allows the hand to touch that person."],
|
|
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: "
|
|
@@ -109,6 +110,6 @@ export const NOTES = {
|
|
|
109
110
|
"object:<id> takes a body or a group (the box around its parts); in a distance a bare object:<id> means its surface.",
|
|
110
111
|
"Goals and probes take every kind; spec.checks keep joint:, prop: and anchor:."],
|
|
111
112
|
fix: [`spec.fix keys: ${FIX_KEYS.join(", ")}; false turns all off. output.fixed reports what each one changed.`,
|
|
112
|
-
"reach, slide and
|
|
113
|
+
"reach, slide, self and stance are on by default; penetration and retime are opt-in.",
|
|
113
114
|
"self keeps the arms out of the body on its fitted skin (QA BODY measures the same skin): wrist targets inside it go onto it, elbows turn about the shoulder-wrist line, a hand a little inside is pushed out. A fit's target (the mouth) is never moved; what is left is BODY, with checks.allow { part, object: <body part>, depth } for a contact meant to touch (a hand on a knee)."],
|
|
114
115
|
};
|
|
@@ -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
|
|
|
@@ -140,6 +142,8 @@ export const FIX_TYPES = {
|
|
|
140
142
|
penetration: { oneOf: [{ type: "boolean" }, { type: "number", minimum: 0, maximum: 0.05 }], default: 0, description: "push hands out of boxes up to this depth, m (true = 0.02)" },
|
|
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 }),
|
|
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 }),
|
|
143
147
|
};
|
|
144
148
|
|
|
145
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; }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The shape of a motion-scene entry to a hand (#816): a sphere and a cylinder are round, not the box they sit in. A
|
|
2
|
+
// 7.4 cm ball in its box is off by up to 30% of its radius at the corners, so fingers closing on the box would stop in
|
|
3
|
+
// the air. The shape is derived from what every entry already carries (renderer, half: scene.js), so no field is added
|
|
4
|
+
// to an entry and a saved take source, which keeps whole entries, reads the same shapes. DOM-free: the compile worker
|
|
5
|
+
// runs it. Everything else (GRAB, COLLIDE, landing) keeps the oriented box.
|
|
6
|
+
// sphere renderer "sphere" whose three halves agree within EVEN: r = their mean (an uneven one is an ellipsoid:
|
|
7
|
+
// its box)
|
|
8
|
+
// cylinder renderer "cylinder": its axis along the entry's local +Y, r the smaller cross half, h = 2 half[1]
|
|
9
|
+
// box anything else: the oriented box on the base pivot (qa/geometry.js sdBox)
|
|
10
|
+
// An entry's pos is its base pivot (the bottom centre); its centre is pos + R [0, half[1], 0].
|
|
11
|
+
import { add, sub, scale, len, apply, T } from "./compiler/math.js";
|
|
12
|
+
import { sdBox, boxNormal, segPoints } from "./qa/geometry.js";
|
|
13
|
+
|
|
14
|
+
const EVEN = 0.05;
|
|
15
|
+
|
|
16
|
+
/** { kind: "sphere", r } | { kind: "cylinder", r, h } | { kind: "box" } of a motion-scene entry. */
|
|
17
|
+
export function shapeOf(entry) {
|
|
18
|
+
const h = entry?.half;
|
|
19
|
+
if (!h) return { kind: "box" };
|
|
20
|
+
if (entry.renderer === "sphere") {
|
|
21
|
+
const r = (h[0] + h[1] + h[2]) / 3;
|
|
22
|
+
return Math.max(...h) - Math.min(...h) <= EVEN * r ? { kind: "sphere", r } : { kind: "box" };
|
|
23
|
+
}
|
|
24
|
+
if (entry.renderer === "cylinder") return { kind: "cylinder", r: Math.min(h[0], h[2]), h: 2 * h[1] };
|
|
25
|
+
return { kind: "box" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const local = (entry, p) => { const q = apply(T(entry.R), sub(p, entry.pos)); q[1] -= entry.half[1]; return q; };
|
|
29
|
+
const centre = entry => add(entry.pos, apply(entry.R, [0, entry.half[1], 0]));
|
|
30
|
+
|
|
31
|
+
/** Signed distance (m) from p to the entry's shape: negative inside. */
|
|
32
|
+
export function signedDistance(entry, p) {
|
|
33
|
+
const s = shapeOf(entry);
|
|
34
|
+
if (s.kind === "sphere") return len(sub(p, centre(entry))) - s.r;
|
|
35
|
+
if (s.kind === "cylinder") {
|
|
36
|
+
const q = local(entry, p), radial = Math.hypot(q[0], q[2]) - s.r, axial = Math.abs(q[1]) - s.h / 2;
|
|
37
|
+
return Math.hypot(Math.max(radial, 0), Math.max(axial, 0)) + Math.min(Math.max(radial, axial), 0);
|
|
38
|
+
}
|
|
39
|
+
return sdBox(p, entry);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Outward unit normal of the entry's shape at p (the nearest surface's from inside). */
|
|
43
|
+
export function normalAt(entry, p) {
|
|
44
|
+
const s = shapeOf(entry);
|
|
45
|
+
if (s.kind === "sphere") { const d = sub(p, centre(entry)), l = len(d); return l > 1e-12 ? scale(d, 1 / l) : apply(entry.R, [0, 1, 0]); }
|
|
46
|
+
if (s.kind === "cylinder") {
|
|
47
|
+
const q = local(entry, p), rq = Math.hypot(q[0], q[2]), radial = rq - s.r, axial = Math.abs(q[1]) - s.h / 2;
|
|
48
|
+
const out = rq > 1e-12 ? [q[0] / rq, 0, q[2] / rq] : [1, 0, 0], cap = [0, Math.sign(q[1]) || 1, 0];
|
|
49
|
+
let n;
|
|
50
|
+
if (radial <= 0 && axial <= 0) n = radial > axial ? out : cap;
|
|
51
|
+
else n = add(scale(out, Math.max(radial, 0)), scale(cap, Math.max(axial, 0)));
|
|
52
|
+
const l = len(n);
|
|
53
|
+
return apply(entry.R, l > 1e-12 ? scale(n, 1 / l) : out);
|
|
54
|
+
}
|
|
55
|
+
return boxNormal(p, entry);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function closestPoint(entry, p) {
|
|
59
|
+
return sub(p, scale(normalAt(entry, p), signedDistance(entry, p)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How far a capsule (segment a-b, `radius` m) goes into the entry's shape, m: positive is the depth of its skin inside,
|
|
64
|
+
* negative the gap between its skin and the surface. Sampled on the segment as the QA capsules are (segPoints).
|
|
65
|
+
*/
|
|
66
|
+
export function capsuleDepth(entry, a, b, radius) {
|
|
67
|
+
let depth = -Infinity;
|
|
68
|
+
for (const p of segPoints(a, b)) depth = Math.max(depth, radius - signedDistance(entry, p));
|
|
69
|
+
return depth;
|
|
70
|
+
}
|
|
@@ -5,22 +5,36 @@ import { walk } from "../compiler/walk.js";
|
|
|
5
5
|
import { keyBook, skillError } from "./book.js";
|
|
6
6
|
import { walkEnd } from "./info.js";
|
|
7
7
|
import { GRASP_FINGERS, GRASP_POINTS } from "../compiler/vocab.js";
|
|
8
|
+
import { clearStance, reachStance } from "./stance.js";
|
|
9
|
+
import { CLOSE, GAZE, gazeAt, headKeyed, earlyTransport, holdsOpen, transportKeys } from "./reach-human.js";
|
|
8
10
|
|
|
9
11
|
/** The fingers' point a reachGrip fits onto its anchor: the finger middles, or where a grasp holds the thing (the palm is -n on the right hand). */
|
|
10
12
|
const graspPoint = (grasp, k) => (GRASP_POINTS[grasp] ? GRASP_POINTS[grasp].map((v, i) => (i === 2 && k === "r" ? -v : v)) : [0, 0, 0]);
|
|
11
13
|
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* standIdle({ at: [x, z], heading = 0, until }): a neutral standing pose, arms down (FK). Without `at` (and no root
|
|
16
|
+
* keyed) before a reachGrip it stands within that hand's reach of the anchor, facing it; a stance in a scene body
|
|
17
|
+
* slides out of it (stance.js). Either says so in `placed`.
|
|
18
|
+
*/
|
|
13
19
|
export function standIdle(ctx) {
|
|
14
20
|
const { sk, args, state } = ctx, I = sk.index, P = sk.restWorldPos, ank = P[I.LeftFoot][1];
|
|
15
|
-
|
|
16
|
-
if (!base)
|
|
21
|
+
let h = args.heading ?? state.yaw ?? 0, base = args.at ?? (state.root ? [state.root[0], state.root[2]] : null), placed = null;
|
|
22
|
+
if (!base) {
|
|
23
|
+
const auto = reachStance(ctx);
|
|
24
|
+
if (!auto) skillError(ctx.tag, "needs args.at: [x, z] (no root is keyed before f0), or a reachGrip after it to stand within reach of");
|
|
25
|
+
({ at: base, heading: h } = auto);
|
|
26
|
+
placed = { at: base, heading: h, why: `within reach of anchor ${auto.anchor}` };
|
|
27
|
+
} else if (args.at && ctx.place !== false) {
|
|
28
|
+
const out = clearStance(sk, ctx.scene, [base[0], base[base.length - 1]], h);
|
|
29
|
+
if (out) { base = out.at; placed = { at: base, heading: h, why: `moved ${out.moved} m out of a scene body` }; }
|
|
30
|
+
}
|
|
17
31
|
const c = [base[0], 0, base[base.length - 1]], R = ry(h), foot = v => { const p = add(c, apply(R, v)); p[1] = ank; return p; };
|
|
18
32
|
const set = { root: [c[0], P[I.Hips][1] - 0.01, c[2]], yaw: h, pitch: 0, roll: 0, spine_yaw: 0, spine_pitch: 0, spine_roll: 0, head_yaw: 0, head_pitch: 0,
|
|
19
33
|
foot_l: foot([0.09, 0, -0.03]), foot_r: foot([-0.09, 0, 0.03]), fyaw_l: h, fyaw_r: h, fpitch_l: 0, fpitch_r: 0, knee_l: [0.05, 0, 1], knee_r: [-0.05, 0, 1],
|
|
20
34
|
armw_l: 0, armw_r: 0, aswing_l: 0, aswing_r: 0, abend_l: 0, abend_r: 0, arm_hang: 1 };
|
|
21
35
|
const keys = [{ f: ctx.f0, set }];
|
|
22
36
|
if (args.until !== undefined) keys.push({ f: args.until, set: structuredClone(set) });
|
|
23
|
-
return { keys, end: args.until ?? ctx.f0 };
|
|
37
|
+
return { keys, end: args.until ?? ctx.f0, ...(placed ? { placed } : {}) };
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
@@ -52,6 +66,23 @@ export function walkTo(ctx) {
|
|
|
52
66
|
return { keys, end };
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
/** base when `props` has no other prop of that name (the same one again is fine), else base_<side>, base_<side>2, ... */
|
|
70
|
+
function freeName(props = {}, base, k, mine) {
|
|
71
|
+
const taken = n => props[n] && JSON.stringify(props[n]) !== JSON.stringify(mine);
|
|
72
|
+
if (!taken(base)) return base;
|
|
73
|
+
let name = `${base}_${k}`;
|
|
74
|
+
for (let i = 2; taken(name); i++) name = `${base}_${k}${i}`;
|
|
75
|
+
return name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What a hand on `obj` may touch: the object, or for a group (an Empty, no body of its own) every part under it. */
|
|
79
|
+
const touchable = (scene, obj) => {
|
|
80
|
+
const objects = scene?.objects ?? {};
|
|
81
|
+
if (!obj || objects[obj] || obj.startsWith("person:")) return obj ? [obj] : [];
|
|
82
|
+
const parts = Object.keys(objects).filter(k => objects[k].ancestors?.includes(obj));
|
|
83
|
+
return parts.length ? parts : [obj];
|
|
84
|
+
};
|
|
85
|
+
|
|
55
86
|
/**
|
|
56
87
|
* reachGrip({ anchor, side, reach = 20, hold = 22, release = 12, elbow, fingers = "grip",
|
|
57
88
|
* define, orient = true }): the hand goes to the anchor (defined elsewhere, or by `define`) over
|
|
@@ -67,23 +98,40 @@ export function reachGrip(ctx) {
|
|
|
67
98
|
// an anchor that takes a grasp closes the hand to that grasp's fingers (../sockets.js)
|
|
68
99
|
const grasp = (args.define ?? ctx.anchors?.[name])?.grasp, preset = args.fingers ?? GRASP_FINGERS[grasp] ?? "grip";
|
|
69
100
|
const w = `w_${name}_${k}`, fg = `fingers_${k}`, armw = `armw_${k}`, el = `elbow_${k}`, wasFk = !(state[armw] > 0.5);
|
|
70
|
-
// a take that ends
|
|
71
|
-
|
|
101
|
+
// a take that ends before the hand lets go cuts the hold or the release: the grip holds to the last frame (a hold
|
|
102
|
+
// asked past the end was refused, session 7f15b4ae: "needs 92 frames from f10 (to f102), past the last frame f85")
|
|
103
|
+
const last = ctx.frames - 1, cut = ctx.f0 + reach <= last && ctx.f0 + reach + hold + release > last;
|
|
104
|
+
const held = Math.min(reach + hold, last - ctx.f0);
|
|
72
105
|
for (const [c, v] of [[w, 0], [fg, { [preset]: 0 }], [armw, 0]]) if (state[c] === undefined) state[c] = v;
|
|
106
|
+
// the head looks at what the hand goes for unless the keys already move it (reach-human.js); read before the keys move state
|
|
107
|
+
// the reach's own view: a thing that spec.objects moves before the hand gets there is not at rest (reach-human.js)
|
|
108
|
+
const end = reach + hold + release, rc = { ...ctx, until: ctx.f0 + reach };
|
|
109
|
+
const look = !headKeyed(ctx.keys ?? [], ctx.f0, ctx.f0 + end) && gazeAt(rc, args.define ?? ctx.anchors?.[name], state);
|
|
110
|
+
const head = look ? { head_yaw: state.head_yaw ?? 0, head_pitch: state.head_pitch ?? 0 } : null;
|
|
73
111
|
const b = keyBook(state, ctx.f0);
|
|
74
|
-
b.hold(0, w, fg, armw, el);
|
|
112
|
+
b.hold(0, w, fg, armw, el, ...(look ? ["head_yaw", "head_pitch"] : []));
|
|
113
|
+
// the transport's early-peaked bell; on a thing the open hand clears, the fingers stay open to CLOSE of the reach, then close
|
|
114
|
+
// (a reach for a moving point, or below the knees, keeps the plain ease: reach-human.js earlyTransport)
|
|
115
|
+
const target = args.define ?? ctx.anchors?.[name], w0 = state[w], a0 = state[armw], lerp = (a, z, t) => a + (z - a) * t;
|
|
116
|
+
if (earlyTransport(rc, target)) for (const [f, t, ta] of transportKeys(reach)) b.at(f, { [w]: lerp(w0, 1, t), [armw]: lerp(a0, 1, ta) });
|
|
117
|
+
const close = Math.round(CLOSE * reach);
|
|
118
|
+
if (close > 0 && close < reach && holdsOpen(rc, target, state)) b.hold(close, fg);
|
|
119
|
+
if (look) b.at(Math.max(1, Math.round(GAZE * reach)), look);
|
|
75
120
|
b.at(reach, { [w]: 1, [fg]: { ...state[fg], [preset]: 1 }, [armw]: 1, ...(args.elbow ? { [el]: args.elbow } : {}) });
|
|
76
|
-
b.hold(
|
|
77
|
-
if (!cut) b.at(
|
|
78
|
-
|
|
121
|
+
b.hold(held, w, fg, armw, el, ...(look ? ["head_yaw", "head_pitch"] : []));
|
|
122
|
+
if (!cut) b.at(end, { [w]: 0, [fg]: { ...state[fg], [preset]: 0 }, ...(wasFk ? { [armw]: 0 } : {}), ...(head ?? {}) });
|
|
123
|
+
// the fingers' prop is <anchor>Grip, unless the spec (or another skill) has a prop of that name of its own: its points
|
|
124
|
+
// would replace these (session 7f15b4ae: props.ballGrip for a carry broke reachGrip on anchor "ball")
|
|
125
|
+
const S = k === "l" ? "Left" : "Right", fingersProp = { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: graspPoint(grasp, k) } };
|
|
126
|
+
const prop = freeName(ctx.props, `${name}Grip`, k, fingersProp);
|
|
79
127
|
const def = args.define ? { [name]: args.define } : {};
|
|
80
128
|
// the hand may touch what it grips: an object, or the other person (people.js) whose joint the anchor is on
|
|
81
129
|
const a = args.define ?? ctx.anchors?.[name], obj = a?.kind === "object" ? a.object : a?.kind === "person" ? `person:${a.person}` : null;
|
|
82
130
|
return {
|
|
83
|
-
keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut:
|
|
84
|
-
props: { [prop]:
|
|
131
|
+
keys: b.keys, end: cut ? last : ctx.f0 + reach + hold + release, ...(cut ? { cut: `${held < reach + hold ? "hold" : "release"} cut at f${last} (hold to the end)` } : {}), anchors: def,
|
|
132
|
+
props: { [prop]: fingersProp },
|
|
85
133
|
fits: [{ side: k, anchor: name, effector: { prop, point: "fingers" }, ...(args.orient === false ? { orient: false } : {}) }],
|
|
86
134
|
checks: { contacts: [{ name: `fingers on ${name}`, point: `prop:${prop}.fingers`, target: `anchor:${name}`, tol: 0.04, when: w }],
|
|
87
|
-
allow: obj
|
|
135
|
+
allow: touchable(ctx.scene, obj).map(object => ({ part: "hand", object, depth: 0.04 })) },
|
|
88
136
|
};
|
|
89
137
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// catchObject: a hand meets a flying thing where it can reach and gives with it (#816). The thing flies as a free entry
|
|
2
|
+
// does, objects.<id>.fall { at, v } from its pose at `at` (flight.js); the skill samples that flight and picks the first
|
|
3
|
+
// frame its point is in front of the chest (FRONT m ahead, between the hips and the head) and SHELL m from the catching
|
|
4
|
+
// shoulder, no earlier than the hand can get there from f0 (REACH frames). The open hand arrives ARRIVE frames before it,
|
|
5
|
+
// the finger middles on the point (a fit on a world anchor there); the fingers close and the carry takes it at
|
|
6
|
+
// that frame (fills: prop, grab, grasp), so it rests in the hand (seat.js) and the fingers close on it (finger-close.js).
|
|
7
|
+
// Then the hand gives with it: along the incoming velocity by clamp(0.08 + 0.02 x mass x speed, 0.08, 0.15) m over ABSORB
|
|
8
|
+
// frames, slowing to a stop, and eases back. side "both", or a thing over HEAVY kg, adds the left hand on its left side
|
|
9
|
+
// along the same path (world anchors: the right hand holds the carry, the left meets it). A flight that never comes within
|
|
10
|
+
// reach is refused, naming its nearest frame and point; a hand that comes too late (f0 past the reach) misses: QA CATCH.
|
|
11
|
+
import { add, sub, scale, apply, ry, len, norm, dot } from "../compiler/math.js";
|
|
12
|
+
import { freePoseAt } from "../free-motion.js";
|
|
13
|
+
import { massOf } from "../mass.js";
|
|
14
|
+
import { skillError } from "./book.js";
|
|
15
|
+
import { transportKeys } from "./reach-human.js";
|
|
16
|
+
|
|
17
|
+
const SHELL = [0.35, 0.65], FRONT = 0.15, REACH = 10, LONG = 20, ARRIVE = 2, ABSORB = 6, BACK = 8, HEAVY = 1;
|
|
18
|
+
const r3 = v => v.map(x => Math.round(x * 1000) / 1000);
|
|
19
|
+
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
|
|
20
|
+
|
|
21
|
+
/** The shoulder of side S and the body's forward on the keyed root and yaw of state. */
|
|
22
|
+
function bodyAt(sk, state, S) {
|
|
23
|
+
const P = sk.restWorldPos, hips = state.root ?? P[sk.index.Hips], R = ry(state.yaw ?? 0);
|
|
24
|
+
return { hips, shoulder: add(hips, apply(R, sub(P[sk.index[`${S}Arm`]], P[sk.index.Hips]))), head: add(hips, apply(R, sub(P[sk.index.Head], P[sk.index.Hips]))), fwd: apply(R, [0, 0, 1]) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function catchObject(ctx) {
|
|
28
|
+
const { args, state, sk, scene, tag } = ctx, id = args.object, side = args.side ?? "r", o = ctx.objects?.[id], fps = ctx.fps ?? 24;
|
|
29
|
+
if (side !== "r" && side !== "both") skillError(tag, `args.side must be "r" or "both", got ${JSON.stringify(side)}`);
|
|
30
|
+
const fall = Array.isArray(o?.fall) ? o.fall[0] : o?.fall;
|
|
31
|
+
if (!Array.isArray(fall?.v)) skillError(tag, `args.object "${id}" must fly: objects.${id}.fall { at, v: [vx, vy, vz] }`);
|
|
32
|
+
const body = bodyAt(sk, state, "Right"), flight = { spec: { objects: { [id]: { fall: o.fall } }, fps }, scene, timeMap: null };
|
|
33
|
+
const at = f => freePoseAt(flight, id, f)?.pos;
|
|
34
|
+
// the first frame of the flight in front of the chest within the shell around the shoulder; else the nearest one
|
|
35
|
+
let pick = null, near = null;
|
|
36
|
+
for (let f = fall.at + 1; f < ctx.frames; f++) {
|
|
37
|
+
const p = at(f), d = len(sub(p, body.shoulder)), ahead = dot(sub(p, body.hips), body.fwd), off = Math.max(0, SHELL[0] - d, d - SHELL[1], FRONT - ahead, body.hips[1] - 0.1 - p[1], p[1] - body.head[1]);
|
|
38
|
+
if (!near || off < near.off) near = { f, p, off };
|
|
39
|
+
if (off === 0) { pick = { f, p }; break; }
|
|
40
|
+
}
|
|
41
|
+
if (!pick) skillError(tag, `${id} never comes within reach of the right hand (in front of the chest, ${SHELL[0]}-${SHELL[1]} m from the shoulder): nearest at f${near.f}, [${r3(near.p).join(", ")}], ${near.off.toFixed(2)} m out; move the body (standIdle at) or the throw`);
|
|
42
|
+
// the hand needs REACH frames from f0: a later pick it would get to in time; else it comes late to where the thing was
|
|
43
|
+
const contact = Math.max(pick.f, ctx.f0 + REACH + ARRIVE), p = pick.p;
|
|
44
|
+
const incoming = scale(sub(at(pick.f), at(pick.f - 1)), fps), speed = len(incoming), mass = massOf({ objects: ctx.objects }, scene, id);
|
|
45
|
+
const give = clamp(0.08 + 0.02 * mass * speed, 0.08, 0.15), u = norm(incoming), ease = t => 1 - (1 - t) ** 3;
|
|
46
|
+
const path = [p, ...Array.from({ length: ABSORB }, (_, i) => add(p, scale(u, give * ease((i + 1) / ABSORB)))), add(p, scale(u, give * 0.4))];
|
|
47
|
+
const start = Math.max(ctx.f0, contact - ARRIVE - LONG), both = side === "both" || mass > HEAVY;
|
|
48
|
+
// the left hand (both hands, or a heavy thing) on its left side, its finger middles across the thing from the right's
|
|
49
|
+
const radius = Math.max(0, ...Object.values(scene.objects).filter(e => e === scene.objects[id] || e.ancestors?.includes(id)).map(e => Math.max(...e.half)));
|
|
50
|
+
const left = apply(ry(state.yaw ?? 0), [1, 0, 0]), part = { keys: [], end: contact + ABSORB + BACK, props: {}, anchors: {}, fits: [], channels: [],
|
|
51
|
+
fills: { [id]: { prop: `${id}Catch`, grab: contact, grasp: "spherical" } }, catch: { contact, point: r3(p), speed: Math.round(speed * 100) / 100, give: Math.round(give * 1000) / 1000, hands: both ? 2 : 1 } };
|
|
52
|
+
for (const k of both ? ["r", "l"] : ["r"]) {
|
|
53
|
+
const S = k === "l" ? "Left" : "Right", prop = k === "r" ? `${id}Catch` : `${id}CatchL`, off = k === "r" ? [0, 0, 0] : scale(left, 2 * radius + 0.04);
|
|
54
|
+
const armw = `armw_${k}`, fg = `fingers_${k}`, open = { ...state[fg], spherical: 0 }, shut = { ...state[fg], spherical: 1 };
|
|
55
|
+
// the finger middles (the prop's origin) on the point at the catch, then on each point of the give: world anchors
|
|
56
|
+
// with a fit each, the weight handed from one to the next frame by frame (the fits solve the hand onto them exactly)
|
|
57
|
+
const names = path.map((_, i) => `${prop}${i}`), w = i => `w_${names[i]}_${k}`;
|
|
58
|
+
part.keys.push(
|
|
59
|
+
// the elbow down and out (body axes), so it swings out round the side, not in across the body
|
|
60
|
+
{ f: start, set: { [armw]: state[armw] ?? 0, [w(0)]: 0, [fg]: open, [`elbow_${k}`]: k === "r" ? [-1, -0.5, -0.5] : [1, -0.5, -0.5] } },
|
|
61
|
+
// the reach's early-peaked transport, the arm's weight ahead of the aim's (reach-human.js; as reachGrip)
|
|
62
|
+
...transportKeys(contact - ARRIVE - start).map(([f, t, ta]) => ({ f: start + f, set: { [w(0)]: t, [armw]: (state[armw] ?? 0) + (1 - (state[armw] ?? 0)) * ta } })),
|
|
63
|
+
{ f: contact - ARRIVE, set: { [armw]: 1, [w(0)]: 1, [fg]: open } },
|
|
64
|
+
{ f: contact, set: { [w(0)]: 1, [fg]: shut } },
|
|
65
|
+
...path.slice(1).flatMap((_, j) => { const i = j + 1, f = i <= ABSORB ? contact + i : contact + ABSORB + BACK; return [{ f: i <= ABSORB ? f - 1 : contact + ABSORB, set: { [w(i - 1)]: 1, [w(i)]: 0 } }, { f, set: { [w(i - 1)]: 0, [w(i)]: 1 } }]; }),
|
|
66
|
+
);
|
|
67
|
+
part.props[prop] = { hand: k, origin: [`${S}HandIndex2`, `${S}HandMiddle2`], points: { fingers: [0, 0, 0] } };
|
|
68
|
+
names.forEach((n, i) => { part.anchors[n] = { kind: "world", p: r3(add(path[i], off)) }; part.fits.push({ side: k, anchor: n, effector: { prop, point: "fingers" } }); part.channels.push(w(i)); });
|
|
69
|
+
}
|
|
70
|
+
return part;
|
|
71
|
+
}
|