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.
Files changed (286) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/bin/agent/agent-routes.mjs +74 -70
  3. package/bin/agent/agent-runner.mjs +10 -3
  4. package/bin/agent/motion-runtime.mjs +32 -260
  5. package/bin/agent/providers.mjs +46 -3
  6. package/bin/agent/session-store.mjs +41 -5
  7. package/bin/agent/studio-prompt.mjs +1 -1
  8. package/bin/agent/studio-tools.mjs +48 -11
  9. package/bin/cozyclay.mjs +26 -8
  10. package/bin/live/cli.mjs +22 -3
  11. package/bin/mcp-runtime.mjs +35 -20
  12. package/bin/telemetry-state.mjs +5 -2
  13. package/bin/update-check.mjs +15 -0
  14. package/dist/app/index.html +6 -6
  15. package/dist/assets/analytics-MVZOEW8O.js +1 -0
  16. package/dist/assets/app-C1sdVbg4.css +1 -0
  17. package/dist/assets/app-CA55tcI3.js +4876 -0
  18. package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
  19. package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
  20. package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
  21. package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
  22. package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
  23. package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
  24. package/dist/assets/workflow-DRdwoDBT.css +1 -0
  25. package/dist/assets/workflow-dNPGY9xz.js +179 -0
  26. package/dist/cozyclay-package.json +1 -1
  27. package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
  28. package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
  29. package/dist/fonts/README.md +13 -9
  30. package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
  31. package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
  32. package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
  34. package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
  35. package/dist/index.html +7 -7
  36. package/dist/privacy/index.html +13 -8
  37. package/dist/sitemap.xml +6 -6
  38. package/dist/workflow/index.html +5 -5
  39. package/mcp/LIVE-PROTOCOL.md +1 -1
  40. package/mcp/live-hub.mjs +35 -93
  41. package/mcp/mesh-file.mjs +48 -0
  42. package/mcp/server.mjs +6 -77
  43. package/mcp/tool-handlers.mjs +224 -377
  44. package/package.json +2 -1
  45. package/src/App.jsx +1273 -9075
  46. package/src/analytics.js +393 -9
  47. package/src/app-context.js +211 -0
  48. package/src/app-stage.jsx +25 -24
  49. package/src/ardy/auto-fix-panel.css +108 -0
  50. package/src/ardy/collision-blockers.js +9 -3
  51. package/src/ardy/fix-collisions.js +10 -66
  52. package/src/ardy/ground.js +4 -4
  53. package/src/ardy/ik-drag.js +101 -0
  54. package/src/ardy/ik-key-json.js +43 -0
  55. package/src/ardy/ik.js +122 -76
  56. package/src/ardy/physics-panel.css +2 -0
  57. package/src/ardy/physics-panel.jsx +17 -5
  58. package/src/ardy/platform-fit-panel.css +64 -0
  59. package/src/ardy/platform-fit-panel.jsx +40 -0
  60. package/src/ardy/platform-fit.js +326 -0
  61. package/src/ardy/playback.js +166 -20
  62. package/src/ardy/range-pin.js +299 -0
  63. package/src/ardy/timeline-coordinates.js +26 -0
  64. package/src/ardy/timeline.css +1199 -0
  65. package/src/ardy/timeline.jsx +149 -41
  66. package/src/ardy/waypoints.js +2 -2
  67. package/src/asset-pane.css +462 -0
  68. package/src/asset-pane.jsx +461 -232
  69. package/src/command-bus.js +349 -0
  70. package/src/commands/ai.js +31 -0
  71. package/src/commands/cast.js +140 -0
  72. package/src/commands/elements/character.js +13 -0
  73. package/src/commands/elements/motion.js +14 -0
  74. package/src/commands/elements/object.js +22 -0
  75. package/src/commands/elements/scene.js +16 -0
  76. package/src/commands/elements/shot.js +9 -0
  77. package/src/commands/elements/stage.js +11 -0
  78. package/src/commands/elements.js +155 -0
  79. package/src/commands/export.js +20 -0
  80. package/src/commands/index.js +40 -0
  81. package/src/commands/motion.js +231 -0
  82. package/src/commands/objects.js +174 -0
  83. package/src/commands/project.js +53 -0
  84. package/src/commands/scene.js +81 -0
  85. package/src/commands/shared.js +19 -0
  86. package/src/commands/shot.js +103 -0
  87. package/src/commands/stage.js +17 -0
  88. package/src/commands/view.js +42 -0
  89. package/src/document-store.js +144 -0
  90. package/src/domains/cast.js +1007 -0
  91. package/src/domains/motion.js +3219 -0
  92. package/src/domains/objects.js +932 -0
  93. package/src/domains/scenes.js +817 -0
  94. package/src/domains/shots.js +404 -0
  95. package/src/domains/stage.js +93 -0
  96. package/src/dualview.jsx +113 -57
  97. package/src/facing-marks.js +3 -0
  98. package/src/first-success-guide.jsx +14 -14
  99. package/src/grid-view.js +18 -5
  100. package/src/hierarchy-model.js +24 -16
  101. package/src/hierarchy-panel.css +386 -0
  102. package/src/hierarchy-panel.jsx +150 -21
  103. package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
  104. package/src/i2v-motion-studio.jsx +180 -0
  105. package/src/ik-camera.js +3 -0
  106. package/src/main.jsx +6 -0
  107. package/src/motion/generation.js +105 -0
  108. package/src/motion-readiness-ui.jsx +4 -4
  109. package/src/motion-readiness.js +11 -0
  110. package/src/motion-trail.js +366 -9
  111. package/src/object-gizmo.jsx +7 -1
  112. package/src/otio.js +2 -1
  113. package/src/panels/CameraPanel.jsx +60 -0
  114. package/src/panels/CharacterTransformPanel.jsx +48 -0
  115. package/src/panels/EnvironmentPanel.jsx +40 -0
  116. package/src/panels/Foldout.jsx +32 -0
  117. package/src/panels/LightPanel.jsx +24 -0
  118. package/src/panels/ObjectTransformPanel.jsx +554 -0
  119. package/src/panels/PosePanel.jsx +97 -0
  120. package/src/panels/ProjectPanel.jsx +33 -0
  121. package/src/panels/PromptBlocksPanel.jsx +442 -0
  122. package/src/panels/PropsPanel.jsx +70 -0
  123. package/src/panels/ReferenceImageField.jsx +91 -0
  124. package/src/panels/RigControlPanel.jsx +78 -0
  125. package/src/panels/RigPanel.jsx +37 -0
  126. package/src/panels/SubjectBox.jsx +47 -0
  127. package/src/panels/SubjectsPanel.jsx +39 -0
  128. package/src/panels/VideoCapturePanel.jsx +180 -0
  129. package/src/panels/details.css +1171 -0
  130. package/src/panels/motion.css +197 -0
  131. package/src/panels/pose.css +39 -0
  132. package/src/planview.jsx +60 -31
  133. package/src/posestudio.jsx +59 -4
  134. package/src/project-browser.css +1073 -0
  135. package/src/project-browser.jsx +284 -123
  136. package/src/range-pin-object-transform.js +19 -0
  137. package/src/range-pin-panel.css +518 -0
  138. package/src/range-pin-panel.jsx +344 -0
  139. package/src/result-modal.jsx +5 -5
  140. package/src/room.jsx +79 -51
  141. package/src/scene-objects.js +37 -1
  142. package/src/scenes.js +5 -0
  143. package/src/semantic-edit.js +2 -0
  144. package/src/settings-menu.jsx +39 -105
  145. package/src/shell/BottomDock.jsx +450 -0
  146. package/src/shell/DetailsSlot.jsx +513 -0
  147. package/src/shell/LibrarySlot.jsx +86 -0
  148. package/src/shell/MenuBar.jsx +601 -0
  149. package/src/shell/OutlinerSlot.jsx +50 -0
  150. package/src/shell/PreferencesDialog.jsx +538 -0
  151. package/src/shell/PreferencesSlot.jsx +40 -0
  152. package/src/shell/StatusBar.jsx +125 -0
  153. package/src/shell/StudioShell.jsx +92 -0
  154. package/src/shell/TopBar.jsx +129 -0
  155. package/src/shell/ViewportToolbar.jsx +656 -0
  156. package/src/shell/agent-glass.css +296 -0
  157. package/src/shell/dock.css +93 -0
  158. package/src/shell/glass-regions.css +985 -0
  159. package/src/shell/glass.css +687 -0
  160. package/src/shell/log-store.js +52 -0
  161. package/src/shell/mode.css +9 -0
  162. package/src/shell/preferences.css +651 -0
  163. package/src/shell/shell.css +282 -0
  164. package/src/shell/studio-shell-context.js +19 -0
  165. package/src/shell/topbar.css +435 -0
  166. package/src/shell/viewport.css +451 -0
  167. package/src/store/authored-intent.js +22 -0
  168. package/src/store/runtime-adapters.js +92 -0
  169. package/src/store/scene-stage.js +21 -0
  170. package/src/store/use-document-store.js +14 -0
  171. package/src/studio-actions.js +210 -0
  172. package/src/studio-agent-commands.js +41 -271
  173. package/src/studio-agent-context.js +39 -10
  174. package/src/studio-agent-motion.js +122 -373
  175. package/src/studio-agent-protocol.js +67 -31
  176. package/src/studio-app-binding.js +379 -0
  177. package/src/studio-contact-sheet.js +73 -0
  178. package/src/studio-elements.js +58 -51
  179. package/src/styles/themes.css +200 -0
  180. package/src/styles/tokens.css +69 -0
  181. package/src/styles.css +460 -1742
  182. package/src/theme.js +44 -0
  183. package/src/timeline-extent.js +16 -0
  184. package/src/trail-key-conflicts.js +39 -0
  185. package/src/trail-pick.js +59 -0
  186. package/src/ui.jsx +8 -8
  187. package/src/use-case-question.jsx +66 -0
  188. package/src/workflow/AgentPanel.jsx +82 -69
  189. package/src/workflow/agent-client.js +12 -2
  190. package/src/workflow/agent-panel.css +34 -32
  191. package/src/workflow/cozy-scene-node.css +2 -0
  192. package/src/workflow/workflow.css +2 -0
  193. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  194. package/tools/ardy/bridge.mjs +164 -146
  195. package/tools/ardy/visual-qa.mjs +5 -5
  196. package/tools/bench/EXP3.md +61 -0
  197. package/tools/bench/cclay_bench_extract_incam.py +125 -0
  198. package/tools/bench/cclay_bench_extract_obs.py +204 -0
  199. package/tools/bench/cclay_bench_runner.py +42 -0
  200. package/tools/bench/cube-contact.mjs +162 -0
  201. package/tools/bench/exp3.mjs +152 -0
  202. package/tools/bench/extract-bench-lib.mjs +216 -0
  203. package/tools/bench/extract-bench.mjs +302 -0
  204. package/tools/bench/fal-generate.mjs +88 -0
  205. package/tools/bench/fit/README.md +189 -0
  206. package/tools/bench/fit/camera.mjs +59 -0
  207. package/tools/bench/fit/contact.mjs +419 -0
  208. package/tools/bench/fit/footlock.mjs +234 -0
  209. package/tools/bench/fit/motion.mjs +85 -0
  210. package/tools/bench/fit/pin.mjs +27 -0
  211. package/tools/bench/fit/remote.mjs +105 -0
  212. package/tools/bench/fit-bench.mjs +126 -0
  213. package/tools/bench/fit-sanity.mjs +45 -0
  214. package/tools/bench/metrics.mjs +313 -0
  215. package/tools/bench/obs/depth.mjs +258 -0
  216. package/tools/bench/obs/extrinsics.mjs +175 -0
  217. package/tools/bench/obs/fit_mannequin_betas.py +340 -0
  218. package/tools/bench/obs/ground.mjs +294 -0
  219. package/tools/bench/obs/heading.mjs +151 -0
  220. package/tools/bench/obs/ladder.mjs +555 -0
  221. package/tools/bench/obs/mannequin-betas.json +124 -0
  222. package/tools/bench/obs/remote.mjs +83 -0
  223. package/tools/bench/obs/rest_joints.py +52 -0
  224. package/tools/bench/obs/ybot-targets.mjs +49 -0
  225. package/tools/bench/obs-bench.mjs +520 -0
  226. package/tools/bench/score.mjs +343 -0
  227. package/tools/bench/summarize.mjs +86 -0
  228. package/tools/dev/pages/privacy.html +13 -8
  229. package/tools/gt-render/browser.mjs +159 -0
  230. package/tools/gt-render/camera-math.mjs +276 -0
  231. package/tools/gt-render/page.mjs +267 -0
  232. package/tools/gt-render/render.mjs +514 -0
  233. package/tools/gt-render/scene-box.mjs +31 -0
  234. package/tools/gt-render/take-transform.mjs +87 -0
  235. package/tools/morphgs/assets/gen_truth.py +39 -0
  236. package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
  237. package/tools/morphgs/assets/playback-check.mjs +30 -0
  238. package/tools/morphgs/demo-gate.sh +26 -0
  239. package/tools/morphgs/fbx2morphgs.mjs +68 -0
  240. package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
  241. package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
  242. package/tools/morphgs/setup-on-cluster.sh +126 -0
  243. package/tools/qa/css-rule-usage.mjs +283 -0
  244. package/tools/qa/studio-control-count.mjs +70 -32
  245. package/tools/run-tests.mjs +134 -12
  246. package/tools/track/DESIGN.md +513 -0
  247. package/tools/track/backfill-provenance.mjs +116 -0
  248. package/tools/track/budget.mjs +8 -0
  249. package/tools/track/check-rig.mjs +209 -0
  250. package/tools/track/diagnostics.schema.json +60 -0
  251. package/tools/track/export-rig.mjs +256 -0
  252. package/tools/track/fallback.mjs +7 -0
  253. package/tools/track/fk-parity-fixture.mjs +161 -0
  254. package/tools/track/gate.mjs +349 -0
  255. package/tools/track/masks.mjs +318 -0
  256. package/tools/track/metrics.mjs +201 -0
  257. package/tools/track/publish-obs.mjs +108 -0
  258. package/tools/track/py/check_env.py +61 -0
  259. package/tools/track/py/eval_lr.py +229 -0
  260. package/tools/track/py/lr_viterbi.py +294 -0
  261. package/tools/track/py/masks.py +405 -0
  262. package/tools/track/py/objective.py +382 -0
  263. package/tools/track/py/rig.py +508 -0
  264. package/tools/track/py/scene.py +326 -0
  265. package/tools/track/py/test_joint_indices.py +123 -0
  266. package/tools/track/py/test_lr_viterbi.py +195 -0
  267. package/tools/track/py/test_masks.py +229 -0
  268. package/tools/track/py/test_rig.py +288 -0
  269. package/tools/track/py/test_scene.py +354 -0
  270. package/tools/track/py/test_track.py +328 -0
  271. package/tools/track/py/track.py +411 -0
  272. package/tools/track/remote.mjs +140 -0
  273. package/tools/track/rig-dump.mjs +227 -0
  274. package/tools/track/run-box-tests.mjs +14 -0
  275. package/tools/track/run-box.mjs +168 -0
  276. package/tools/track/setup-box.sh +59 -0
  277. package/tools/track/study-2d.mjs +728 -0
  278. package/dist/assets/analytics-B1hnH66c.js +0 -1
  279. package/dist/assets/app-BWusbqgO.js +0 -4861
  280. package/dist/assets/app-qKDo4PBX.css +0 -1
  281. package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
  282. package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
  283. package/dist/assets/workflow-CVwzMhz2.js +0 -179
  284. package/dist/assets/workflow-DsudxKHF.css +0 -1
  285. package/src/fal-motion-studio.jsx +0 -180
  286. 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
