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
|
@@ -7,13 +7,16 @@ import { stateAt } from "../compiler/walk.js";
|
|
|
7
7
|
import { didYouMean, suggestField } from "../compiler/vocab.js";
|
|
8
8
|
import { USE_FIELDS } from "../schema/fields.js";
|
|
9
9
|
import { SKILL_INFO } from "./info.js";
|
|
10
|
+
import { fixOptions } from "../fix/index.js";
|
|
10
11
|
import { standIdle, walkTo, reachGrip } from "./basic.js";
|
|
11
12
|
import { sitInSeat, exitCar } from "./car.js";
|
|
12
13
|
import { smokeCigarette } from "./smoke.js";
|
|
13
14
|
import { flip } from "./flip.js";
|
|
14
15
|
import { hitReaction } from "./impact.js";
|
|
16
|
+
import { throwObject } from "./throw.js";
|
|
17
|
+
import { catchObject } from "./catch.js";
|
|
15
18
|
|
|
16
|
-
const SKILLS = { standIdle, sitInSeat, exitCar, walkTo, reachGrip, smokeCigarette, flip, hitReaction };
|
|
19
|
+
const SKILLS = { standIdle, sitInSeat, exitCar, walkTo, reachGrip, smokeCigarette, flip, hitReaction, throwObject, catchObject };
|
|
17
20
|
export const SKILL_NAMES = Object.keys(SKILLS);
|
|
18
21
|
const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
|
|
19
22
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -52,7 +55,7 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
|
52
55
|
if (!spec.use?.length) return { spec, used: [] };
|
|
53
56
|
if (!Array.isArray(spec.use)) bad("spec.use must be an array of { skill, args, f0 }");
|
|
54
57
|
const own = (spec.keys ?? []).map((k, i) => ({ ...k, src: `keys[${i}]` }));
|
|
55
|
-
const merged = { anchors: {}, props: {}, objects: {}, fits: [], checks: {}, channels: [] }, skillKeys = [], used = [];
|
|
58
|
+
const merged = { anchors: {}, props: {}, objects: {}, fits: [], checks: {}, channels: [], fills: {}, goals: [] }, skillKeys = [], used = [];
|
|
56
59
|
spec.use.forEach((u, i) => {
|
|
57
60
|
const tag = `use[${i}] ${u?.skill}`;
|
|
58
61
|
if (!SKILLS[u?.skill]) bad(`use[${i}].skill "${u?.skill}" is not a skill${didYouMean(String(u?.skill), SKILL_NAMES)} (skills: ${SKILL_NAMES.join(", ")})`);
|
|
@@ -60,7 +63,8 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
|
60
63
|
argsOk(u, i, lenient);
|
|
61
64
|
if (!Number.isInteger(u.f0) || u.f0 < 0 || u.f0 > spec.frames - 1) bad(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
|
|
62
65
|
const keys = [...skillKeys, ...own];
|
|
63
|
-
const ctx = { sk, scene, frames: spec.frames, auto, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys,
|
|
66
|
+
const ctx = { sk, scene, frames: spec.frames, fps: spec.fps ?? 24, auto, props: { ...merged.props, ...spec.props }, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys,
|
|
67
|
+
use: spec.use, index: i, place: fixOptions(spec.fix).stance, state: structuredClone(stateAt(keys, u.f0)), anchors: { ...merged.anchors, ...spec.anchors }, objects: { ...merged.objects, ...spec.objects } };
|
|
64
68
|
const part = SKILLS[u.skill](ctx), last = Math.max(part.end ?? u.f0, ...part.keys.map(k => k.f));
|
|
65
69
|
if (last > spec.frames - 1) bad(`${tag} needs ${last - u.f0} frames from f${u.f0} (to f${last}), past the last frame f${spec.frames - 1}; set frames >= ${last + 1} or frames: "auto"`);
|
|
66
70
|
skillKeys.push(...part.keys.map(k => ({ ...k, src: k.src ?? tag })));
|
|
@@ -70,7 +74,10 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
|
70
74
|
for (const fit of part.fits ?? []) if (!merged.fits.some(x => same(x, fit))) merged.fits.push(fit);
|
|
71
75
|
for (const ch of part.channels ?? []) if (!merged.channels.includes(ch)) merged.channels.push(ch);
|
|
72
76
|
mergeChecks(merged.checks, part.checks);
|
|
73
|
-
|
|
77
|
+
// fields a skill fills into an entry when it leaves them out (throwObject: a carry's release and fall)
|
|
78
|
+
for (const [id, f] of Object.entries(part.fills ?? {})) merged.fills[id] = { ...merged.fills[id], ...f };
|
|
79
|
+
merged.goals.push(...(part.goals ?? []));
|
|
80
|
+
used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}), ...(part.placed ? { placed: part.placed } : {}) });
|
|
74
81
|
});
|
|
75
82
|
const out = { ...spec, keys: [...skillKeys, ...own], anchors: { ...merged.anchors, ...spec.anchors }, props: { ...merged.props, ...spec.props },
|
|
76
83
|
objects: { ...merged.objects, ...spec.objects } };
|
|
@@ -80,6 +87,8 @@ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
|
|
|
80
87
|
out.fits = [...merged.fits.filter(f => !mine.some(m => m.side === f.side && m.anchor === f.anchor)), ...mine];
|
|
81
88
|
mergeChecks(merged.checks, spec.checks);
|
|
82
89
|
out.checks = merged.checks;
|
|
90
|
+
for (const [id, f] of Object.entries(merged.fills)) if (out.objects[id]) out.objects[id] = { ...f, ...out.objects[id] };
|
|
91
|
+
if (merged.goals.length) out.goals = [...merged.goals, ...(spec.goals ?? [])];
|
|
83
92
|
delete out.use;
|
|
84
93
|
return { spec: out, used };
|
|
85
94
|
}
|
|
@@ -19,7 +19,7 @@ const walkArgs = Object.fromEntries(WALK_FIELDS.filter(f => f !== "type" && f !=
|
|
|
19
19
|
|
|
20
20
|
export const SKILL_INFO = {
|
|
21
21
|
standIdle: {
|
|
22
|
-
args: { at: xz("[x, z] world m; default the root keyed before f0"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
|
|
22
|
+
args: { at: xz("[x, z] world m; default the root keyed before f0, else within reach of the next reachGrip's anchor (output.observations.placed); a stance in a body slides out of it"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
|
|
23
23
|
needs: "args.at, or a root keyed before f0", frames: "1 (to args.until when given)",
|
|
24
24
|
adds: ["keys: a standing pose, FK arms down"], example: { f0: 0, args: { at: [0, 0], heading: 0 } },
|
|
25
25
|
place: { at: 0.03 },
|
|
@@ -77,4 +77,17 @@ export const SKILL_INFO = {
|
|
|
77
77
|
speed: num("launch speed, m/s (a shove 2, a car at 30 km/h about 8)", { default: 4, exclusiveMinimum: 0 }), land: { enum: ["back", "front"], description: "default: back when hit from the front" } },
|
|
78
78
|
needs: "a pose; dir or from", frames: "~40", adds: ["keys: thrown to rest"], example: { f0: 10, args: { dir: [0, -1], speed: 3 } },
|
|
79
79
|
},
|
|
80
|
+
throwObject: {
|
|
81
|
+
args: { object: str("the carried object's id (objects.<id>.prop on the hand's prop)"), side: side("the throwing hand (default r)"), to: { type: "array", items: { type: "number" }, minItems: 3, maxItems: 3, description: "[x, y, z] world m: where it lands" },
|
|
82
|
+
arc: { enum: ["low", "high"], description: "launch angle: low 20 deg, high 50 deg (default low)" } },
|
|
83
|
+
required: ["object", "to"], needs: "the object in that hand (its objects entry on the hand's prop)", frames: "24 (wind-up, swing, release at f0+18, follow-through)",
|
|
84
|
+
adds: ["keys: wind-up, swing, follow-through", "objects.<id>: release and fall { v: \"hand\" } when the entry leaves them out", "goals: <id> lands at the target"],
|
|
85
|
+
example: { f0: 40, args: { object: "ball", to: [0, 0, 5] } },
|
|
86
|
+
},
|
|
87
|
+
catchObject: {
|
|
88
|
+
args: { object: str("the flying object's id (objects.<id>.fall { at, v })"), side: { enum: ["r", "both"], description: "the right hand, or both (a thing over 1 kg takes both)" } },
|
|
89
|
+
required: ["object"], needs: "the object flying: objects.<id>.fall { at, v }", frames: "to the catch (the first frame in reach, at least 10 from f0) + 14",
|
|
90
|
+
adds: ["keys: reach, close, give with it", "objects.<id>: prop, grab, grasp when the entry leaves them out", "props.<id>Catch"],
|
|
91
|
+
example: { f0: 0, args: { object: "ball" } },
|
|
92
|
+
},
|
|
80
93
|
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// How a person's reach to grasp unfolds in time (reachGrip's keys), from the prehension literature:
|
|
2
|
+
// transport the wrist's speed is a single bell whose peak comes early, at about a third of the movement, and the
|
|
3
|
+
// longer deceleration (about two thirds) is the slow approach onto the object (time to peak velocity 33 %
|
|
4
|
+
// of movement time: Paulignan et al., Exp Brain Res 1991, doi:10.1007/BF00231858). A minimum-jerk bell
|
|
5
|
+
// (Flash & Hogan 1985) on time warped by t^WARP puts the peak at 0.5^(1/WARP) of the reach.
|
|
6
|
+
// grasp the hand stays open wider than the object through the transport and closes onto it in the slow
|
|
7
|
+
// approach: the maximum grip aperture comes at about two thirds to three quarters of the movement (Jeannerod,
|
|
8
|
+
// J Mot Behav 1984; Hu et al., Exp Brain Res 2005), and it scales with the object's size (Marteniuk et al.,
|
|
9
|
+
// Hum Mov Sci 1990), a couple of centimetres wider than the object (MARGIN). These rigs' relaxed hands are
|
|
10
|
+
// already open (thumb tip to index tip: Y Bot 11.5 cm, X Bot 9.7 cm) and there is no wider preset: a thing
|
|
11
|
+
// that fits that opening with the margin (a can, a cup) is reached with the hand held open to CLOSE of the
|
|
12
|
+
// reach, then closed on it; a wider one closes along the reach as before, or the open fingers would sweep
|
|
13
|
+
// into it on the way (a 10 cm box: 6 cm deep). So does a thing that sits more than OVER m in from the near
|
|
14
|
+
// edge of what it stands on: the open fingertips hang below the hand and dragged across the table top on the
|
|
15
|
+
// way (a cup 12 cm in, bench S8). CLOSE is 0.4, earlier than a person's: the hand comes straight in, and the
|
|
16
|
+
// open fingertips (about 8 cm past the finger middles) went into a cup wrapped from the side when they
|
|
17
|
+
// stayed open later (1.5 cm at 0.7); a hand that plans an arc around the thing is the next step.
|
|
18
|
+
// gaze the eyes go to the grasp point before the hand gets there and stay on it until the hand has it
|
|
19
|
+
// (Johansson et al., J Neurosci 2001;21:6917). The rigs have no eyes: the head turns HEAD_SHARE of the
|
|
20
|
+
// way, done by GAZE of the reach, held through the hold, back by the end of the release.
|
|
21
|
+
import { add, apply, len, sub } from "../compiler/math.js";
|
|
22
|
+
import { restState } from "../compiler/skeleton.js";
|
|
23
|
+
import { fingers } from "../compiler/limbs.js";
|
|
24
|
+
import { sdBox } from "../qa/geometry.js";
|
|
25
|
+
|
|
26
|
+
const WARP = 0.65, ARM_WARP = 0.75, HEAD_SHARE = 0.75, MARGIN = 0.025, KNEE = 0.1, OVER = 0.08, BELOW = 0.12;
|
|
27
|
+
export const CLOSE = 0.4, GAZE = 0.3;
|
|
28
|
+
const TRANSPORT = [0.15, 0.3, 0.45, 0.6, 0.7, 0.8, 0.9];
|
|
29
|
+
const minJerk = t => t * t * t * (10 - 15 * t + 6 * t * t);
|
|
30
|
+
/** The share of the way the hand has come at fraction t of the reach. */
|
|
31
|
+
// two phases: the fast transport covers NEAR of the way by SLOW of the time, the low-velocity approach the rest while the
|
|
32
|
+
// fingers close (about half the movement time is that slow approach after peak deceleration, Paulignan et al. 1991)
|
|
33
|
+
const NEAR = 0.9, SLOW = 0.7;
|
|
34
|
+
const transport = (t, warp) => {
|
|
35
|
+
const u = Math.min(1, Math.max(0, t));
|
|
36
|
+
return u < SLOW ? NEAR * minJerk(Math.pow(u / SLOW, warp)) : NEAR + (1 - NEAR) * minJerk((u - SLOW) / (1 - SLOW));
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Inner keys of the transport for a reach of n frames: [frame from f0, the target's share, the arm's IK share]. The arm's
|
|
40
|
+
* IK weight follows a milder bell (ARM_WARP): the hand is the blend of the two, and the target's bell alone on an arm
|
|
41
|
+
* blending in at its plain ease peaked at mid-reach, while both at WARP pushed a cup reach past the QA's one-frame pop.
|
|
42
|
+
*/
|
|
43
|
+
export const transportKeys = n => [...new Map(TRANSPORT.map(t => Math.round(t * n)).filter(f => f > 0 && f < n).map(f => [f, [transport(f / n, WARP), transport(f / n, ARM_WARP)]])).entries()].map(([f, [t, a]]) => [f, t, a]);
|
|
44
|
+
|
|
45
|
+
const DEG = 180 / Math.PI, clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
46
|
+
/**
|
|
47
|
+
* True when the reach to anchor a takes the early-peaked transport: a thing at rest (a world point or a scene object's;
|
|
48
|
+
* a reach to a body, a bone or a prop chases a moving point) above the knees (KNEE m over the rest knee). A person
|
|
49
|
+
* reaching lower bends or crouches, which these keys do not plan: the arm alone, sped up early, swept the thigh and
|
|
50
|
+
* shin on the way to a vending tray (QA 5 -> 7), so a low reach keeps the plain ease.
|
|
51
|
+
*/
|
|
52
|
+
export function earlyTransport(ctx, a) {
|
|
53
|
+
const p = restPoint(ctx, a), I = ctx.sk.index;
|
|
54
|
+
return !!p && p[1] >= ctx.sk.restWorldPos[I.LeftLeg][1] + KNEE;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The world point an anchor rests at before the grab (world, or a scene object's point), or null for one that moves with a body. */
|
|
58
|
+
function restPoint(ctx, a) {
|
|
59
|
+
if (a?.kind === "world" && Array.isArray(a.p)) return a.p.map(Number); // a copy, as in bodyOf
|
|
60
|
+
const o = a?.kind === "object" ? ctx.scene?.objects?.[a.object] : null;
|
|
61
|
+
if (!o || [a.object, ...(o.ancestors ?? [])].some(id => movesBy(ctx, id))) return null;
|
|
62
|
+
return add(o.pos, apply(o.R, a.local ?? [0, o.half?.[1] ?? 0, 0]));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* True when spec.objects moves record `id` before the reach is done (ctx.until): a follow or a hinge, or keys or a fall
|
|
67
|
+
* from before that frame (a carry whose keys start at a grab after the hand arrives rests until then).
|
|
68
|
+
*/
|
|
69
|
+
function movesBy(ctx, id) {
|
|
70
|
+
const o = ctx.objects?.[id];
|
|
71
|
+
if (!o || typeof o !== "object") return false;
|
|
72
|
+
if (o.follow !== undefined || o.hinge !== undefined) return true;
|
|
73
|
+
const from = Math.min(...(o.keys ?? []).map(k => k.f).filter(Number.isFinite), o.fall?.at ?? Infinity);
|
|
74
|
+
return from < (ctx.until ?? Infinity);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const opened = new WeakMap();
|
|
78
|
+
/** The rig's relaxed hand opening, thumb tip to index tip (m). */
|
|
79
|
+
function openHand(sk) {
|
|
80
|
+
if (!opened.has(sk)) {
|
|
81
|
+
const s = restState(sk), I = sk.index;
|
|
82
|
+
fingers(sk, s, "Right", {});
|
|
83
|
+
opened.set(sk, len(sub(s.P[I.RightHandThumb4], s.P[I.RightHandIndex4])));
|
|
84
|
+
}
|
|
85
|
+
return opened.get(sk);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The scene body anchor a reaches for (its object, or the body its world point lies in), or null. */
|
|
89
|
+
function bodyOf(ctx, a) {
|
|
90
|
+
const objects = ctx.scene?.objects ?? {};
|
|
91
|
+
if (a?.kind === "object") return objects[a.object]?.half ? objects[a.object] : null;
|
|
92
|
+
if (a?.kind !== "world" || !Array.isArray(a.p)) return null;
|
|
93
|
+
// a copy: the spec's own (frozen) array passed into the shared vector math deoptimised it for the whole compile (2x)
|
|
94
|
+
const p = a.p.map(Number);
|
|
95
|
+
return Object.values(objects).filter(o => o.half && sdBox(p, o) <= 0.01).sort((x, y) => sdBox(p, x) - sdBox(p, y))[0] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** True when, OVER m short of point p toward the character (root r), the hand is still above a surface within BELOW m under p. */
|
|
99
|
+
function overSupport(ctx, p, r, skip) {
|
|
100
|
+
const dx = r[0] - p[0], dz = r[2] - p[2], d = Math.hypot(dx, dz);
|
|
101
|
+
if (d < 1e-6) return false;
|
|
102
|
+
const q = [p[0] + (dx / d) * OVER, p[1], p[2] + (dz / d) * OVER];
|
|
103
|
+
return Object.entries(ctx.scene?.objects ?? {}).some(([id, o]) => {
|
|
104
|
+
if (!o.half || skip.has(id) || skip.has(o.parent)) return false;
|
|
105
|
+
const top = sub(q, [0, BELOW, 0]);
|
|
106
|
+
return sdBox(q, o) > 0 && sdBox(top, o) <= 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* True when the hand stays open on the way to the thing anchor a reaches for, from the pose at f0 (state): it fits the
|
|
112
|
+
* rig's open hand with MARGIN, and the hand's last OVER m to it are not over the surface it rests on.
|
|
113
|
+
*/
|
|
114
|
+
export function holdsOpen(ctx, a, state) {
|
|
115
|
+
const o = bodyOf(ctx, a), p = restPoint(ctx, a) ?? (o && add(o.pos, apply(o.R, [0, o.half[1], 0])));
|
|
116
|
+
if (!o || !p || 2 * Math.min(...o.half) + MARGIN > openHand(ctx.sk)) return false;
|
|
117
|
+
const skip = new Set(Object.entries(ctx.scene.objects).filter(([, x]) => x === o).map(([id]) => id));
|
|
118
|
+
return !(Array.isArray(state.root) && overSupport(ctx, p, state.root, skip));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The head's yaw and pitch (deg, the head's own channels) that turn the gaze HEAD_SHARE of the way onto anchor a from the
|
|
123
|
+
* pose at f0 (state), or null when the anchor moves with a body or the pose has no root.
|
|
124
|
+
*/
|
|
125
|
+
export function gazeAt(ctx, a, state) {
|
|
126
|
+
const p = restPoint(ctx, a), r = state.root;
|
|
127
|
+
if (!p || !Array.isArray(r)) return null;
|
|
128
|
+
const P = ctx.sk.restWorldPos, I = ctx.sk.index, eye = [r[0], r[1] + P[I.Head][1] - P[I.Hips][1], r[2]];
|
|
129
|
+
const d = [p[0] - eye[0], p[1] - eye[1], p[2] - eye[2]], flat = Math.hypot(d[0], d[2]);
|
|
130
|
+
if (flat < 0.05) return null;
|
|
131
|
+
let yaw = Math.atan2(d[0], d[2]) * DEG - (state.yaw ?? 0) - (state.spine_yaw ?? 0);
|
|
132
|
+
while (yaw > 180) yaw -= 360;
|
|
133
|
+
while (yaw < -180) yaw += 360;
|
|
134
|
+
const down = Math.atan2(-d[1], flat) * DEG - (state.pitch ?? 0) - (state.spine_pitch ?? 0);
|
|
135
|
+
return { head_yaw: clamp(HEAD_SHARE * yaw, -60, 60), head_pitch: clamp(HEAD_SHARE * down, -30, 50) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** True when keys before this skill already move the head within [a, b]: a keyed look wins. */
|
|
139
|
+
export const headKeyed = (keys, a, b) => keys.some(k => k.f >= a && k.f <= b && (k.set?.head_yaw !== undefined || k.set?.head_pitch !== undefined));
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where a standing body stands (standIdle): clear of the scene's bodies, and within reach of what it reaches for.
|
|
2
|
+
// Session 7f15b4ae: every cup and ball attempt stood with a foot in the table (COLLIDE foot_r x cube over the whole
|
|
3
|
+
// take) and the arm at full reach, while QA's hint already said how far to move the body; the agent kept guessing
|
|
4
|
+
// `at` by hand. Two rules:
|
|
5
|
+
// clear a stance whose legs, feet or hips are inside a body (a table, a box on the floor) slides out of it, the
|
|
6
|
+
// least distance that clears it by MARGIN: straight back first, then sideways, then the diagonals
|
|
7
|
+
// reach standIdle with no `at` (and no root keyed) before a reachGrip in the same spec keeps its heading and steps
|
|
8
|
+
// up to the anchor: the reaching shoulder in line with it, as near as COMFORT of the arm's length allows,
|
|
9
|
+
// then back out of whatever is in the way (it stands at a table's edge)
|
|
10
|
+
// The body is the rest pose's legs, feet and hips as capsules (qa/geometry.js radii); a body whose top is under
|
|
11
|
+
// SUPPORT m (a floor, a mat, a low step) is stood on, not stood clear of.
|
|
12
|
+
import { add, sub, len, apply, ry } from "../compiler/math.js";
|
|
13
|
+
import { sdBox } from "../qa/geometry.js";
|
|
14
|
+
import { partsOf } from "../object-pose.js";
|
|
15
|
+
|
|
16
|
+
const MARGIN = 0.01, SUPPORT = 0.12, COMFORT = 0.8, SLIDE = 0.6, STEP = 0.01, NEAREST = 0.15, HANG = 0.62;
|
|
17
|
+
// [from, to, radius] of the standing body, as in qa/geometry.js CAPSULES (the foot: ankle to the toe tip)
|
|
18
|
+
const PARTS = ["Left", "Right"].flatMap(S => [[`${S}UpLeg`, `${S}Leg`, 0.07], [`${S}Leg`, `${S}Foot`, 0.05], [`${S}Foot`, `${S}ToeBase`, 0.045], [`${S}ToeBase`, `${S}Toe_End`, 0.04]])
|
|
19
|
+
.concat([["LeftUpLeg", "RightUpLeg", 0.095], ["Hips", "Spine2", 0.12]]);
|
|
20
|
+
const DIRS = [[0, -1], [1, 0], [-1, 0], [Math.SQRT1_2, -Math.SQRT1_2], [-Math.SQRT1_2, -Math.SQRT1_2]];
|
|
21
|
+
|
|
22
|
+
/** The standing body's sample points [p, r] at ground point c = [x, z] facing heading h (deg): legs, feet, hips and the arms hanging at the sides (standIdle's FK arms down). */
|
|
23
|
+
function bodyPoints(sk, c, h) {
|
|
24
|
+
const I = sk.index, P = sk.restWorldPos, hip = P[I.Hips], R = ry(h), at = p => add([c[0], 0, c[1]], apply(R, [p[0] - hip[0], p[1], p[2] - hip[2]]));
|
|
25
|
+
const seg = (A, B, r) => [0, 0.25, 0.5, 0.75, 1].map(t => [add(A, sub(B, A).map(v => v * t)), r]);
|
|
26
|
+
const arms = ["Left", "Right"].flatMap(S => { const sh = P[I[`${S}Arm`]], side = Math.sign(sh[0] - hip[0]) * 0.04; return seg(at(sh), at([sh[0] + side, sh[1] - HANG, sh[2]]), 0.045); });
|
|
27
|
+
return PARTS.filter(([a, b]) => I[a] !== undefined && I[b] !== undefined).flatMap(([a, b, r]) => seg(at(P[I[a]]), at(P[I[b]]), r)).concat(arms);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The bodies a stance must stay clear of: every scene body whose top is above SUPPORT m. */
|
|
31
|
+
const obstacles = scene => Object.values(scene?.objects ?? {}).filter(o => o.half && o.pos && o.R && o.pos[1] + 2 * o.half[1] * o.R[1][1] > SUPPORT);
|
|
32
|
+
|
|
33
|
+
/** How far the body at c, h is clear of the obstacles (m; negative: inside one by that much, past MARGIN). */
|
|
34
|
+
function clearance(sk, boxes, c, h) {
|
|
35
|
+
let worst = Infinity;
|
|
36
|
+
for (const [p, r] of bodyPoints(sk, c, h)) for (const b of boxes) worst = Math.min(worst, sdBox(p, b) - r - MARGIN);
|
|
37
|
+
return worst;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A stance at c ([x, z]) facing h moved the least distance out of the scene's bodies: { at, moved } or null when it is clear. */
|
|
41
|
+
export function clearStance(sk, scene, c, h) {
|
|
42
|
+
const boxes = obstacles(scene);
|
|
43
|
+
if (!boxes.length || clearance(sk, boxes, c, h) >= 0) return null;
|
|
44
|
+
const R = ry(h);
|
|
45
|
+
for (let d = STEP; d <= SLIDE + 1e-9; d += STEP) {
|
|
46
|
+
for (const [x, z] of DIRS) {
|
|
47
|
+
const v = apply(R, [x * d, 0, z * d]), at = [c[0] + v[0], c[1] + v[2]];
|
|
48
|
+
if (clearance(sk, boxes, at, h) >= 0) return { at: at.map(n => +n.toFixed(4)), moved: +d.toFixed(2) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The world point an anchor definition names on the still scene, as anchors.js resolves it: world p; an object's (or a
|
|
56
|
+
* group's) `local` point, its base pivot without one; with `grasp` and no `local`, the centre of its parts (where a
|
|
57
|
+
* thin part's socket sits). Null for any other kind.
|
|
58
|
+
*/
|
|
59
|
+
function anchorPoint(scene, a) {
|
|
60
|
+
if (a?.kind === "world" && Array.isArray(a.p)) return a.p;
|
|
61
|
+
if (a?.kind !== "object" || !scene) return null;
|
|
62
|
+
const o = scene.objects?.[a.object] ?? scene.groups?.[a.object];
|
|
63
|
+
if (!o) return null;
|
|
64
|
+
if (!a.grasp || a.local) return add(o.pos, apply(o.R, a.local ?? [0, 0, 0]));
|
|
65
|
+
const parts = partsOf(scene, a.object).map(id => scene.objects[id]).filter(p => p?.half);
|
|
66
|
+
const centres = parts.map(p => add(p.pos, apply(p.R, [0, p.half[1], 0])));
|
|
67
|
+
return centres.reduce((m, p) => add(m, p.map(v => v / centres.length)), [0, 0, 0]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The stance for a standIdle with no `at`: the first reachGrip after it in `use` (index i) and its anchor. The body
|
|
72
|
+
* keeps its facing (args.heading, else the keyed yaw, else 0: the agent turns it by heading) and steps up to the
|
|
73
|
+
* anchor: the reaching shoulder lined up with it, COMFORT of the arm's length from it (at least NEAREST m ahead), then
|
|
74
|
+
* straight back out of any body in the way (a table: the body stands at its edge). { at, heading, anchor, moved } or
|
|
75
|
+
* null when no reachGrip follows or its anchor has no still point.
|
|
76
|
+
*/
|
|
77
|
+
export function reachStance(ctx) {
|
|
78
|
+
const { sk, scene, use = [], index = -1 } = ctx, next = use.slice(index + 1).find(u => u?.skill === "reachGrip");
|
|
79
|
+
const a = next && (next.args?.define ?? ctx.anchors?.[next.args?.anchor]), T = anchorPoint(scene, a);
|
|
80
|
+
if (!T) return null;
|
|
81
|
+
const I = sk.index, P = sk.restWorldPos, S = next.args.side === "l" ? "Left" : "Right", hip = P[I.Hips], sh = P[I[`${S}Arm`]];
|
|
82
|
+
const arm = len(sub(P[I[`${S}ForeArm`]], sh)) + len(sub(P[I[`${S}Hand`]], P[I[`${S}ForeArm`]])) + len(sub(P[I[`${S}HandMiddle2`]], P[I[`${S}Hand`]]));
|
|
83
|
+
const dy = sh[1] - T[1], ahead = Math.max(NEAREST, Math.sqrt(Math.max(0, (COMFORT * arm) ** 2 - dy * dy)));
|
|
84
|
+
const h = ctx.args.heading ?? ctx.state?.yaw ?? 0, R = ry(h), boxes = obstacles(scene);
|
|
85
|
+
const off = apply(R, [sh[0] - hip[0], 0, sh[2] - hip[2] + ahead]), c0 = [T[0] - off[0], T[2] - off[2]];
|
|
86
|
+
let moved = 0, c = c0;
|
|
87
|
+
for (; moved <= SLIDE + 1e-9 && clearance(sk, boxes, c, h) < 0; moved += STEP) {
|
|
88
|
+
const back = apply(R, [0, 0, -(moved + STEP)]);
|
|
89
|
+
c = [c0[0] + back[0], c0[1] + back[2]];
|
|
90
|
+
}
|
|
91
|
+
return { at: c.map(n => +n.toFixed(4)), heading: h, anchor: next.args.anchor, moved: +moved.toFixed(2) };
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// throwObject: the hand throws what it carries at a point (#816). From the pose at f0 the arm winds up behind the shoulder,
|
|
2
|
+
// swings forward and lets go, then follows through. The launch is closed form: the arc's launch angle (low LOW deg, high
|
|
3
|
+
// HIGH deg) and the speed that carries the object from where the hand lets it go to args.to under G (free-motion.js). The
|
|
4
|
+
// last frame before the release and the release frame put the wrist one frame's travel apart along that velocity, so the
|
|
5
|
+
// wrist moves at the launch velocity there; the fall the skill writes leaves at that velocity, { at: release, v } (flight.js).
|
|
6
|
+
// Not v: "hand": the hand's turn as the elbow opens adds a whip to the object (about 2 m/s at the release, so the throw
|
|
7
|
+
// carried 2 m long), which only the solved frames know; "hand" stays for an agent's own release. It writes the carry's release and fall (fills: fields the agent's own entry leaves out) and a goal: the
|
|
8
|
+
// object near args.to at its landing. A release with no fall stays where the hand let it go (C3), so the skill writes one.
|
|
9
|
+
import { add, sub, scale, apply, ry, len } from "../compiler/math.js";
|
|
10
|
+
import { G } from "../free-motion.js";
|
|
11
|
+
import { skillError } from "./book.js";
|
|
12
|
+
import { GRASP_FINGERS } from "../compiler/vocab.js";
|
|
13
|
+
|
|
14
|
+
const LOW = 20, HIGH = 50, WIND = 10, SWING = 8, FOLLOW = 10, SETTLE_FRAMES = 8, TOL = 0.25;
|
|
15
|
+
// the wrist at the release: ahead of the shoulder along the throw and up; the thing held rides PALM m ahead of the wrist
|
|
16
|
+
const AHEAD = 0.42, UP = 0.22, PALM = 0.09, BACK = 0.12, SIDE = 0.25, LOWER = 0.4;
|
|
17
|
+
const r3 = v => v.map(x => Math.round(x * 1000) / 1000);
|
|
18
|
+
|
|
19
|
+
/** How far the root of scene body or group `id` sits above its lowest point at rest (m). */
|
|
20
|
+
function liftOf(scene, id) {
|
|
21
|
+
const parts = Object.entries(scene?.objects ?? {}).filter(([k, e]) => k === id || e.ancestors?.includes(id)).map(([, e]) => e);
|
|
22
|
+
const root = scene?.objects?.[id]?.pos ?? parts[0]?.ancestorPoses?.[0]?.pos;
|
|
23
|
+
if (!root || !parts.length) return 0;
|
|
24
|
+
return root[1] - Math.min(...parts.map(e => e.pos[1]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function throwObject(ctx) {
|
|
28
|
+
const { args, state, sk, scene, tag } = ctx, id = args.object, k = args.side ?? "r", o = ctx.objects?.[id];
|
|
29
|
+
if (k !== "l" && k !== "r") skillError(tag, `args.side must be "l" or "r", got ${JSON.stringify(k)}`);
|
|
30
|
+
if (!o?.prop || ctx.props?.[o.prop]?.hand !== k) skillError(tag, `args.object "${id}" must be carried by the ${k === "r" ? "right" : "left"} hand (objects.${id}.prop on a prop with hand "${k}")`);
|
|
31
|
+
if (!Array.isArray(args.to) || args.to.length !== 3) skillError(tag, "args.to must be [x, y, z] world m (where it lands)");
|
|
32
|
+
const theta = ((args.arc === "high" ? HIGH : LOW) * Math.PI) / 180, fps = ctx.fps ?? 24, S = k === "l" ? "Left" : "Right";
|
|
33
|
+
// the shoulder where the body stands at f0: the bind pose's offset from the hips, turned by the yaw, on the keyed root
|
|
34
|
+
const P = sk.restWorldPos, hips = state.root ?? P[sk.index.Hips], R = ry(state.yaw ?? 0);
|
|
35
|
+
const shoulder = add(hips, apply(R, sub(P[sk.index[`${S}Arm`]], P[sk.index.Hips])));
|
|
36
|
+
const flat = [args.to[0] - shoulder[0], 0, args.to[2] - shoulder[2]], dist = len(flat);
|
|
37
|
+
if (dist < 0.5) skillError(tag, `args.to is ${dist.toFixed(2)} m from the shoulder; throw it at least 0.5 m`);
|
|
38
|
+
const dir = scale(flat, 1 / dist), wrist = add(shoulder, add(scale(dir, AHEAD), [0, UP, 0])), from = add(wrist, scale(dir, PALM));
|
|
39
|
+
const land = args.to[1] + liftOf(scene, id), d = len([args.to[0] - from[0], 0, args.to[2] - from[2]]), dy = land - from[1];
|
|
40
|
+
const den = 2 * Math.cos(theta) ** 2 * (d * Math.tan(theta) - dy);
|
|
41
|
+
if (!(den > 0)) skillError(tag, `args.to is too high for a ${args.arc ?? "low"} arc from the hand; use arc "high" or a lower point`);
|
|
42
|
+
const speed = Math.sqrt((G * d * d) / den), v = add(scale(dir, speed * Math.cos(theta)), [0, speed * Math.sin(theta), 0]);
|
|
43
|
+
const flight = (v[1] + Math.sqrt(v[1] ** 2 + 2 * G * Math.max(0, from[1] - land))) / G;
|
|
44
|
+
const release = ctx.f0 + WIND + SWING, landed = Math.min(ctx.frames - 1, release + Math.ceil(flight * fps) + SETTLE_FRAMES);
|
|
45
|
+
// the wind-up goes back out on the hand's side (not behind the back), the follow-through on down across the body
|
|
46
|
+
const out = scale([dir[2], 0, -dir[0]], k === "r" ? -1 : 1), hand = `hand_${k}`, armw = `armw_${k}`;
|
|
47
|
+
const wind = add(shoulder, add(add(scale(dir, -BACK), scale(out, SIDE)), [0, -0.05, 0]));
|
|
48
|
+
// mid-swing the wrist stays out from the shoulder (a target near it folds the elbow past its stop); after the release it
|
|
49
|
+
// slows over three frames, going on and down, then crosses low in front
|
|
50
|
+
const mid = add(shoulder, add(add(scale(out, 0.32), scale(dir, 0.12)), [0, 0.08, 0]));
|
|
51
|
+
const on = add(wrist, scale(v, 0.45 / fps)), through = add(shoulder, add(scale(dir, 0.46), [0, -0.1, 0])), follow = add(shoulder, add(add(scale(dir, 0.3), scale(out, -0.15)), [0, -LOWER, 0]));
|
|
52
|
+
// the hand keeps its grip on the thing through the swing and opens as it lets go
|
|
53
|
+
const fg = `fingers_${k}`, preset = GRASP_FINGERS[o.grasp] ?? "grip", grip = { ...state[fg], [preset]: 1 }, open = { ...state[fg], [preset]: 0 };
|
|
54
|
+
const keys = [
|
|
55
|
+
{ f: ctx.f0, set: { [armw]: state[armw] ?? 0, [fg]: grip, ...(state[hand] ? { [hand]: state[hand] } : {}) } },
|
|
56
|
+
{ f: release, set: { [fg]: grip } }, { f: release + 3, set: { [fg]: open } },
|
|
57
|
+
{ f: ctx.f0 + WIND, set: { [armw]: 1, [hand]: r3(wind) } },
|
|
58
|
+
{ f: ctx.f0 + WIND + Math.round(SWING / 2), set: { [armw]: 1, [hand]: r3(mid) } },
|
|
59
|
+
{ f: release - 1, set: { [armw]: 1, [hand]: r3(sub(wrist, scale(v, 1 / fps))) } },
|
|
60
|
+
{ f: release, set: { [armw]: 1, [hand]: r3(wrist) } },
|
|
61
|
+
{ f: release + 1, set: { [armw]: 1, [hand]: r3(on) } },
|
|
62
|
+
{ f: release + 3, set: { [armw]: 1, [hand]: r3(through) } },
|
|
63
|
+
{ f: release + FOLLOW, set: { [armw]: 1, [hand]: r3(follow) } },
|
|
64
|
+
];
|
|
65
|
+
const target = `${id}Target`;
|
|
66
|
+
return {
|
|
67
|
+
keys, end: release + FOLLOW, anchors: { [target]: { kind: "world", p: r3([args.to[0], land, args.to[2]]) } },
|
|
68
|
+
fills: { [id]: { release, fall: { at: release, v: r3(v) } } },
|
|
69
|
+
goals: [{ name: `${id} lands at the target`, near: [`object:${id}`, `anchor:${target}`], at: landed, tol: TOL }],
|
|
70
|
+
throw: { release, speed: Math.round(speed * 100) / 100, v: r3(v), flight: Math.round(flight * 100) / 100 },
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/motion3/sockets.js
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
// bar, a can, a handle) is one too, named by its id, through its centre along its long axis. An object anchor with
|
|
12
12
|
// `grasp` (power, pinch, hook, palm) takes the hand by a socket of its object: `socket` names it, else the one
|
|
13
13
|
// nearest the anchor's `local` point or, without one, nearest the shoulder of the hand, picked once per compile.
|
|
14
|
+
// A disc (a cylinder whose height is under THIN m and whose two sides are wider: a steering wheel) is a rim socket
|
|
15
|
+
// instead: `rim` its radius, `normal` its axis; the hand takes the rim where it is nearest the anchor's `local` point
|
|
16
|
+
// or the hand's shoulder, the knuckle line along the rim (session 2ad2760a: the wheel was held by points in the
|
|
17
|
+
// driver's frame, 15 cm off the rim, and they did not follow the truck). anchorSocket fixes that point once per hand.
|
|
14
18
|
import { add, sub, scale, norm, cross, dot, len, apply, ry, T } from "./compiler/math.js";
|
|
15
19
|
import { partsOf } from "./object-pose.js";
|
|
16
20
|
|
|
@@ -24,6 +28,9 @@ function thinAxis(half) {
|
|
|
24
28
|
return [0, 1, 2].filter(i => i !== long).every(i => half[i] < THIN) ? long : -1;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/** A cylinder flat along its axis (its height under THIN m) and round across it, both sides over THIN m. */
|
|
32
|
+
const isDisc = (renderer, half) => renderer === "cylinder" && !!half && half[1] < THIN && Math.min(half[0], half[2]) >= THIN && Math.abs(half[0] - half[2]) < 0.1 * half[0];
|
|
33
|
+
|
|
27
34
|
/** The record's stored sockets: a body's in scene.objects, a group's (an Empty) in scene.others. */
|
|
28
35
|
const stored = (scene, id) => scene?.objects?.[id]?.grasp ?? scene?.others?.[id]?.grasp ?? null;
|
|
29
36
|
|
|
@@ -36,8 +43,10 @@ export function socketsOf(scene, id) {
|
|
|
36
43
|
own(id, "");
|
|
37
44
|
for (const part of partsOf(scene, id)) {
|
|
38
45
|
if (part !== id) own(part, `${part}.`);
|
|
39
|
-
const half = scene.objects[part]
|
|
40
|
-
|
|
46
|
+
const { half, renderer } = scene.objects[part], long = thinAxis(half);
|
|
47
|
+
// a thin body's radius: the thinner of the two half sizes across the axis (how far the palm sits off the axis)
|
|
48
|
+
if (long >= 0) out.push({ name: part, owner: part, at: [0, half[1], 0], axis: AXES[long], radius: Math.min(...half.filter((_, i) => i !== long)) });
|
|
49
|
+
else if (isDisc(renderer, half)) out.push({ name: part, owner: part, at: [0, half[1], 0], rim: half[0], normal: AXES[1] });
|
|
41
50
|
}
|
|
42
51
|
return out;
|
|
43
52
|
}
|
|
@@ -48,7 +57,14 @@ export const socketNamed = (scene, id, name) => socketsOf(scene, id).find(s => s
|
|
|
48
57
|
/** A socket in world space on its owner's pose { pos, R }: { p, axis?, from?, thumb? }. */
|
|
49
58
|
export function socketWorld(socket, pose) {
|
|
50
59
|
const dir = v => (v ? norm(apply(pose.R, v)) : undefined);
|
|
51
|
-
return { p: add(pose.pos, apply(pose.R, socket.at ?? [0, 0, 0])), axis: dir(socket.axis), from: dir(socket.from), thumb: dir(socket.thumb)
|
|
60
|
+
return { p: add(pose.pos, apply(pose.R, socket.at ?? [0, 0, 0])), axis: dir(socket.axis), from: dir(socket.from), thumb: dir(socket.thumb), radius: socket.radius,
|
|
61
|
+
...(socket.rim ? { rim: socket.rim, normal: dir(socket.normal) } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The point of world rim socket w nearest world point q, and the rim's direction there: { p, axis }. */
|
|
65
|
+
export function rimPoint(w, q) {
|
|
66
|
+
const v = flat(sub(q, w.p), w.normal), r = len(v) > 1e-6 ? norm(v) : norm(flat(Math.abs(w.normal[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0], w.normal));
|
|
67
|
+
return { p: add(w.p, scale(r, w.rim)), axis: norm(cross(w.normal, r)) };
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
/** The outward normal of the face of the box (of boxes [{ pos, R, half }], base pivots) nearest world point p, or undefined. */
|
|
@@ -62,9 +78,10 @@ export function faceFrom(boxes, p) {
|
|
|
62
78
|
return best?.n;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
/** How far world point q is from a world socket: from its axis line through p, else from p. */
|
|
81
|
+
/** How far world point q is from a world socket: from its rim, else its axis line through p, else from p. */
|
|
66
82
|
export function socketDistance(w, q) {
|
|
67
83
|
const v = sub(q, w.p);
|
|
84
|
+
if (w.rim) return Math.hypot(dot(v, w.normal), len(flat(v, w.normal)) - w.rim);
|
|
68
85
|
return len(w.axis ? flat(v, w.axis) : v);
|
|
69
86
|
}
|
|
70
87
|
|
package/src/motion3/views.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// C11 own views: the cameras of a verify_result contact_sheet with `views`. They are the
|
|
2
2
|
// agent's own cameras around the subject, never the user's shot camera. A view stays at one
|
|
3
|
-
// place for every frame of the sheet
|
|
3
|
+
// place for every frame of the sheet (or rides with the focus when it travels farther than its
|
|
4
|
+
// size) and frames what matters over those frames (the focus
|
|
4
5
|
// refs, else the whole character, plus any trail), so a small action fills the tile and
|
|
5
6
|
// motion reads as change inside one still frame. Pure over a compile result (compileSpec .out).
|
|
6
7
|
// front in front of the character, 10 degrees up
|
|
@@ -78,7 +79,8 @@ function fit(name, points, back, upHint, aspect) {
|
|
|
78
79
|
* Deterministic: the same compile, frames and arguments give the same cameras.
|
|
79
80
|
*/
|
|
80
81
|
export function viewCameras(out, { views, frames, focus = [], trails = [], aspect = 1 }) {
|
|
81
|
-
|
|
82
|
+
const ride = rideOf(out, frames, focus);
|
|
83
|
+
let points = frames.flatMap(f => focus.flatMap(ref => refPoints(out, f, ref)).map(p => (ride ? sub(p, ride[f]) : p)));
|
|
82
84
|
const body = frames.flatMap(f => jointsAt(out, f));
|
|
83
85
|
if (!points.length) points = body;
|
|
84
86
|
points = [...points, ...trails.flatMap(t => t.points.map(q => q.p))];
|
|
@@ -86,12 +88,33 @@ export function viewCameras(out, { views, frames, focus = [], trails = [], aspec
|
|
|
86
88
|
const { left, forward } = facingOf(out, frames), hips = mean(frames.map(f => jointsAt(out, f)[out.ctx.sk.index.Hips]).filter(Boolean));
|
|
87
89
|
const side = focus.length && dot(sub(mean(points), hips), left) > 0.02 ? left : scale(left, -1);
|
|
88
90
|
return views.map(name => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
91
|
+
const cam = name === "top" ? fit(name, points, UP, forward, aspect)
|
|
92
|
+
: (e => fit(name, points, add(scale(name === "front" ? forward : name === "side" ? side : norm(add(forward, side)), Math.cos(e)), scale(UP, Math.sin(e))), UP, aspect))(rad(ELEVATION[name]));
|
|
93
|
+
return ride ? { ...cam, ride } : cam;
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The focus's travel over the frames: { [frame]: its centre there less its mean centre } when it moves farther than its
|
|
99
|
+
* own size (a wheel in a driving truck, session 2ad2760a: one camera for the whole drive framed the road and showed the
|
|
100
|
+
* hands too small to read), else null. A riding view keeps its place relative to the focus (cameraAt).
|
|
101
|
+
*/
|
|
102
|
+
function rideOf(out, frames, focus) {
|
|
103
|
+
if (!focus.length) return null;
|
|
104
|
+
const at = frames.map(f => ({ f, points: focus.flatMap(ref => refPoints(out, f, ref)) })).filter(x => x.points.length);
|
|
105
|
+
if (at.length < 2) return null;
|
|
106
|
+
const centres = at.map(x => mean(x.points)), mid = mean(centres);
|
|
107
|
+
const size = Math.max(MIN_HALF, ...at.map((x, i) => Math.max(...x.points.map(p => Math.hypot(...sub(p, centres[i]))))));
|
|
108
|
+
if (Math.max(...centres.map(c => Math.hypot(...sub(c, mid)))) <= size) return null;
|
|
109
|
+
return Object.fromEntries(at.map((x, i) => [x.f, sub(centres[i], mid)]));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** View camera `cam` at `frame`: a riding view moved by the focus's travel there (rideOf), else cam itself. */
|
|
113
|
+
export function cameraAt(cam, frame) {
|
|
114
|
+
const d = cam.ride?.[frame];
|
|
115
|
+
return d ? { ...cam, eye: add(cam.eye, d), target: add(cam.target, d) } : cam;
|
|
116
|
+
}
|
|
117
|
+
|
|
95
118
|
// A picture hides distance along its own line of sight, so a sheet holds two views at least APART
|
|
96
119
|
// degrees apart. Front and three-quarter (45 degrees apart) look along nearly one line: the
|
|
97
120
|
// guitarist's right hand 20 cm in front of the guitar (session df388a9b) showed in neither.
|
package/src/object-catalog.jsx
CHANGED
|
@@ -4,7 +4,6 @@ import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
|
|
|
4
4
|
|
|
5
5
|
const GROUP_LABELS_KO = {
|
|
6
6
|
Primitives: "기본 도형",
|
|
7
|
-
"Set pieces": "세트 소품",
|
|
8
7
|
Lights: "조명",
|
|
9
8
|
Effects: "효과",
|
|
10
9
|
Text: "텍스트",
|
|
@@ -18,9 +17,6 @@ const OBJECT_LABELS_KO = {
|
|
|
18
17
|
Cylinder: "원기둥",
|
|
19
18
|
Cone: "원뿔",
|
|
20
19
|
Plane: "평면",
|
|
21
|
-
Chair: "의자",
|
|
22
|
-
Car: "자동차",
|
|
23
|
-
"Plane (aircraft)": "비행기",
|
|
24
20
|
"Point light": "점 조명",
|
|
25
21
|
"Spot light": "스포트 조명",
|
|
26
22
|
Light: "조명",
|
package/src/object-gizmo.jsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import * as THREE from "three";
|
|
4
|
-
import { GIZMO_LAYER, SHOT_ASPECT
|
|
4
|
+
import { GIZMO_LAYER, SHOT_ASPECT } from "./dualview.jsx";
|
|
5
|
+
import { fitAspect } from "./shot-frame.js";
|
|
5
6
|
import { HANDLE_PROXY_FLAG, claimsPress } from "./gizmo-claim.js";
|
|
6
7
|
import { CUTOUT_KIND, rotatePatch, scalePatch, screenRotatePatch, translatePatch, wrapAngle } from "./scene-objects.js";
|
|
7
8
|
import { dragTravel, dragTurn } from "./held-object-pose.js";
|
package/src/object-path.js
CHANGED
|
@@ -464,4 +464,4 @@ export function strokeToPathPoints(stroke, simplify, { maxPoints = STROKE_MAX_PO
|
|
|
464
464
|
return points.map((point) => ({ x: point.x, y: 0, z: point.z }));
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
export { MAX_PATH_POINTS, MAX_PATH_MARKS, MARK_GAP, MAX_LEAN as MAX_PATH_LEAN,
|
|
467
|
+
export { MAX_PATH_POINTS, MAX_PATH_MARKS, MARK_GAP, MAX_LEAN as MAX_PATH_LEAN, STROKE_MAX_POINTS };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
1
3
|
import { ko } from "../locale.js";
|
|
4
|
+
import { usePopover } from "../popover.js";
|
|
2
5
|
import ResourceStatus from "../resource-status.jsx";
|
|
3
6
|
import { useBus } from "../app-context.js";
|
|
4
7
|
import "./details.css";
|
|
@@ -7,27 +10,45 @@ export default function ProjectPanel({
|
|
|
7
10
|
projectMenuOpen, setProjectMenuOpen, projectDirty, projectName, projectStartupOpen, projectManifest,
|
|
8
11
|
}) {
|
|
9
12
|
const { run } = useBus();
|
|
13
|
+
const triggerRef = useRef(null);
|
|
14
|
+
const [menuBox, setMenuBox] = useState(null);
|
|
15
|
+
usePopover(projectMenuOpen, () => setProjectMenuOpen(false));
|
|
16
|
+
// The menu is portaled and fixed under the trigger: the glass column clips
|
|
17
|
+
// and blurs its children, so an in-place menu was cut at the column edge
|
|
18
|
+
// and read the column's transparent panel fill. App.jsx closes it on an
|
|
19
|
+
// outside pointerdown via `.closest(".project-menu-wrap")`, so the portal
|
|
20
|
+
// layer carries that class (display:contents, no box of its own).
|
|
21
|
+
useLayoutEffect(() => {
|
|
22
|
+
const rect = projectMenuOpen ? triggerRef.current?.getBoundingClientRect() : null;
|
|
23
|
+
setMenuBox(rect ? { left: rect.left, top: rect.bottom + 6 } : null);
|
|
24
|
+
}, [projectMenuOpen]);
|
|
25
|
+
// Hidden until the layout effect has placed it, so it never paints at the
|
|
26
|
+
// wrong spot; without a document (server render) it stays in place.
|
|
27
|
+
const menu = projectMenuOpen && (
|
|
28
|
+
<div className="project-menu-wrap v2-project-menu project-menu-layer">
|
|
29
|
+
<div className="project-menu" role="menu" style={menuBox ?? { visibility: "hidden" }} onClick={() => setProjectMenuOpen(false)}>
|
|
30
|
+
<button type="button" role="menuitem" onClick={() => run("project.new")}>{ko("New Project", "새 프로젝트")}</button>
|
|
31
|
+
<button type="button" role="menuitem" onClick={() => run("project.browse")}>{ko("Open Project…", "프로젝트 열기…")}</button>
|
|
32
|
+
<button type="button" role="menuitem" onClick={() => run("project.save")}>{ko("Save Project", "프로젝트 저장")}</button>
|
|
33
|
+
<button type="button" role="menuitem" onClick={() => run("project.saveAs")}>{ko("Save Project As…", "다른 이름으로 저장…")}</button>
|
|
34
|
+
<ResourceStatus manifest={projectManifest} compact />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
10
38
|
return (
|
|
11
|
-
<div className="project-menu-wrap v2-project-menu">
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<button type="button" role="menuitem" onClick={() => run("project.browse")}>{ko("Open Project…", "프로젝트 열기…")}</button>
|
|
26
|
-
<button type="button" role="menuitem" onClick={() => run("project.save")}>{ko("Save Project", "프로젝트 저장")}</button>
|
|
27
|
-
<button type="button" role="menuitem" onClick={() => run("project.saveAs")}>{ko("Save Project As…", "다른 이름으로 저장…")}</button>
|
|
28
|
-
<ResourceStatus manifest={projectManifest} compact />
|
|
29
|
-
</div>
|
|
30
|
-
)}
|
|
31
|
-
</div>
|
|
39
|
+
<div className="project-menu-wrap v2-project-menu">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
className="project-menu-trigger"
|
|
43
|
+
ref={triggerRef}
|
|
44
|
+
aria-expanded={projectMenuOpen}
|
|
45
|
+
onClick={() => setProjectMenuOpen((open) => !open)}
|
|
46
|
+
>
|
|
47
|
+
{projectDirty && <i className="project-dirty-dot" aria-label={ko("Unsaved changes", "저장되지 않은 변경사항")} />}
|
|
48
|
+
{projectName ?? (projectStartupOpen ? ko("Choose Project", "프로젝트 선택") : ko("Untitled Project", "제목 없는 프로젝트"))}
|
|
49
|
+
<span className="caret">▾</span>
|
|
50
|
+
</button>
|
|
51
|
+
{menu && (globalThis.document ? createPortal(menu, globalThis.document.body) : menu)}
|
|
52
|
+
</div>
|
|
32
53
|
);
|
|
33
54
|
}
|