cozyclay 1.9.0 → 2.0.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 +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { StudioProtocolError } from './studio-agent-protocol.js';
|
|
3
|
+
|
|
4
|
+
export const AppContext = createContext(null);
|
|
5
|
+
export function useBus() { return useContext(AppContext).bus; }
|
|
6
|
+
|
|
7
|
+
const ref = current => ({ current });
|
|
8
|
+
|
|
9
|
+
// Stores own the document and its snapshots. The facade owns only the ordered
|
|
10
|
+
// relationship between their committed sessions, never a second state history.
|
|
11
|
+
export function createAppContext({
|
|
12
|
+
characters: charactersRef = ref(null), scenes: scenesRef = ref(null),
|
|
13
|
+
motion: motionRef = ref(null), state: liveStateRef = ref(null), getBus, notify,
|
|
14
|
+
} = {}) {
|
|
15
|
+
const storeDomains = new Map(), domainBegins = new WeakMap(), observedStores = new WeakSet();
|
|
16
|
+
const entries = new Map();
|
|
17
|
+
let past = [], future = [], activeComposition = null, historyRevision = 0;
|
|
18
|
+
let currentPorts = {};
|
|
19
|
+
const storeDomain = domain => storeDomains.get(domain);
|
|
20
|
+
const registeredDomains = () => [...storeDomains.values()];
|
|
21
|
+
const ports = { storeDomain, storeDomains: registeredDomains }, actionPorts = { storeDomain, storeDomains: registeredDomains };
|
|
22
|
+
const fail = (code, message) => { throw new StudioProtocolError(code, message); };
|
|
23
|
+
const memberRetained = row => storeDomain(row.name) === row.handle && row.store.isRetained(row.id);
|
|
24
|
+
const retained = entry => entry.members.every(memberRetained);
|
|
25
|
+
const frontier = redo => redo ? future[0] : past.at(-1);
|
|
26
|
+
const ready = (entry, redo) => !activeComposition && entry === frontier(redo) && retained(entry)
|
|
27
|
+
&& entry.members.every(row => redo ? row.store.canRedo() && row.store.history().future[0]?.historyEntryId === row.id : row.store.canUndo(row.id));
|
|
28
|
+
function prune() {
|
|
29
|
+
// The oldest store snapshot has no pre-image, but is still a boundary.
|
|
30
|
+
// Keep it (and partially expired compositions) so traversal cannot skip
|
|
31
|
+
// an unundoable newer transition to reach an older owner.
|
|
32
|
+
for (const [id, entry] of entries) if (!entry.members.some(row => {
|
|
33
|
+
if (storeDomain(row.name) !== row.handle) return false;
|
|
34
|
+
const history = row.store.history();
|
|
35
|
+
return row.store.isRetained(row.id) || (history.past[0] ?? history.present).historyEntryId === row.id;
|
|
36
|
+
})) entries.delete(id);
|
|
37
|
+
past = past.filter(entry => entries.has(entry.id));
|
|
38
|
+
future = future.filter(entry => entries.has(entry.id));
|
|
39
|
+
}
|
|
40
|
+
function step(entry, redo) {
|
|
41
|
+
if (!entry || !ready(entry, redo)) return false;
|
|
42
|
+
const revision = ports.revision?.current;
|
|
43
|
+
for (const row of redo ? entry.members : [...entry.members].reverse()) (redo ? row.store.redo : row.store.undo)();
|
|
44
|
+
if (redo) { future.shift(); past.push(entry); }
|
|
45
|
+
else { past.pop(); future.unshift(entry); }
|
|
46
|
+
historyRevision++;
|
|
47
|
+
// Multiple owners publish synchronously, but traversal is one authored
|
|
48
|
+
// transition at the binding boundary.
|
|
49
|
+
if (revision !== undefined) ports.revision.current = revision + 1;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
function remember(members) {
|
|
53
|
+
if (!members.length) return { historyEntryId: null };
|
|
54
|
+
const id = members.length === 1 ? members[0].id : crypto.randomUUID();
|
|
55
|
+
if (entries.has(id)) return { historyEntryId: id };
|
|
56
|
+
for (const entry of future) entries.delete(entry.id);
|
|
57
|
+
future = [];
|
|
58
|
+
const entry = { id, members };
|
|
59
|
+
entry.handle = members.length === 1 ? members[0].handle : {
|
|
60
|
+
documentStore: { isRetained: wanted => wanted === id && entries.has(id) && retained(entry) },
|
|
61
|
+
canUndo: wanted => (wanted === undefined || wanted === id) && ready(entry, false),
|
|
62
|
+
stepHistory: redo => step(entry, redo),
|
|
63
|
+
};
|
|
64
|
+
entries.set(id, entry); past.push(entry); historyRevision++; prune();
|
|
65
|
+
return { historyEntryId: id };
|
|
66
|
+
}
|
|
67
|
+
function beginAction(domain, targetId = null, nested = false) {
|
|
68
|
+
if (activeComposition) {
|
|
69
|
+
if (!activeComposition.running && !nested) fail('TARGET_BUSY', 'A composed document transaction is already open.');
|
|
70
|
+
const parent = activeComposition;
|
|
71
|
+
parent.touch(domain, targetId);
|
|
72
|
+
return { run: parent.run, commit: () => ({ historyEntryId: null }), cancel: parent.cancel };
|
|
73
|
+
}
|
|
74
|
+
const members = new Map();
|
|
75
|
+
let closed = false;
|
|
76
|
+
const check = () => { if (closed) fail('STALE_TARGET', 'The composed document transaction is closed.'); };
|
|
77
|
+
const session = {
|
|
78
|
+
running: 0,
|
|
79
|
+
touches: handle => members.has(handle),
|
|
80
|
+
touch(name, targetId) {
|
|
81
|
+
check();
|
|
82
|
+
const handle = storeDomain(name);
|
|
83
|
+
if (!handle) fail('CAPABILITY_MISSING', `No document owner is registered for ${name}.`);
|
|
84
|
+
if (!members.has(handle)) members.set(handle, { name, handle, store: handle.documentStore,
|
|
85
|
+
session: domainBegins.get(handle)(targetId) });
|
|
86
|
+
},
|
|
87
|
+
run(fn) {
|
|
88
|
+
check(); session.running++;
|
|
89
|
+
try { return [...members.values()].reduceRight((next, row) => () => row.session.run(next), fn)(); }
|
|
90
|
+
finally { session.running--; }
|
|
91
|
+
},
|
|
92
|
+
commit() {
|
|
93
|
+
check();
|
|
94
|
+
const changed = [...members.values()].flatMap(row => {
|
|
95
|
+
const result = row.session.commit();
|
|
96
|
+
return result.historyEntryId ? [{ name: row.name, handle: row.handle, store: row.store, id: result.historyEntryId }] : [];
|
|
97
|
+
});
|
|
98
|
+
closed = true; activeComposition = null;
|
|
99
|
+
return remember(changed);
|
|
100
|
+
},
|
|
101
|
+
cancel(options) {
|
|
102
|
+
if (closed) return false;
|
|
103
|
+
closed = true; activeComposition = null;
|
|
104
|
+
for (const row of [...members.values()].reverse()) row.session.cancel(options);
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
activeComposition = session;
|
|
109
|
+
try { session.touch(domain, targetId); }
|
|
110
|
+
catch (error) { session.cancel(); throw error; }
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
function recordAction(domain, run, targetId = null, nested = false) {
|
|
114
|
+
const session = beginAction(domain, targetId, nested);
|
|
115
|
+
const done = result => ({ result, ...session.commit() });
|
|
116
|
+
const failed = error => { session.cancel(); throw error; };
|
|
117
|
+
try { const result = session.run(run); return result?.then ? result.then(done, failed) : done(result); }
|
|
118
|
+
catch (error) { return failed(error); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
beginAction, recordAction, notify, storeDomain,
|
|
123
|
+
storeDomainForReceipt(receipt) {
|
|
124
|
+
const entry = entries.get(receipt?.undo?.historyEntryId);
|
|
125
|
+
return entry && retained(entry) ? entry.handle : undefined;
|
|
126
|
+
},
|
|
127
|
+
storeDomains: registeredDomains,
|
|
128
|
+
registerStoreDomain(name, handle) {
|
|
129
|
+
const store = handle.documentStore;
|
|
130
|
+
if (store && !observedStores.has(store)) {
|
|
131
|
+
observedStores.add(store);
|
|
132
|
+
const committed = result => {
|
|
133
|
+
if (result.historyEntryId && !activeComposition) remember([{ name, handle, store, id: result.historyEntryId }]);
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
|
+
const begin = store.beginAction, record = store.recordAction;
|
|
137
|
+
store.beginAction = (...args) => {
|
|
138
|
+
const session = begin(...args);
|
|
139
|
+
return { ...session, commit: () => committed(session.commit()) };
|
|
140
|
+
};
|
|
141
|
+
store.recordAction = (...args) => {
|
|
142
|
+
const result = record(...args);
|
|
143
|
+
return result?.then ? result.then(committed) : committed(result);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (handle.beginAction && !domainBegins.has(handle)) {
|
|
147
|
+
domainBegins.set(handle, handle.beginAction.bind(handle));
|
|
148
|
+
handle.beginAction = targetId => beginAction(name, targetId);
|
|
149
|
+
handle.stepHistory = redo => {
|
|
150
|
+
const entry = frontier(redo);
|
|
151
|
+
return entry?.handle === handle && step(entry, redo);
|
|
152
|
+
};
|
|
153
|
+
if (handle.load) {
|
|
154
|
+
const load = handle.load.bind(handle);
|
|
155
|
+
handle.load = (...args) => {
|
|
156
|
+
if (activeComposition?.touches(handle)) activeComposition.cancel();
|
|
157
|
+
const result = load(...args); prune(); return result;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
storeDomains.set(name, handle);
|
|
162
|
+
return () => {
|
|
163
|
+
if (activeComposition?.touches(handle)) activeComposition.cancel();
|
|
164
|
+
if (storeDomain(name) === handle) storeDomains.delete(name);
|
|
165
|
+
prune();
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
nextStoreHistory(redo) {
|
|
169
|
+
prune();
|
|
170
|
+
const entry = frontier(redo);
|
|
171
|
+
return entry && ready(entry, redo) ? entry.handle : undefined;
|
|
172
|
+
},
|
|
173
|
+
historyEntry(redo = false) {
|
|
174
|
+
const entry = frontier(redo);
|
|
175
|
+
return entry && ready(entry, redo) ? entry.id : null;
|
|
176
|
+
},
|
|
177
|
+
loadStoreDomains(slices) {
|
|
178
|
+
const loaded = new Set();
|
|
179
|
+
for (const [name, domain] of storeDomains) if (domain.load) {
|
|
180
|
+
domain.load(domain.sceneSlice ? domain.sceneSlice(slices) : slices[name]);
|
|
181
|
+
loaded.add(name);
|
|
182
|
+
}
|
|
183
|
+
return loaded;
|
|
184
|
+
},
|
|
185
|
+
forRender(shared) { return Object.create(this, { shared: { value: shared } }); },
|
|
186
|
+
// A change counter for project save checkpoints, not undo arbitration.
|
|
187
|
+
get undoClock() { return historyRevision; },
|
|
188
|
+
live: Object.freeze({
|
|
189
|
+
get state() { return liveStateRef.current; },
|
|
190
|
+
get characters() { return charactersRef.current; },
|
|
191
|
+
get scenes() { return scenesRef.current; },
|
|
192
|
+
get motion() { return motionRef.current; },
|
|
193
|
+
}),
|
|
194
|
+
publishLive(value) { liveStateRef.current = value; },
|
|
195
|
+
patchLive(patch) { Object.assign(liveStateRef.current, patch); },
|
|
196
|
+
patchTimeline(patch) { Object.assign(liveStateRef.current.timeline, patch); },
|
|
197
|
+
publishCharacters(value) { charactersRef.current = value; },
|
|
198
|
+
publishScenes(value) { scenesRef.current = value; },
|
|
199
|
+
publishMotion(value) { motionRef.current = value; },
|
|
200
|
+
get bus() { return getBus(); },
|
|
201
|
+
ports, actionPorts,
|
|
202
|
+
updatePorts(next) {
|
|
203
|
+
currentPorts = next;
|
|
204
|
+
for (const key of Object.keys(next)) {
|
|
205
|
+
if (key === 'revision') ports[key] = next[key];
|
|
206
|
+
else if (!ports[key]) ports[key] = (...args) => currentPorts[key](...args);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
updateActionPorts(next) { Object.assign(actionPorts, next); },
|
|
210
|
+
};
|
|
211
|
+
}
|
package/src/app-stage.jsx
CHANGED
|
@@ -21,9 +21,9 @@ import {
|
|
|
21
21
|
TRAIL_TRACKS,
|
|
22
22
|
falloffWeight,
|
|
23
23
|
jointTrailPoints,
|
|
24
|
-
nearestFrameToRay,
|
|
25
24
|
trailEditRange,
|
|
26
25
|
} from "./motion-trail.js";
|
|
26
|
+
import { pickTrailPoint } from "./trail-pick.js";
|
|
27
27
|
import Timeline from "./ardy/timeline.jsx";
|
|
28
28
|
import { FlyControls, aimAt } from "./controls.jsx";
|
|
29
29
|
import { GIZMO_LAYER } from "./dualview.jsx";
|
|
@@ -482,11 +482,13 @@ export const DEMO_MOTION_PROMPT = "A person walks forward.";
|
|
|
482
482
|
// same window so the two deletion paths feel like one rule.
|
|
483
483
|
export const OBJECT_DELETE_UNDO_MS = 7000;
|
|
484
484
|
export const ASSET_DELETE_UNDO_MS = 7000;
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
// Neutral grey clay: the set is the grey workbench, so the figures are grey
|
|
486
|
+
// too, light enough to stand off the deck and the sky.
|
|
487
|
+
export const CLAY = "#bdbec3";
|
|
488
|
+
export const CLAY_B = "#a4a5aa";
|
|
487
489
|
// X Bot's shell is smooth (no raised exoskeleton like Y Bot's), so it gets a
|
|
488
490
|
// brighter, whiter clay to keep it readable against the set.
|
|
489
|
-
export const CLAY_X = "#
|
|
491
|
+
export const CLAY_X = "#d0d0d4";
|
|
490
492
|
// Model/role default for a cast member; a user-picked entry.tint always wins
|
|
491
493
|
// over this at render time.
|
|
492
494
|
export const defaultCharacterTint = (entry, index) => (entry.model === "x-bot-tpose" ? CLAY_X : index === 0 ? CLAY : CLAY_B);
|
|
@@ -2671,7 +2673,7 @@ export function loadSceneStartup() {
|
|
|
2671
2673
|
* re-drawn on top as a bright highlight. Grabbing any point of a line starts
|
|
2672
2674
|
* a drag on a camera-facing plane through the grab point; the caller deforms
|
|
2673
2675
|
* the take (motion-trail.js falloff math) so the preview updates live. */
|
|
2674
|
-
export const MotionTrails = memo(function MotionTrails({ motion, baseY, charScale, ikFocus, falloffFrames, pendingEdit, enabled, visible = true, onDragStart, onDragPreview, onDragEnd }) {
|
|
2676
|
+
export const MotionTrails = memo(function MotionTrails({ motion, rig = null, baseY, charScale, ikFocus, falloffFrames, playheadFrame, pendingEdit, enabled, visible = true, onDragStart, onDragPreview, onDragEnd }) {
|
|
2675
2677
|
const { camera, gl, invalidate } = useThree();
|
|
2676
2678
|
const [drag, setDrag] = useState(null);
|
|
2677
2679
|
const callbacksRef = useRef({ onDragStart, onDragPreview, onDragEnd });
|
|
@@ -2682,14 +2684,16 @@ export const MotionTrails = memo(function MotionTrails({ motion, baseY, charScal
|
|
|
2682
2684
|
for (let index = 0; index + 2 < flat.length; index += 3) out.push([flat[index], flat[index + 1], flat[index + 2]]);
|
|
2683
2685
|
return out;
|
|
2684
2686
|
};
|
|
2685
|
-
// Every trail track (root + IK endpoints + head) in its handle colour
|
|
2687
|
+
// Every trail track (root + IK endpoints + head) in its handle colour,
|
|
2688
|
+
// sampled where playback renders each bone on this rig (not the clip's
|
|
2689
|
+
// own joints, which sit 4-15 cm off the body), so draw and pick agree.
|
|
2686
2690
|
const tracks = useMemo(
|
|
2687
2691
|
() => TRAIL_TRACKS.map((track) => {
|
|
2688
|
-
const flat = jointTrailPoints(motion, track.joint, { baseY, scale: charScale });
|
|
2692
|
+
const flat = jointTrailPoints(motion, track.joint, { baseY, scale: charScale, rig });
|
|
2689
2693
|
return flat ? { ...track, flat, points: toTriples(flat) } : null;
|
|
2690
2694
|
}).filter(Boolean),
|
|
2691
2695
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2692
|
-
[motion, baseY, charScale],
|
|
2696
|
+
[motion, rig, baseY, charScale],
|
|
2693
2697
|
);
|
|
2694
2698
|
const trackById = (id) => tracks.find((track) => track.id === id) ?? null;
|
|
2695
2699
|
// The falloff window rides whichever line is being (or was last) grabbed.
|
|
@@ -2713,7 +2717,7 @@ export const MotionTrails = memo(function MotionTrails({ motion, baseY, charScal
|
|
|
2713
2717
|
// A single pointerdown listener that measures point-to-ray distance against
|
|
2714
2718
|
// the cached trail arrays costs nothing while the mouse merely moves.
|
|
2715
2719
|
const pickRef = useRef(null);
|
|
2716
|
-
pickRef.current = { tracks, enabled, falloffFrames };
|
|
2720
|
+
pickRef.current = { tracks, enabled, falloffFrames, playheadFrame };
|
|
2717
2721
|
// Line2 instances for in-place geometry rewrites during a drag.
|
|
2718
2722
|
const lineRefs = useRef({});
|
|
2719
2723
|
const highlightRef = useRef(null);
|
|
@@ -2731,21 +2735,16 @@ export const MotionTrails = memo(function MotionTrails({ motion, baseY, charScal
|
|
|
2731
2735
|
);
|
|
2732
2736
|
raycaster.setFromCamera(ndc, camera);
|
|
2733
2737
|
const { origin, direction } = raycaster.ray;
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
grabFrame = near.frame;
|
|
2745
|
-
bestDistance = near.distance;
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
if (!track) return;
|
|
2738
|
+
const picked = pickTrailPoint({
|
|
2739
|
+
tracks: pick.tracks,
|
|
2740
|
+
playheadFrame: pick.playheadFrame,
|
|
2741
|
+
falloffFrames: pick.falloffFrames,
|
|
2742
|
+
rayOrigin: origin,
|
|
2743
|
+
rayDirection: direction,
|
|
2744
|
+
maxDistance: 0.2,
|
|
2745
|
+
});
|
|
2746
|
+
if (!picked) return;
|
|
2747
|
+
const { track, grabFrame } = picked;
|
|
2749
2748
|
// The grab wins over the camera controls listening in the bubble phase.
|
|
2750
2749
|
event.stopPropagation();
|
|
2751
2750
|
event.preventDefault();
|
|
@@ -2851,10 +2850,12 @@ export const MotionTrails = memo(function MotionTrails({ motion, baseY, charScal
|
|
|
2851
2850
|
);
|
|
2852
2851
|
}, (previous, next) => (
|
|
2853
2852
|
previous.motion === next.motion &&
|
|
2853
|
+
previous.rig === next.rig &&
|
|
2854
2854
|
previous.baseY === next.baseY &&
|
|
2855
2855
|
previous.charScale === next.charScale &&
|
|
2856
2856
|
previous.ikFocus === next.ikFocus &&
|
|
2857
2857
|
previous.falloffFrames === next.falloffFrames &&
|
|
2858
|
+
previous.playheadFrame === next.playheadFrame &&
|
|
2858
2859
|
previous.pendingEdit === next.pendingEdit &&
|
|
2859
2860
|
previous.enabled === next.enabled &&
|
|
2860
2861
|
previous.visible === next.visible
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
@layer legacy {
|
|
2
|
+
.auto-fix-group {
|
|
3
|
+
--auto-fix-space: 4px;
|
|
4
|
+
display: grid;
|
|
5
|
+
gap: calc(var(--auto-fix-space) * 3);
|
|
6
|
+
margin-top: calc(var(--auto-fix-space) * 3);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.auto-fix-card {
|
|
10
|
+
--auto-fix-space: 4px;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: calc(var(--auto-fix-space) * 2);
|
|
15
|
+
padding: calc(var(--auto-fix-space) * 3);
|
|
16
|
+
border: 1px solid var(--line2);
|
|
17
|
+
border-radius: var(--radius);
|
|
18
|
+
background: var(--card2);
|
|
19
|
+
color: var(--fg);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.auto-fix-card h4 {
|
|
23
|
+
margin: 0;
|
|
24
|
+
color: var(--fg);
|
|
25
|
+
font-size: 12px;
|
|
26
|
+
font-weight: 650;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.auto-fix-card-head {
|
|
30
|
+
display: grid;
|
|
31
|
+
gap: var(--auto-fix-space);
|
|
32
|
+
min-width: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.auto-fix-card .inspector-hint {
|
|
36
|
+
margin: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.auto-fix-actions {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
42
|
+
gap: calc(var(--auto-fix-space) * 2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.auto-fix-actions > .btn {
|
|
46
|
+
min-width: 0;
|
|
47
|
+
margin-top: 0;
|
|
48
|
+
white-space: normal;
|
|
49
|
+
line-height: 1.3;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.body-collision-card .auto-fix-actions {
|
|
53
|
+
grid-template-columns: 1fr;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.auto-fix-card > .btn.full {
|
|
57
|
+
margin-top: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.auto-fix-result {
|
|
61
|
+
min-width: 0;
|
|
62
|
+
padding: calc(var(--auto-fix-space) * 2);
|
|
63
|
+
border: 1px solid var(--line2);
|
|
64
|
+
border-radius: var(--radius);
|
|
65
|
+
background: var(--card2);
|
|
66
|
+
color: var(--muted);
|
|
67
|
+
font-size: 10px;
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.auto-fix-card details {
|
|
72
|
+
border-top: 1px solid var(--line2);
|
|
73
|
+
padding-top: calc(var(--auto-fix-space) * 2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.auto-fix-card details > summary {
|
|
77
|
+
color: var(--muted);
|
|
78
|
+
font-size: 11px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.auto-fix-card .physics-actions {
|
|
82
|
+
margin: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.auto-fix-card.platform-fit,
|
|
86
|
+
.auto-fix-card.physics-review {
|
|
87
|
+
margin-top: 0;
|
|
88
|
+
padding-top: calc(var(--auto-fix-space) * 3);
|
|
89
|
+
border-top: 1px solid var(--line2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Four tools: the shared .segmented pill (styles.css, loaded after this file)
|
|
93
|
+
is sized for two, so these rules need the extra class to win. Same inset
|
|
94
|
+
geometry as the base pill, one quarter wide. */
|
|
95
|
+
.segmented.ik-edit-tools:before {
|
|
96
|
+
width: calc((100% - 8px) / 4);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.segmented.ik-edit-tools[data-active="ik"]:before { transform: translateX(0); }
|
|
100
|
+
.segmented.ik-edit-tools[data-active="trail"]:before { transform: translateX(100%); }
|
|
101
|
+
.segmented.ik-edit-tools[data-active="pin"]:before { transform: translateX(200%); }
|
|
102
|
+
.segmented.ik-edit-tools[data-active="autofix"]:before { transform: translateX(300%); }
|
|
103
|
+
|
|
104
|
+
.ik-tool-panel {
|
|
105
|
+
min-width: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
}
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import * as THREE from "three";
|
|
29
29
|
import { buildCollisionCapsules } from "./fix-collisions.js";
|
|
30
|
-
import { objectSize } from "../scene-objects.js";
|
|
30
|
+
import { isEffectivelyHidden, objectSize } from "../scene-objects.js";
|
|
31
31
|
import { objectTransformAt } from "../object-path.js";
|
|
32
32
|
|
|
33
33
|
const DEG = Math.PI / 180;
|
|
@@ -115,14 +115,16 @@ function resolveSize(object, library) {
|
|
|
115
115
|
* @param {Iterable<string>} [options.skipIds]
|
|
116
116
|
* @returns {Array<{id:string,kind:"box",center:THREE.Vector3,halfExtents:THREE.Vector3,yaw:number}>}
|
|
117
117
|
*/
|
|
118
|
-
export function sceneObjectBlockers(sceneObjects, { library = null, frame = null, take = null, skipIds = null } = {}) {
|
|
118
|
+
export function sceneObjectBlockers(sceneObjects, { library = null, frame = null, take = null, skipIds = null, characters = null } = {}) {
|
|
119
119
|
if (!Array.isArray(sceneObjects) || !sceneObjects.length) return [];
|
|
120
120
|
const skip = skipIds ? new Set(skipIds) : null;
|
|
121
|
+
const cast = !characters ? [] : Array.isArray(characters) ? characters : [...characters];
|
|
121
122
|
const out = [];
|
|
122
123
|
for (const object of sceneObjects) {
|
|
123
124
|
if (!object || typeof object !== "object") continue;
|
|
124
125
|
if (typeof object.id !== "string" || !object.id) continue;
|
|
125
126
|
if (skip?.has(object.id)) continue;
|
|
127
|
+
if (isEffectivelyHidden(object, sceneObjects, cast)) continue;
|
|
126
128
|
// A carried prop's numbers are not world numbers — see the note above.
|
|
127
129
|
if (object.attach) continue;
|
|
128
130
|
const size = resolveSize(object, library);
|
|
@@ -229,10 +231,14 @@ function characterIdSet(characterIds) {
|
|
|
229
231
|
*/
|
|
230
232
|
export function characterBlockers(rigs, activeId = null, { radii = null, characterIds = null } = {}) {
|
|
231
233
|
const cast = characterIdSet(characterIds);
|
|
234
|
+
const hiddenCast = new Set(
|
|
235
|
+
[...(characterIds ?? [])].filter((each) => each && typeof each === "object" && each.hidden === true).map((each) => each.id),
|
|
236
|
+
);
|
|
232
237
|
const out = [];
|
|
233
238
|
for (const [characterId, rig] of rigEntries(rigs)) {
|
|
234
239
|
if (!rig || characterId === activeId) continue;
|
|
235
240
|
if (cast && !cast.has(characterId)) continue;
|
|
241
|
+
if (hiddenCast.has(characterId)) continue;
|
|
236
242
|
const override = radii instanceof Map ? radii.get(characterId) ?? null : radii?.[characterId] ?? radii ?? null;
|
|
237
243
|
const capsules = buildCollisionCapsules(rig, override);
|
|
238
244
|
if (!capsules) continue;
|
|
@@ -258,7 +264,7 @@ export function characterBlockers(rigs, activeId = null, { radii = null, charact
|
|
|
258
264
|
export function collisionBlockers({ rigs = null, activeId = null, characterIds = null, sceneObjects = null, library = null, frame = null, take = null, skipIds = null, radii = null } = {}) {
|
|
259
265
|
return [
|
|
260
266
|
...characterBlockers(rigs, activeId, { radii, characterIds }),
|
|
261
|
-
...sceneObjectBlockers(sceneObjects ?? [], { library, frame, take, skipIds }),
|
|
267
|
+
...sceneObjectBlockers(sceneObjects ?? [], { library, frame, take, skipIds, characters: characterIds }),
|
|
262
268
|
];
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -1206,23 +1206,14 @@ const NO_PUSH = Object.freeze({ ran: false, moved: false });
|
|
|
1206
1206
|
* limb's rest pose by escapeVector — see the REST-BIASED ESCAPE note there for
|
|
1207
1207
|
* why the raw separation normal is not enough on a deep hit.
|
|
1208
1208
|
*/
|
|
1209
|
-
function pushCapsule(capsule, chains, push, ikState, { onlyChains = null, floorY = 0
|
|
1209
|
+
function pushCapsule(capsule, chains, push, ikState, { onlyChains = null, floorY = 0 } = {}) {
|
|
1210
1210
|
const target = capsule.def.movable;
|
|
1211
1211
|
if (!target) return NO_PUSH;
|
|
1212
1212
|
if (onlyChains && !onlyChains.has(target.chain)) return NO_PUSH;
|
|
1213
1213
|
const chain = chains?.get(target.chain);
|
|
1214
1214
|
if (!chain) return NO_PUSH;
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
// only there — so the reset happens either way; doing it HERE means the push
|
|
1218
|
-
// target is read on the same skeleton the solver will produce, which keeps
|
|
1219
|
-
// the baked delta the push rather than the length compensation, while every
|
|
1220
|
-
// chain this run does not solve keeps the clip's own translations. Doing it
|
|
1221
|
-
// to the WHOLE rig up front (what this used to do) meant detecting on a
|
|
1222
|
-
// skeleton the caller never sees: phantom pairs on limbs the readout calls
|
|
1223
|
-
// clean, and leg chains keyed — and snapped to bind length — for penetrations
|
|
1224
|
-
// that only existed at bind.
|
|
1225
|
-
if (normalise) normalise(target.chain, chain);
|
|
1215
|
+
// Detection, target and solve all use the clip's current translations.
|
|
1216
|
+
// A bind reset here would reintroduce length compensation into the key.
|
|
1226
1217
|
const rotations = chain.bones.map((bone) => bone.quaternion.clone());
|
|
1227
1218
|
const driven = target.joint === "mid" ? chain.bones[1] : chain.bones[2];
|
|
1228
1219
|
const before = driven.getWorldPosition(new THREE.Vector3());
|
|
@@ -1288,24 +1279,10 @@ function pushCapsule(capsule, chains, push, ikState, { onlyChains = null, floorY
|
|
|
1288
1279
|
* pose on the rig at entry is used, which is exact whenever no earlier key is
|
|
1289
1280
|
* already blending into this frame.
|
|
1290
1281
|
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
* keeps the baked delta the push rather than the LENGTH COMPENSATION: aiming at
|
|
1296
|
-
* a clip-length limb's wrist with a bind-length one turned a 15 mm lift into an
|
|
1297
|
-
* 11.8 ° knee swing whose components cancel only at full weight, and a partly
|
|
1298
|
-
* weighted blend of that wandered 31 mm.
|
|
1299
|
-
*
|
|
1300
|
-
* What this must NOT do — and used to — is normalise every chain up front,
|
|
1301
|
-
* before the first capsule is built. That made the pass detect on a skeleton the
|
|
1302
|
-
* caller never sees: on a real clip (~19 mm off bind per bone) it reported pairs
|
|
1303
|
-
* the UI's own readout calls clean, keyed limbs for penetrations that exist only
|
|
1304
|
-
* at bind (whole-clip runs moved a leg 142 mm on frames with no leg pair at all,
|
|
1305
|
-
* and put a toe 17 mm under the floor), and made a second press of the button
|
|
1306
|
-
* put an arm back inside the crate the first press took it out of. Detection now
|
|
1307
|
-
* reads the pose as handed over; only a chain being solved is normalised, and a
|
|
1308
|
-
* chain this run does not key gets its clip translations back untouched.
|
|
1282
|
+
* CLIP TRANSLATIONS THROUGHOUT. The solvers measure each frame's segment
|
|
1283
|
+
* lengths without changing its locals. Detection, pushes and warm starts must
|
|
1284
|
+
* therefore leave those translations alone too; the bake records them so the
|
|
1285
|
+
* pose measured here is the one playback reproduces.
|
|
1309
1286
|
*/
|
|
1310
1287
|
export function fixCollisions(rig, chains, {
|
|
1311
1288
|
radii = null,
|
|
@@ -1328,33 +1305,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1328
1305
|
const bases = baseQuats ?? new Map(
|
|
1329
1306
|
[...chains].map(([id, chain]) => [id, chain.bones.map((bone) => bone.quaternion.clone())]),
|
|
1330
1307
|
);
|
|
1331
|
-
/** Bind translations, once, for a chain that is about to be solved — see
|
|
1332
|
-
* pushCapsule. Nothing else on the rig is touched, so DETECTION always reads
|
|
1333
|
-
* the pose the caller handed over (plus whatever this run has already
|
|
1334
|
-
* solved), which is the same pose detectPenetrations reports to the UI. */
|
|
1335
|
-
const clipTranslations = [];
|
|
1336
|
-
const normalised = new Set();
|
|
1337
|
-
const normaliseChain = (id, chain) => {
|
|
1338
|
-
if (normalised.has(id) || !chain?.bindPositions) return;
|
|
1339
|
-
normalised.add(id);
|
|
1340
|
-
chain.bones.forEach((bone, index) => {
|
|
1341
|
-
clipTranslations.push([id, bone, bone.position.clone()]);
|
|
1342
|
-
bone.position.copy(chain.bindPositions[index]);
|
|
1343
|
-
});
|
|
1344
|
-
rig.updateMatrixWorld(true);
|
|
1345
|
-
};
|
|
1346
|
-
/** Give the clip its translations back on every chain this pass did not
|
|
1347
|
-
* actually key. Only a keyed chain owes the bind pose its rotations were
|
|
1348
|
-
* solved in; the rest are the clip's business and are handed back untouched. */
|
|
1349
|
-
const restoreTranslations = (keptIds = null) => {
|
|
1350
|
-
let restored = false;
|
|
1351
|
-
for (const [id, bone, position] of clipTranslations) {
|
|
1352
|
-
if (keptIds?.has(id)) continue;
|
|
1353
|
-
bone.position.copy(position);
|
|
1354
|
-
restored = true;
|
|
1355
|
-
}
|
|
1356
|
-
if (restored) rig.updateMatrixWorld(true);
|
|
1357
|
-
};
|
|
1358
1308
|
const touched = new Set();
|
|
1359
1309
|
let poseDirty = false;
|
|
1360
1310
|
/**
|
|
@@ -1377,7 +1327,7 @@ export function fixCollisions(rig, chains, {
|
|
|
1377
1327
|
const chainId = capsule.def.movable?.chain;
|
|
1378
1328
|
if (allowedChains && chainId && !allowedChains.has(chainId)) return false;
|
|
1379
1329
|
const outcome = pushCapsule(capsule, chains, vector, ikState, {
|
|
1380
|
-
onlyChains, floorY,
|
|
1330
|
+
onlyChains, floorY,
|
|
1381
1331
|
});
|
|
1382
1332
|
if (outcome.ran) {
|
|
1383
1333
|
touched.add(capsule.def.movable.chain);
|
|
@@ -1549,7 +1499,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1549
1499
|
for (; pass < maxIterations; pass += 1) {
|
|
1550
1500
|
const capsules = buildCollisionCapsules(rig, radii);
|
|
1551
1501
|
if (!capsules) {
|
|
1552
|
-
if (!changed) restoreTranslations();
|
|
1553
1502
|
return { supported: false, changed, passes: pass, residual, touched: [...touched], baseQuats: bases };
|
|
1554
1503
|
}
|
|
1555
1504
|
// `changed` is cumulative across passes, so it can never report what
|
|
@@ -1803,7 +1752,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1803
1752
|
&& afterFoot < Math.min(entryFoot, floorY + PUSH_FLOOR_CLEARANCE) - 1e-6;
|
|
1804
1753
|
if (!improved || buried) {
|
|
1805
1754
|
frameRestore(entryPose);
|
|
1806
|
-
restoreTranslations();
|
|
1807
1755
|
return {
|
|
1808
1756
|
supported: true,
|
|
1809
1757
|
changed: false,
|
|
@@ -1816,8 +1764,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1816
1764
|
}
|
|
1817
1765
|
}
|
|
1818
1766
|
|
|
1819
|
-
// Hand the pose back as it arrived everywhere this pass did not key.
|
|
1820
|
-
restoreTranslations(changed ? touched : null);
|
|
1821
1767
|
// Final measurement for an honest residual report.
|
|
1822
1768
|
if (changed) {
|
|
1823
1769
|
const capsules = buildCollisionCapsules(rig, radii);
|
|
@@ -2043,16 +1989,14 @@ export function fixCollisionsRange({
|
|
|
2043
1989
|
}
|
|
2044
1990
|
return deltas;
|
|
2045
1991
|
};
|
|
2046
|
-
/** Seed `ids` with the previous frame's correction
|
|
2047
|
-
*
|
|
2048
|
-
* this frame's rotations composed with that frame's delta. */
|
|
1992
|
+
/** Seed `ids` with the previous frame's rotation correction on this frame's
|
|
1993
|
+
* own translations, the same solve space used by the fresh attempts. */
|
|
2049
1994
|
const applyWarmStart = (ids, deltas) => {
|
|
2050
1995
|
for (const id of ids) {
|
|
2051
1996
|
const chain = chains.get(id);
|
|
2052
1997
|
const delta = deltas.get(id);
|
|
2053
1998
|
if (!chain || !delta) continue;
|
|
2054
1999
|
chain.bones.forEach((bone, index) => {
|
|
2055
|
-
if (chain.bindPositions) bone.position.copy(chain.bindPositions[index]);
|
|
2056
2000
|
if (delta[index]) bone.quaternion.multiply(delta[index]);
|
|
2057
2001
|
});
|
|
2058
2002
|
}
|
package/src/ardy/ground.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { objectFootprintBounds } from "../scene-objects.js";
|
|
1
|
+
import { isEffectivelyHidden, objectFootprintBounds } from "../scene-objects.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Ground height under a point, from the scene's objects (issue #250).
|
|
@@ -25,8 +25,8 @@ import { objectFootprintBounds } from "../scene-objects.js";
|
|
|
25
25
|
* the floor. Sloped tops are out of scope - the height is flat per object and
|
|
26
26
|
* the foot keeps its source rotation.
|
|
27
27
|
*/
|
|
28
|
-
export function createGroundSampler(objects = [], { floorY = 0, collidable = isCollidable } = {}) {
|
|
29
|
-
const surfaces = objects.filter(collidable).map((object) => objectFootprintBounds(object));
|
|
28
|
+
export function createGroundSampler(objects = [], { floorY = 0, collidable = isCollidable, characters = [] } = {}) {
|
|
29
|
+
const surfaces = objects.filter((object) => !isEffectivelyHidden(object, objects, characters) && collidable(object)).map((object) => objectFootprintBounds(object));
|
|
30
30
|
const groundAt = (x, z, maxY = Infinity) => {
|
|
31
31
|
let best = floorY;
|
|
32
32
|
for (const b of surfaces) {
|
|
@@ -44,7 +44,7 @@ export function createGroundSampler(objects = [], { floorY = 0, collidable = isC
|
|
|
44
44
|
/** Objects a foot can stand on: solid props with a real footprint. Cutouts,
|
|
45
45
|
* cameras, lights, and attached/child objects are not ground. */
|
|
46
46
|
export function isCollidable(object) {
|
|
47
|
-
if (!object || object.kind === "cutout") return false;
|
|
47
|
+
if (!object || object.kind === "cutout" || object.hidden === true) return false;
|
|
48
48
|
if (object.parentId || object.attach) return false;
|
|
49
49
|
if (object.collide === false) return false;
|
|
50
50
|
const b = objectFootprintBounds(object);
|