- export const CLAY = "#f2eee6";
486
- export const CLAY_B = "#ddd6ca";
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 = "#faf8f2";
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
- // TRAIL_TRACKS order is limbs-first, hips last: an overlapping grab
2735
- // prefers the finer limb target, and among candidates within the
2736
- // threshold the closest line wins.
2737
- let track = null;
2738
- let grabFrame = 0;
2739
- let bestDistance = Infinity;
2740
- for (const candidate of pick.tracks) {
2741
- const near = nearestFrameToRay(candidate.flat, origin, direction, 0.2);
2742
- if (near && near.distance < bestDistance) {
2743
- track = candidate.id;
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, normalise = null } = {}) {
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
- // BIND TRANSLATIONS, HERE, FOR THIS CHAIN ONLY. solveIk resets them anyway —
1216
- // its segment lengths were measured at bind and its law of cosines is exact
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
- * BIND TRANSLATIONS, PER CHAIN, AT SOLVE TIME. solveIk resets a chain to its
1292
- * bind translations the moment it runs — its segment lengths were measured at
1293
- * bind and its law of cosines is exact only there — so the push target is read
1294
- * AFTER that reset, on the same skeleton the solver will produce. That is what
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, normalise: normaliseChain,
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: bind translations (the
2047
- * pose a keyed chain is evaluated at, so it is the pose worth measuring) and
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
  }
@@ -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);