cozyclay 2.1.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,24 +1,11 @@
1
- import { useState, useEffect, useSyncExternalStore, useContext, useRef, useMemo } from "react";
2
- import { AppContext } from '../app-context.js';
1
+ import { useState, useEffect, useSyncExternalStore, useRef, useMemo } from "react";
3
2
  import { createDocumentStore } from '../document-store.js';
4
3
  import { useDocumentDomain } from '../store/use-document-store.js';
5
- import { createPhysicsProgress } from "../ardy/physics-panel.jsx";
6
4
  import {
7
- TIMELINE_FPS,
8
5
  hierarchyIdForIkFocus,
9
- ARDY_PRESERVE_DEFAULT,
10
6
  REST_BONES,
11
- ARDY_SEED_MAX,
12
- lineTrackLabel,
13
- ARDY_DURATION_MIN,
14
- buildPromptSchedule,
15
- posePlacementFrame,
16
- toArdySegments,
17
- ARDY_FPS,
18
- toArdyFrame,
19
7
  } from "../app-stage.jsx";
20
- import { copyPhysicsKeys, physicsKeyStamp, reviewAutoPhysics } from "../ardy/physics-review.js";
21
- import { fitPlatforms } from "../ardy/platform-fit.js";
8
+ import { copyPhysicsKeys } from "../motion/physics-review.js";
22
9
  import {
23
10
  createIkState,
24
11
  ikTouch,
@@ -39,84 +26,46 @@ import {
39
26
  ikBakeKeyframe,
40
27
  ikEvaluate,
41
28
  resolveIkRig,
42
- } from "../ardy/ik.js";
29
+ } from "../motion/ik.js";
43
30
  import * as THREE from "three";
44
31
  import { StudioProtocolError } from "../studio-agent-protocol.js";
45
- import { restorePlaybackBones, snapshotPlaybackBones, applyMotionFrame, captureArdyRoot } from "../ardy/playback.js";
32
+
33
+ import { applyTakeFrame } from "../motion3/playback.js";
46
34
  import { isKo, ko } from "../locale.js";
47
- import { characterScaleFor, loadMotionFromUrl } from "../ardy/npz.js";
48
- import { supportHeightForObject, OBJECT_LIBRARY } from "../scene-objects.js";
49
- import { applySupportRise, autoRoofDrop, applyAutoFall, applyRootDrop, normalizeRootDrop } from "../ardy/root-drop.js";
50
35
  import { isProxyFigure } from "../scenes.js";
51
- import { characterCapabilities, kindRefusal, refuseRigOnly } from "../character-kind.js";
52
- import { retimeMotion } from "../ardy/retime.js";
53
- import {
54
- createMotionEdit,
55
- motionEditLayout,
56
- renderMotionEdit,
57
- remapFrameKeyMap,
58
- remapTimelineFrame,
59
- } from "../ardy/motion-edit.js";
36
+ import { refuseRigOnly } from "../character-kind.js";
60
37
  import { DEFAULT_POSE, restoreBindPositions, applyPose, applyHipsOffset } from "../poses.js";
61
- import { normalizeMotionCalibration, applyMotionCalibration } from "../ardy/motion-calibration.js";
62
- import { collisionBlockers } from "../ardy/collision-blockers.js";
63
- import { fixCollisions, fixCollisionsRange } from "../ardy/fix-collisions.js";
64
- import { trackFeature, startMotionRequest, motionPreflightReason, trackActivation } from "../analytics.js";
65
- import { buildArdyPose } from "../ardy/export.js";
66
- import { slateLine } from "../shot.js";
67
- import { motionReadiness } from "../motion-readiness.js";
68
- import { motionReadinessMessage } from "../motion-readiness-ui.jsx";
69
- import {
70
- resolveSeed,
71
- stripSourceMotion,
72
- blocksFromRequest,
73
- replayPayload,
74
- freshRecipe,
75
- withLineEdit,
76
- pushTakeVersion,
77
- TAKE_VERSIONS_MAX,
78
- } from "../take-recipe.js";
79
- import { buildGenerationRequest, generationRefusal } from '../motion/generation.js';
80
- import { planPosePin } from '../ardy/pose-pin.js';
81
- import { TRAIL_TRACKS, applyTrailFalloffDelta, restoreTrailEdits, trailEditRange, worldDeltaToTrailClip } from "../motion-trail.js";
82
- import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../ardy/ik-drag.js";
83
- import { ikKeyJson, ikTrackKeyFromJson } from "../ardy/ik-key-json.js";
84
- import { findAbsoluteIkKeyConflicts } from "../trail-key-conflicts.js";
38
+ import { bakeIkDragKey, bakeIkDragRange, chainsChangedBy, ikDragRecord, ikDragTouch } from "../motion/ik-drag.js";
39
+ import { ikKeyJson, ikTrackKeyFromJson } from "../motion/ik-key-json.js";
85
40
  import { chooseIkEntryPose } from "../ik-camera.js";
86
- import { generate as ardyGenerate } from "../ardy/client.js";
87
- import { isLineEditUnsupported } from "../line-edit.js";
88
- import { openMotionDb, getMotion, putMotion } from "../motion-store.js";
89
- import { resolveMotionSource, decodeMotionResource, encodeMotionResource, sha256Hex } from "../motion-resources.js";
90
- import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../ardy/range-pin.js";
41
+ import { applyRangePin, captureRangePinTarget, normalizeRangePin, rangePinTargetWorld, rangePinTracks, removeRangePinKeys } from "../motion/range-pin.js";
91
42
  import { rangePinObjectMatrixAt } from "../range-pin-object-transform.js";
92
- import { createMotionVideoCapture } from "./motion/motion-video-capture.js";
93
43
  import { ensureRangePinState, overlappingRangePin, rangePinConflictFrames, rangePinOverlapMessage } from "./motion/motion-range-pin-rules.js";
94
44
 
45
+ import { takeSource, normalizeTakeSource, compileTakeSource, renderTakeEdit } from "./motion/take-source.js";
95
46
  import { sameMotionIntent, emptyMotionLayer, encodeMotionKeys, decodeMotionKeys, cloneRangePin, encodeRangePins, decodeRangePins, encodeRangePinResiduals, decodeRangePinResiduals } from "./motion/motion-intent-codec.js";
96
47
  // Plain intent owns take identities and JSON keys. Decoded buffers and rig
97
48
  // preimages are copy-on-record runtime resources, never mutable store values.
98
49
  export function createMotionDomain(appContext, characters) {
99
- const takes = new Map(), decoded = new WeakMap(), rigImages = new WeakMap(), previews = new Map(), prepared = new Map();
50
+ const takes = new Map(), decoded = new WeakMap(), rigImages = new WeakMap();
100
51
  let running = 0;
101
52
  const references = entries => entries.map(entry => {
102
- const ref = entry.motionRef;
103
- const take = ref ? { resourceId: `restore:${entry.id}:${ref.motionId ?? ref.url}`, url: ref.url ?? null, anchorX: ref.anchorX, anchorZ: ref.anchorZ,
104
- rotationDeg: ref.rotationDeg, prompt: ref.prompt ?? '', ...(ref.trailEdits ? { trailEdits: ref.trailEdits } : {}), ...(ref.studioTakeId ? { studioTakeId: ref.studioTakeId } : {}) } : null;
53
+ const saved = normalizeTakeSource(entry.motionTake), take = saved ? restoredTake(entry.id, saved) : null;
105
54
  // The saved Pose-mode / IK edits come back with the scene (scenes.js ikEdits).
106
55
  const edits = entry.ikEdits;
107
- return { id: entry.id, take, fullTake: take, takeVersions: ref?.url ? [{ motionUrl: ref.url, recipe: null, savedAt: Date.now(), label: ko('Loaded', '불러옴') }] : [],
108
- ikKeys: edits?.keys ?? [], ikPins: edits?.pins ?? [], ikPinResiduals: edits?.pinResiduals ?? [] };
56
+ return { id: entry.id, take, fullTake: take, ikKeys: edits?.keys ?? [], ikPins: edits?.pins ?? [], ikPinResiduals: edits?.pinResiduals ?? [] };
109
57
  });
110
- function snapshotTake(take) {
58
+ // A take v3 keeps its source (spec, placement, scene) in the authored layer, so a reload
59
+ // or an undo past a pruned resource compiles the same bones again (take-source.js).
60
+ function snapshotTake(take, resourceId = crypto.randomUUID(), source = null, stored = true) {
111
61
  if (!take) return null;
112
62
  if (take.resourceId) return { ...take };
113
- const resourceId = crypto.randomUUID(), copy = structuredClone(take);
114
- takes.set(resourceId, copy);
115
- return { resourceId, frames: take.frames, fps: take.fps, url: take.url ?? null, prompt: take.prompt ?? '',
116
- anchorX: take.anchorX ?? 0, anchorZ: take.anchorZ ?? 0, anchorFrame: take.anchorFrame ?? 0, rotationDeg: take.rotationDeg ?? 0,
117
- ...(take.trailEdits ? { trailEdits: take.trailEdits } : {}),
118
- editSegments: take.editSegments ?? createMotionEdit(take.frames), studioTakeId: take.studioTakeId ?? resourceId };
63
+ if (stored) takes.set(resourceId, structuredClone(take));
64
+ source ??= takeSource(take, appContext.shared.storeRef?.current?.objects ?? appContext.shared.sceneObjects);
65
+ return { resourceId, frames: take.frames, fps: take.fps, prompt: take.prompt ?? '', studioTakeId: take.studioTakeId ?? resourceId, ...(source ? { source } : {}), ...(take.edit ? { edit: take.edit } : {}) };
119
66
  }
67
+ const restoredTake = (id, { takeId, edit, ...source }) => snapshotTake({ schema: source.schema, frames: Math.round(source.spec.frames), fps: 24,
68
+ prompt: source.spec.label ?? '', studioTakeId: takeId, edit }, `restore:${id}:${takeId ?? 'take-v3'}`, source, false);
120
69
  const normalize = rows => rows.map(row => ({ ...emptyMotionLayer(row.id), ...row,
121
70
  take: snapshotTake(row.take), fullTake: snapshotTake(row.fullTake ?? row.take),
122
71
  ikKeys: row.ikKeys ?? [], ikPins: row.ikPins ?? [], ikPinResiduals: row.ikPinResiduals ?? [], committedIkEdits: row.committedIkEdits ?? [] }));
@@ -128,15 +77,19 @@ export function createMotionDomain(appContext, characters) {
128
77
  const read = () => documentStore.read('motion');
129
78
  const layer = id => read().find(row => row.id === id) ?? emptyMotionLayer(id);
130
79
  function materialize(take) {
80
+ if (take?.source && !takes.has(take.resourceId)) {
81
+ try { const compiled = compileTakeSource(take.source); if (compiled) takes.set(take.resourceId, compiled); }
82
+ catch (error) { console.warn('[cozyclay] could not compile the saved take', error); }
83
+ }
131
84
  if (!take || !takes.has(take.resourceId)) return null;
132
85
  if (!decoded.has(take)) {
133
86
  const { resourceId, ...metadata } = take;
134
- decoded.set(take, { ...takes.get(resourceId), ...metadata });
87
+ decoded.set(take, renderTakeEdit({ ...takes.get(resourceId), ...metadata }));
135
88
  }
136
89
  return decoded.get(take);
137
90
  }
138
91
  const motionFor = id => materialize(layer(id).take), fullMotionFor = id => materialize(layer(id).fullTake);
139
- const visibleMotion = id => previews.get(id) ?? motionFor(id);
92
+ const visibleMotion = motionFor;
140
93
  const frame = () => appContext.live.state?.timeline.currentFrame ?? 0;
141
94
  const rigFor = id => appContext.shared.rigs[id];
142
95
  // Rig-only tools refuse a capsule figure by name before looking for its rig.
@@ -160,9 +113,9 @@ export function createMotionDomain(appContext, characters) {
160
113
  const rig = rigFor(id);
161
114
  if (rig) {
162
115
  const resolved = resolveIkRig(rig); if (resolved) Object.assign(state, resolved, { rig });
163
- if (savedRigs?.has(id) && !previews.has(id)) appContext.shared.restoreExportRig(savedRigs.get(id));
116
+ if (savedRigs?.has(id)) appContext.shared.restoreExportRig(savedRigs.get(id));
164
117
  else if (resolved) {
165
- if (take) applyMotionFrame(rig, take, Math.min(frame(), take.frames - 1));
118
+ if (take) applyTakeFrame(rig, take, Math.min(frame(), take.frames - 1));
166
119
  else {
167
120
  const character = cast.read().find(entry => entry.id === id);
168
121
  restoreBindPositions(rig); applyPose(rig, { ...REST_BONES, ...(character?.pose ?? DEFAULT_POSE).bones });
@@ -176,7 +129,6 @@ export function createMotionDomain(appContext, characters) {
176
129
  const id = appContext.shared.loadedLayerCharRef.current;
177
130
  appContext.shared.ikStateRef.current = states.get(id) ?? createIkState();
178
131
  appContext.shared.bufferRef.current = { ...appContext.shared.bufferRef.current, motion: visibleMotion(id), ik: appContext.shared.ikStateRef.current };
179
- appContext.shared.takeRecipeRef.current = layer(id).takeRecipe;
180
132
  domain.onProject?.(); viewRevision++;
181
133
  }
182
134
  const unsubscribe = documentStore.subscribe(project);
@@ -270,68 +222,24 @@ export function createMotionDomain(appContext, characters) {
270
222
  for (const [track, key] of incoming) entry.set(track, key); state.keys.set(at, entry);
271
223
  });
272
224
  }
273
- function castWrite(update) { return appContext.recordAction('cast', () => appContext.storeDomain('cast').write(update), null, true); }
274
225
  function synchronizeTimeline() { appContext.storeDomain('cast').syncTimeline(); }
275
- function replace(id, take, { fullTake = take, ikKeys = [], ikPins = [], ikPinResiduals = [],
276
- recipe = take ? { seed: null, blocks: [{ prompt: take.prompt ?? '', duration: take.frames / take.fps }], lineEdits: [] } : null,
277
- versions = take?.url ? pushTakeVersion(layer(id).takeVersions, { motionUrl: take.url, recipe, savedAt: Date.now(), label: ko('Loaded', '불러옴') }, TAKE_VERSIONS_MAX) : layer(id).takeVersions,
278
- } = {}) {
279
- previews.delete(id);
280
- writeLayer(id, { take: snapshotTake(take), fullTake: snapshotTake(fullTake), ikKeys, ikPins, ikPinResiduals, takeRecipe: recipe, takeVersions: versions, committedIkEdits: [] });
226
+ // Install a take (motion.compile): one descriptor serves as both the visible and the full take.
227
+ function replace(id, take, { ikKeys = [], ikPins = [], ikPinResiduals = [] } = {}) {
228
+ const descriptor = snapshotTake(take);
229
+ writeLayer(id, { take: descriptor, fullTake: descriptor, ikKeys, ikPins, ikPinResiduals, committedIkEdits: [] });
281
230
  synchronizeTimeline();
282
231
  }
283
232
  function clear(id) {
284
- previews.delete(id);
285
- writeLayer(id, { take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [], takeRecipe: null });
286
- castWrite(rows => rows.map(row => row.id === id ? { ...row, scale: 1, motionRef: null,
287
- layer: { ...row.layer, promptClips: [] } } : row));
233
+ writeLayer(id, { take: null, fullTake: null, ikKeys: [], ikPins: [], ikPinResiduals: [], committedIkEdits: [] });
288
234
  synchronizeTimeline();
289
235
  }
290
- function editSegments(id, segments) {
291
- const full = fullMotionFor(id), take = motionFor(id);
292
- if (!full || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'This character has no take to edit.');
293
- const previous = take.editSegments, rendered = { ...renderMotionEdit(full, segments), url: null };
294
- const keys = remapFrameKeyMap(decodeMotionKeys(layer(id).ikKeys), previous, segments);
295
- writeLayer(id, { take: snapshotTake(rendered), ikKeys: encodeMotionKeys(keys) });
296
- castWrite(rows => rows.map(row => row.id !== id ? row : { ...row, layer: { ...row.layer,
297
- promptClips: row.layer.promptClips.flatMap(clip => {
298
- const start = remapTimelineFrame(previous, segments, clip.startFrame), end = remapTimelineFrame(previous, segments, clip.endFrame);
299
- if (start === null && end === null) return [];
300
- const startFrame = Math.max(0, Math.min(start ?? 0, rendered.frames - 1));
301
- const endFrame = Math.max(0, Math.min(end ?? rendered.frames - 1, rendered.frames - 1));
302
- return startFrame <= endFrame ? [{ ...clip, startFrame, endFrame }] : [];
303
- }) } }));
304
- synchronizeTimeline();
305
- }
306
- function fix(id, scope = 'frame') {
307
- rigOnly(id, 'collision');
308
- const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
309
- if (!resolved) throw new StudioProtocolError('TARGET_NOT_READY', 'The character rig is not loaded.');
310
- const state = appContext.shared.ikStatesRef.current.get(id) ?? { ...createIkState(), ...resolved };
311
- const blockers = at => collisionBlockers({ rigs: appContext.shared.rigs, activeId: id, characterIds: appContext.live.characters,
312
- sceneObjects: appContext.ports.read().objects, library: OBJECT_LIBRARY, frame: at, take: { frameCount: motionFor(id)?.frames ?? 1, fps: 24 } });
313
- if (scope === 'clip') {
314
- const take = motionFor(id); if (!take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take first.');
315
- const originals = rigSnapshots();
316
- try {
317
- fixCollisionsRange({ rig, ...resolved, ikState: state, startFrame: 0, endFrame: take.frames - 1,
318
- applyFrame(at) { applyMotionFrame(rig, take, at); ikEvaluate(resolved.chains, state, at, resolved.fkJoints, 6); },
319
- blockersAt(at) { for (const row of read()) if (row.id !== id && rigFor(row.id)) appContext.shared.poseMemberAtFrame(rigFor(row.id), motionFor(row.id), appContext.shared.ikStatesRef.current.get(row.id), at, 6); return blockers(at); } });
320
- } finally { for (const snapshot of originals.values()) appContext.shared.restoreExportRig(snapshot); }
321
- } else {
322
- const result = fixCollisions(rig, resolved.chains, { ikState: state, fkJoints: resolved.fkJoints, blockers: blockers(frame()) });
323
- if (!result.supported) throw new StudioProtocolError('TARGET_NOT_READY', 'This rig does not support collision cleanup.');
324
- if (result.changed) ikBakeKeyframe(resolved.chains, state, frame(), resolved.fkJoints, result.touched, null, result.baseQuats);
325
- }
326
- setKeys(id, state.keys);
327
- }
328
236
  function bakeCurrentKey(id, at, dragIds = null) {
329
237
  const rig = rigFor(id), resolved = rig && resolveIkRig(rig);
330
238
  if (!resolved) return null;
331
239
  const take = motionFor(id);
332
240
  if (dragIds) {
333
241
  const keyed = bakeIkDragKey(resolved.chains, resolved.fkJoints, at, dragIds, take
334
- ? (rawRig) => applyMotionFrame(rawRig, take, at)
242
+ ? (rawRig) => applyTakeFrame(rawRig, take, at)
335
243
  : null);
336
244
  if (keyed && take) for (const key of keyed.values()) key.blend = 6;
337
245
  return keyed ? run('ik.setKey', { characterId: id, frame: at, tracks: ikKeyJson(keyed) }) : null;
@@ -351,71 +259,6 @@ export function createMotionDomain(appContext, characters) {
351
259
  ikBakeKeyframe(resolved.chains, state, at, resolved.fkJoints);
352
260
  });
353
261
  }
354
- function editTrail(id, { track = "hips", grabFrame, radiusFrames, delta }) {
355
- rigOnly(id, 'trails');
356
- const take = motionFor(id), character = appContext.storeDomain('cast').read().find(row => row.id === id);
357
- if (!take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take first.');
358
- const scale = character.scale ?? 1;
359
- const rig = rigFor(id);
360
- const clipDelta = worldDeltaToTrailClip(take, delta, { track, grabFrame, radiusFrames, rig, scale });
361
- const edited = snapshotTake(applyTrailFalloffDelta(take, { track, grabFrame, radiusFrames, clipDelta }));
362
- writeLayer(id, { take: edited });
363
- castWrite(rows => rows.map(row => row.id === id ? { ...row, motionRef: { ...row.motionRef, trailEdits: edited.trailEdits } } : row));
364
- }
365
- const physicsReviews = new Map();
366
- let physicsJob = 0;
367
- const physicsStamp = id => JSON.stringify([layer(id), appContext.storeDomain('cast').read().find(row => row.id === id)]);
368
- function applyPhysics(id) {
369
- rigOnly(id, 'physics');
370
- const preview = physicsReviews.get(id);
371
- if (!preview || preview.stamp !== physicsStamp(id)) throw new StudioProtocolError('STALE_TARGET', 'Analyse this take again before applying corrections.');
372
- setKeys(id, preview.result.candidate.keys);
373
- domain.onPhysicsPreview?.(null);
374
- }
375
- async function autoPhysics(id, options, context) {
376
- rigOnly(id, 'physics');
377
- const rig = rigFor(id), resolved = rig && resolveIkRig(rig), take = motionFor(id);
378
- if (!resolved || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take and character rig first.');
379
- const job = ++physicsJob, stamp = physicsStamp(id), original = appContext.shared.snapshotExportRig(rig);
380
- let lastYield = performance.now();
381
- const check = () => { context.check(); if (job !== physicsJob) throw new StudioProtocolError('STALE_TARGET', 'Physics analysis was superseded.'); };
382
- domain.onPhysicsRunning?.(true); domain.onPhysicsPreview?.(null);
383
- try {
384
- check();
385
- const result = await reviewAutoPhysics({ rig, ...resolved, motion: take, sourceKeys: decodeMotionKeys(layer(id).ikKeys),
386
- applyRaw: at => applyMotionFrame(rig, take, at), sceneObjects: appContext.ports.read().objects,
387
- ...options, cache: appContext.shared.physicsSourceCacheRef.current, onProgress: value => domain.onPhysicsProgress?.(value),
388
- yieldFrame: async () => {
389
- check(); if (performance.now() - lastYield < 16) return;
390
- appContext.shared.restoreExportRig(original);
391
- await new Promise(resolve => { const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0); });
392
- lastYield = performance.now(); check();
393
- },
394
- });
395
- appContext.shared.restoreExportRig(original); check();
396
- physicsReviews.set(id, { result, stamp });
397
- if (options.apply) context.commit(() => applyPhysics(id)); else domain.onPhysicsPreview?.(result);
398
- return { before: result.before, after: result.after, changedFrames: result.changedFrames.length, warnings: result.warnings, unresolved: result.unresolved };
399
- } finally {
400
- if (job === physicsJob) { if (stamp === physicsStamp(id)) appContext.shared.restoreExportRig(original); domain.onPhysicsRunning?.(false); }
401
- }
402
- }
403
- function runPrepared(characterId, apply) {
404
- if (running) return apply();
405
- let result;
406
- const token = crypto.randomUUID(); prepared.set(token, () => { result = apply(); return result; });
407
- try { run('motion.applyPrepared', { characterId, token }); return result; }
408
- finally { prepared.delete(token); }
409
- }
410
- function persistTake(take, motionRef) {
411
- if (!take?.sourceBytes) return;
412
- (async () => {
413
- let record = appContext.shared.motionEncodingCacheRef.current.get(take.sourceBytes);
414
- if (!record) { record = await encodeMotionResource(take.sourceBytes, { prompt: motionRef?.prompt, sourceUrl: motionRef?.url }); appContext.shared.motionEncodingCacheRef.current.set(take.sourceBytes, record); }
415
- appContext.shared.projectMotionsRef.current.set(record.motionId.toLowerCase(), record);
416
- const db = await openMotionDb(); try { await putMotion(db, record); } finally { db.close(); }
417
- })().catch(error => console.warn('[cozyclay] could not cache motions', error));
418
- }
419
262
  function run(id, args) {
420
263
  const result = appContext.bus.run(id, args);
421
264
  const checked = receipt => { if (!receipt.ok) throw new StudioProtocolError(receipt.code, receipt.message); return receipt; };
@@ -445,15 +288,14 @@ export function createMotionDomain(appContext, characters) {
445
288
  return run('run.update', { txId: gesture.txId, args });
446
289
  }
447
290
  const domain = { documentStore, read, write, layer, writeLayer, motionFor, fullMotionFor, visibleMotion, snapshotTake, project,
448
- setKeys, setRangePinState, bakeRangePin, removePin, editKeys, setKey, replace, clear, editSegments, fix, castWrite, run, edit, beginGesture, finishGesture, beginAction, runPrepared, persistTake,
449
- insideAction: () => running > 0, bakeCurrentKey, keyPose, editTrail, autoPhysics, applyPhysics,
450
- applyPrepared(token) { const apply = prepared.get(token); if (!apply) throw new StudioProtocolError('STALE_TARGET', 'Prepared motion is no longer available.'); return apply(); },
291
+ setKeys, setRangePinState, bakeRangePin, removePin, editKeys, setKey, replace, clear, run, edit, beginGesture, finishGesture, beginAction,
292
+ insideAction: () => running > 0, bakeCurrentKey, keyPose,
451
293
  bindRender(context) { appContext = context; },
452
294
  viewRevision: () => viewRevision, canUndo: id => documentStore.canUndo(id),
453
295
  stepHistory: redo => { finishGesture(); return Boolean((redo ? documentStore.redo : documentStore.undo)()); },
454
296
  document: () => ({ motion: read().map(row => {
455
297
  const take = motionFor(row.id);
456
- return take ? { ...row, take: { ...row.take, frames: take.frames, fps: take.fps, editSegments: take.editSegments } } : row;
298
+ return take ? { ...row, take: { ...row.take, frames: take.frames, fps: take.fps } } : row;
457
299
  }) }), sceneSlice: slices => slices.motion ?? references(slices.cast),
458
300
  publish: rows => write(rows), commitDraft: write,
459
301
  switchLayer(id) { appContext.shared.loadedLayerCharRef.current = id; project(); notify(); },
@@ -465,29 +307,13 @@ export function createMotionDomain(appContext, characters) {
465
307
  frameCount: state.frameCount, committedIkEdits: layer(id).committedIkEdits, renderer: rig ? appContext.shared.snapshotExportRig(rig) : null };
466
308
  },
467
309
  removeLayer(id) { write(rows => rows.filter(row => row.id !== id)); },
468
- preview(id, take) { if (take) previews.set(id, take); else previews.delete(id); project(); notify(); },
469
- hydrate(id, take, reference) {
470
- const current = appContext.storeDomain('cast').read().find(row => row.id === id);
471
- const descriptor = layer(id).take;
472
- if (!descriptor || !sameMotionIntent(current?.motionRef, reference)) return false;
473
- takes.set(descriptor.resourceId, structuredClone(take));
474
- for (const rows of [read(), ...documentStore.history().past.map(entry => entry.snapshot.motion), ...documentStore.history().future.map(entry => entry.snapshot.motion)]) {
475
- for (const row of rows) for (const value of [row.take, row.fullTake]) if (value?.resourceId === descriptor.resourceId) decoded.delete(value);
476
- }
477
- notify(); return true;
478
- },
479
- load(rows) { finishGesture(true); previews.clear(); release(); native.dispose(); native = createDocumentStore({ owned: { motion: normalize(rows) } }); release = native.subscribe(notify); pruneTakes(); notify(); },
310
+ load(rows) { finishGesture(true); release(); native.dispose(); native = createDocumentStore({ owned: { motion: normalize(rows) } }); release = native.subscribe(notify); pruneTakes(); notify(); },
480
311
  dispose() { finishGesture(true); unregister(); unsubscribe(); release(); native.dispose(); listeners.clear(); takes.clear(); },
481
312
  };
482
313
  const unregister = appContext.registerStoreDomain('motion', domain);
483
314
  return domain;
484
315
  }
485
316
 
486
- export function useMotionCommands() {
487
- const app = useContext(AppContext), owner = app.storeDomain('motion');
488
- return { run: (id, args = {}) => owner.run(id, { characterId: app.storeDomain('cast').activeId, ...args }) };
489
- }
490
-
491
317
  export function useMotion(appContext) {
492
318
  const [domain] = useState(() => appContext.storeDomain('motion') ?? createMotionDomain(appContext, appContext.shared.characters));
493
319
  domain.bindRender(appContext);
@@ -498,7 +324,6 @@ export function useMotion(appContext) {
498
324
  const start = () => domain.beginGesture(); window.addEventListener('pointerdown', start, true);
499
325
  return () => { window.removeEventListener('pointerdown', start, true); domain.finishGesture(true); };
500
326
  }, [domain, appContext.shared.activeChar.id]);
501
- const [i2vMotion, setI2vMotion] = useState({ a: null, b: null, job: null, status: "idle", error: "", instruction: "", dailyRemaining: null });
502
327
  // IK posing for Subject 1: dragging a wrist/ankle handle FOCUSES that
503
328
  // joint and solves its chain backward (two-bone analytic IK) on top of
504
329
  // the current pose; joints never dragged stay purely on the FK pose.
@@ -534,44 +359,14 @@ export function useMotion(appContext) {
534
359
  // hide the seam, short enough that a mid-clip fix stays visibly local.
535
360
  const IK_CORRECTION_BLEND_FRAMES = 6;
536
361
 
537
- const [autoPhysicsRunning, setAutoPhysicsRunning] = useState(false);
538
-
539
- const [physicsPreview, setPhysicsPreview] = useState(null);
540
-
541
- const [physicsShow, setPhysicsShow] = useState(true);
542
-
543
- const [physicsProgress] = useState(createPhysicsProgress);
544
-
545
- const setPhysicsProgress = physicsProgress.set;
546
-
547
- const [physicsOptions, setPhysicsOptions] = useState({ overrides: [], protectedFrames: [], strength: 1 });
548
-
549
- const [platformFitRunning, setPlatformFitRunning] = useState(false);
550
- const [platformFitLast, setPlatformFitLast] = useState(null);
551
- const [platformFitProgress] = useState(createPhysicsProgress);
552
- const platformFitProgressSet = platformFitProgress.set;
553
- const platformFitJobRef = useRef(0);
554
- const platformFitAppliedRef = useRef(null);
555
- const [platformFitApplied, setPlatformFitApplied] = useState(false);
556
-
557
362
  const [ikTick, setIkTick] = useState(0);
558
363
 
559
364
  const committedIkEdits = domain.layer(appContext.shared.activeChar.id).committedIkEdits;
560
365
  const setCommittedIkEdits = update => domain.writeLayer(appContext.shared.activeChar.id, { committedIkEdits: typeof update === 'function' ? update(domain.layer(appContext.shared.activeChar.id).committedIkEdits) : update });
561
366
  domain.onProject = () => setIkTick(value => value + 1);
562
367
 
563
- /* Grabbing the viewport trajectory line deforms the loaded take with a
564
- * smoothstep falloff around the grab frame (pure local preview). The last
565
- * finished drag stays pending so "Regenerate from trail edit" can send the
566
- * auto-derived window through the existing motionEdit pipeline. */
567
- const [trailFalloffS, setTrailFalloffS] = useState(0.5);
568
-
569
- const [showTrails, setShowTrails] = useState(true);
570
-
368
+ // The Pose-mode tool: "ik" (drag handles) or "pin" (range pins).
571
369
  const [ikEditTool, setIkEditTool] = useState("ik");
572
-
573
- const [trailEdit, setTrailEdit] = useState(null);
574
- const [trailTrackFocus, setTrailTrackFocus] = useState("hips");
575
370
  const [rangePinSelection, setRangePinSelection] = useState(null);
576
371
  const [rangePinPartPick, setRangePinPartPick] = useState(null);
577
372
  const [rangePinPreview, setRangePinPreview] = useState(null);
@@ -579,8 +374,6 @@ export function useMotion(appContext) {
579
374
  const rangePins = useMemo(() => encodeRangePins(pinLayer.ikPins), [pinLayer.ikPins]);
580
375
  const rangePinResiduals = useMemo(() => decodeRangePinResiduals(pinLayer.ikPinResiduals), [pinLayer.ikPinResiduals]);
581
376
 
582
- const trailFalloffFrames = Math.max(1, Math.round(trailFalloffS * TIMELINE_FPS));
583
-
584
377
  function focusIkHandle(focus) {
585
378
  setIkFocus(focus);
586
379
  const hierarchyId = hierarchyIdForIkFocus(focus, appContext.shared.rowIdForCharIndex(appContext.shared.activeCharIndex));
@@ -589,17 +382,10 @@ export function useMotion(appContext) {
589
382
  }
590
383
  }
591
384
 
592
- function selectedIkRange(frame = appContext.shared.tlFrame) {
385
+ // An IK correction applies across the whole installed take.
386
+ function selectedIkRange() {
593
387
  const count = Math.max(1, motion?.frames ?? appContext.shared.tlFrameCount ?? 1);
594
- if (!motion) return { startFrame: 0, endFrame: count - 1 };
595
- const edit = motion.editSegments ?? createMotionEdit(count);
596
- const layout = motionEditLayout(edit);
597
- const at = Math.max(0, Math.min(count - 1, Math.round(frame) || 0));
598
- const selected = layout.find((segment) => at >= segment.timelineStart && at <= segment.timelineEnd) ?? layout[0];
599
- return {
600
- startFrame: Math.max(0, selected?.timelineStart ?? 0),
601
- endFrame: Math.min(count - 1, selected?.timelineEnd ?? count - 1),
602
- };
388
+ return { startFrame: 0, endFrame: count - 1 };
603
389
  }
604
390
 
605
391
  function setPendingIkEditValue(value) {
@@ -659,19 +445,6 @@ export function useMotion(appContext) {
659
445
  return true;
660
446
  }
661
447
 
662
- function clearTrailTrackFocus() {
663
- setTrailTrackFocus(null);
664
- setIkFocus(null);
665
- }
666
- function selectTrailTrack(track) {
667
- setTrailTrackFocus(track);
668
- focusIkHandle(track);
669
- }
670
-
671
- useEffect(() => {
672
- if (!ikMode || ikEditTool !== "trail") setTrailTrackFocus(TRAIL_TRACKS.some(track => track.id === ikFocus) ? ikFocus : "hips");
673
- }, [ikMode, ikEditTool, ikFocus]);
674
-
675
448
  function rangePinObjectWorldMatrix(objectId, frame) {
676
449
  const object = appContext.shared.sceneObjects.find((entry) => entry.id === objectId);
677
450
  if (!object) return null;
@@ -817,85 +590,6 @@ export function useMotion(appContext) {
817
590
  return count;
818
591
  }
819
592
 
820
- const [bridge, setBridge] = useState(null);
821
-
822
- const [bridgeChecking, setBridgeChecking] = useState(false);
823
-
824
- const [motionSetupReveal, setMotionSetupReveal] = useState(0);
825
-
826
- const [motionSetupKind, setMotionSetupKind] = useState("prompt");
827
-
828
- const [ardyPrompt, setArdyPrompt] = useState("");
829
-
830
- const [ardyDuration, setArdyDuration] = useState(4);
831
-
832
- // Optional native-ARDY seed: empty string = omit from the request (the
833
- // box picks a fresh random one each run); otherwise a plain integer in
834
- // 0..2**31-1 to reproduce a result.
835
- const [ardySeed, setArdySeed] = useState("");
836
-
837
- // How much of the loaded take a regeneration keeps (scheduled inpainting).
838
- // 1 = hold the take everywhere the user did not edit, 0 = ignore it and
839
- // generate fresh. Only ever consulted when the take still has a bridge
840
- // source to preserve FROM, so the control renders with the take, not with
841
- // the panel.
842
- const [preserveStrength, setPreserveStrength] = useState(ARDY_PRESERVE_DEFAULT);
843
-
844
- // Off by default on purpose: pinning runs the box's pose mode, which builds
845
- // on a fixed reference base, so it is a choice the operator makes when they
846
- // actually want the pose in the generated clip.
847
- const [ardyStartFromPose, setArdyStartFromPose] = useState(false);
848
-
849
- // WHERE the pose lands in the clip. "start" leaves from it, "end" arrives at
850
- // it, "middle" passes through it, "playhead" places it on the frame the
851
- // operator scrubbed to — the box takes any destination frame.
852
- const [ardyPosePlacement, setArdyPosePlacement] = useState("start");
853
-
854
- /* The recipe of the take that is loaded RIGHT NOW: seed + prompt blocks +
855
- * the line edits pulled on top of it. It is written in exactly one place
856
- * (commitTakeRecipe, on a successful run) and read in two: the request
857
- * assembly, which attaches it as C10 `replay`, and the version strip, which
858
- * stores a copy beside every motionUrl so clicking v1 restores v1's recipe
859
- * and not the one the artist has since edited into existence.
860
- * The ref shadows the state because the recipe is consulted from inside an
861
- * async job completion, where a stale closure would silently record the
862
- * previous take's edits against this take's url. */
863
- const takeRecipe = domain.layer(appContext.shared.activeChar.id).takeRecipe;
864
- const setTakeRecipe = value => domain.writeLayer(appContext.shared.activeChar.id, { takeRecipe: value });
865
- const takeVersions = domain.layer(appContext.shared.activeChar.id).takeVersions;
866
- const setTakeVersions = update => domain.writeLayer(appContext.shared.activeChar.id, { takeVersions: typeof update === 'function' ? update(domain.layer(appContext.shared.activeChar.id).takeVersions) : update });
867
-
868
- // Which C10 replay entries came back failed or boundary-warned. Non-blocking
869
- // by contract: the take generated, one refinement may not have survived it,
870
- // and that is worth a line next to the take rather than a toast that scrolls
871
- // away before the artist has looked at the result.
872
- const [replayNotices, setReplayNotices] = useState([]);
873
-
874
- // Whether the Scene entry's action menu is open. The Refine entry needs no
875
- // equivalent — it IS its action.
876
- const [sceneMenuOpen, setSceneMenuOpen] = useState(false);
877
-
878
- const [ardyRunning, setArdyRunning] = useState(false);
879
-
880
- const [ardyStatus, setArdyStatus] = useState("");
881
-
882
- // Keep the latest ARDY status inline with the Prompt Blocks controls. The
883
- // former bottom Console history was removed because it duplicated this state
884
- // and exposed an editor surface that is not part of the production workflow.
885
- function reportArdyStatus(line) {
886
- setArdyStatus(line);
887
- }
888
-
889
- const [ardyReport, setArdyReport] = useState(null);
890
-
891
- const [ardyOutcome, setArdyOutcome] = useState(null);
892
-
893
- // Wave-2 gate: the bridge only routes lineEdit once M4's routing lands.
894
- // Until the /ardy/health payload says so, the request is never sent —
895
- // today's bridge ignores unknown fields, so an ungated POST would quietly
896
- // return a fresh unrelated take instead of an edit.
897
- const [lineEditBackend, setLineEditBackend] = useState(false);
898
-
899
593
  // Loaded motion: decoded arrays plus the world anchor captured at load.
900
594
  const motion = domain.visibleMotion(appContext.shared.activeChar.id);
901
595
  useEffect(() => {
@@ -907,19 +601,6 @@ export function useMotion(appContext) {
907
601
  }, [appContext.shared.activeChar.id, appContext.shared.activeRig, motion, pinLayer, ikMode, ikEditTool]);
908
602
  const rangePinRebuildRef = useRef(null);
909
603
  const rangePinSourceStamp = JSON.stringify([pinLayer.take, appContext.shared.activeChar.id]);
910
- // A fit is invalidated by a different take, trim or preview, not by key
911
- // writes: every layer write re-snapshots the take descriptor, so the decoded
912
- // `motion` object is new after the fit's own commit even though the take is
913
- // unchanged. Key the reset on the descriptor's content instead.
914
- const platformFitPreview = motion === domain.motionFor(appContext.shared.activeChar.id) ? null : motion;
915
- useEffect(() => {
916
- platformFitJobRef.current += 1;
917
- setPlatformFitRunning(false);
918
- setPlatformFitLast(null);
919
- platformFitProgressSet(0);
920
- platformFitAppliedRef.current = null;
921
- setPlatformFitApplied(false);
922
- }, [rangePinSourceStamp, platformFitPreview, appContext.shared.activeRig]);
923
604
  // Object-bound pins follow edited prop transforms and paths, as in the
924
605
  // source branch. Rebuilding derived keys must not create an undo entry.
925
606
  useEffect(() => {
@@ -941,33 +622,7 @@ export function useMotion(appContext) {
941
622
  }, [rangePinSourceStamp, ikChains]);
942
623
 
943
624
 
944
- const [motionBusy, setMotionBusy] = useState(false);
945
-
946
- const [motionError, setMotionError] = useState("");
947
-
948
- const [multiModelUrl, setMultiModelUrl] = useState("");
949
-
950
- const [multiModelSource, setMultiModelSource] = useState(null);
951
-
952
- const [multiModelStatus, setMultiModelStatus] = useState("idle");
953
-
954
- // The ingest is a real download + decode, so it owns real progress and a
955
- // real receipt: the footage numbers below come from the decoded media.
956
- const [multiModelStage, setMultiModelStage] = useState("idle");
957
-
958
- const [multiModelProgress, setMultiModelProgress] = useState(null);
959
-
960
- const [multiModelFootage, setMultiModelFootage] = useState(null);
961
-
962
- const [multiModelError, setMultiModelError] = useState("");
963
-
964
- const [multiModelTake, setMultiModelTake] = useState(null);
965
-
966
- const [multiModelExtract, setMultiModelExtract] = useState("idle");
967
625
 
968
- const [multiModelExtractProgress, setMultiModelExtractProgress] = useState(null);
969
-
970
- const [multiModelExtractError, setMultiModelExtractError] = useState("");
971
626
 
972
627
  function advanceFrame(steps = 1) {
973
628
  const count = Math.max(1, Math.floor(steps));
@@ -1001,192 +656,11 @@ export function useMotion(appContext) {
1001
656
  setIkFocus(null);
1002
657
  }
1003
658
 
1004
- /** Hand `rig` over to playback. Every path that starts a clip — a loaded
1005
- * take, a browser-baked one — does the same two things: drop out of IK
1006
- * EDIT mode (playback is the new context; the IK KEYS stay and keep
1007
- * correcting the clip layer-style) and swap the pre-playback bone
1008
- * baseline, so a re-load never snapshots mid-animation and clearing
1009
- * always returns to the blocking pose. */
1010
- function beginPlaybackOn(rig) {
1011
- leaveIkMode();
1012
- const previous = appContext.shared.restoreRef.current;
1013
- appContext.shared.restoreRef.current = null;
1014
- if (previous) restorePlaybackBones(previous.rig, previous.bones);
1015
- appContext.shared.restoreRef.current = { rig, bones: snapshotPlaybackBones(rig) };
1016
- }
1017
-
1018
- /** Land takes 1..N-1 on the rest of the cast. Each extra take is its OWN
1019
- * layer: it goes to that entry's sessionMotion, NOT through the editing
1020
- * buffer, which holds the active character's clip alone. Returns how many
1021
- * performers actually landed. */
1022
- const authoredSupportDescriptors = () => appContext.shared.sceneObjects.map((object) => ({
1023
- x: object.x,
1024
- z: object.z,
1025
- rotDeg: object.rot ?? 0,
1026
- supportY: (object.y ?? 0) + supportHeightForObject(object) * (object.scaleY ?? 1),
1027
- topY: (object.y ?? 0) + supportHeightForObject(object) * (object.scaleY ?? 1),
1028
- width: (object.footprint?.width ?? 0) * (object.scaleX ?? 1),
1029
- depth: (object.footprint?.depth ?? 0) * (object.scaleZ ?? 1),
1030
- }));
1031
-
1032
- const applyAuthoredSupportRise = (clip, anchor, rotationDeg, worldScale = characterScaleFor(clip)) => applySupportRise(clip, authoredSupportDescriptors(), {
1033
- subjectX: anchor.x,
1034
- subjectY: anchor.y ?? 0,
1035
- subjectZ: anchor.z,
1036
- rotationDeg,
1037
- worldScale,
1038
- });
1039
-
1040
- function commitLoadedTake(characterId, take, { recipe = null, job = null, promptClips = null, scale = characterScaleFor(take) } = {}) {
1041
- const owned = appContext.storeDomain('motion');
1042
- const previous = owned.layer(characterId);
1043
- const imported = recipe ?? (job ? previous.takeRecipe : { seed: null, blocks: [{ prompt: take.prompt, duration: take.frames / take.fps }], lineEdits: [] });
1044
- const versions = recipe || job ? previous.takeVersions : pushTakeVersion(previous.takeVersions, {
1045
- motionUrl: take.url, recipe: imported, savedAt: Date.now(), label: ko('Loaded', '불러옴'),
1046
- }, TAKE_VERSIONS_MAX);
1047
- owned.replace(characterId, take, { recipe: imported, versions });
1048
- const motionRef = { url: take.url, prompt: take.prompt, rotationDeg: take.rotationDeg, anchorX: take.anchorX, anchorZ: take.anchorZ,
1049
- calibration: take.sceneCalibration, ...(take.trailEdits ? { trailEdits: take.trailEdits } : {}), ...(take.motionId ? { motionId: take.motionId } : {}) };
1050
- owned.castWrite(rows => rows.map(row => row.id === characterId ? { ...row, scale, motionRef,
1051
- layer: promptClips ? { ...row.layer, promptClips } : row.layer } : row));
1052
- if (job) {
1053
- commitTakeRecipe(job, take.url);
1054
- if (job.hasBlockEdits) owned.writeLayer(characterId, { committedIkEdits: job.committedEditKeys });
1055
- }
1056
- owned.persistTake(take, motionRef);
1057
- }
1058
-
1059
- // Decoded motion + the world anchor: frame 0 always starts at Subject 1.
1060
- // Authored root destinations are generated by ARDY as sparse constraints,
1061
- // so playback consumes the returned trajectory without coordinate warping.
1062
- async function loadMotion(
1063
- url,
1064
- prompt,
1065
- rotationDeg = appContext.shared.charA.rot,
1066
- drop = null,
1067
- targetCharacterId = appContext.shared.activeChar.id,
1068
- targetPromptClips = null,
1069
- // `preview: true` means "put this clip on screen, do not treat it as a
1070
- // new take". The line-edit preview loop swaps the viewport several times
1071
- // a minute, and every announcement this function normally makes — the
1072
- // load toast, the auto-drop toast, clearing the IK keys, snapping the
1073
- // playhead back to 0 — is an announcement about a take CHANGING. A
1074
- // preview is the same take seen a second time, so it makes none of them.
1075
- { preview = false, calibration = null, tutorialEpoch = null, commandContext = null, recipe = null, job = null, personScaleFallback = null } = {},
1076
- ) {
1077
- setMotionBusy(true);
1078
- setMotionError("");
1079
- try {
1080
- refuseRigOnly(appContext.live.characters.find((entry) => entry.id === targetCharacterId), "take");
1081
- // Inbound boundary: an ARDY take (20 fps) or a filmed one (30/60)
1082
- // becomes a production-clock clip here, once, before anything on
1083
- // the timeline counts its frames. Same-rate input rides through.
1084
- // A drop is staging applied to the clip itself, so it happens at
1085
- // the same boundary — trims and IK then see the dropped take.
1086
- const retimed = retimeMotion(await loadMotionFromUrl(url), TIMELINE_FPS);
1087
- if (retimed.sourceBytes) retimed.motionId = await sha256Hex(retimed.sourceBytes);
1088
- if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
1089
- const normalizedCalibration = normalizeMotionCalibration(calibration);
1090
- // Scene yaw/XY translation belong to the character's scene transform.
1091
- // Applying them to both the arrays and the Character group would rotate
1092
- // the trajectory twice and leave rotMats facing the old direction.
1093
- const playbackCalibration = { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 };
1094
- // Scene calibration is optional metadata from the capture boundary. It
1095
- // runs before support/fall staging so every downstream measurement uses
1096
- // the same scene-space coordinates.
1097
- const raw = applyMotionCalibration(retimed, playbackCalibration).motion;
1098
- // Staging descriptors are authored in scene metres while decoded
1099
- // trajectories are canonical-body units. Resolve stature before any
1100
- // support or fall math so a 0.8x/1.2x performer still lands exactly on
1101
- // the same authored surface after playback multiplies the clip.
1102
- const motionScale = characterScaleFor(raw);
1103
- const targetCharacter = appContext.live.characters.find((entry) => entry.id === targetCharacterId);
1104
- if (!targetCharacter) throw new Error(`Motion target ${targetCharacterId} no longer exists.`);
1105
- const sceneAnchorX = targetCharacter.x + normalizedCalibration.offsetX;
1106
- const sceneAnchorZ = targetCharacter.z + normalizedCalibration.offsetZ;
1107
- const sceneRotationDeg = rotationDeg + normalizedCalibration.yawDeg;
1108
- const rig = appContext.shared.rigs[targetCharacter.id] ?? await appContext.shared.waitForRig(targetCharacter.id);
1109
- if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
1110
- // No explicit drop staged: a character standing on a raised object
1111
- // whose take walks off the edge falls on its own — ARDY motion is
1112
- // flat-ground, so the stage supplies the gravity.
1113
- const supports = authoredSupportDescriptors();
1114
- // A support's top is scene data, never guessed from the motion. Apply
1115
- // only when the clip shows an upward root trend entering its footprint;
1116
- // ordinary deck walks remain byte-for-byte unchanged.
1117
- const raised = drop ? raw : applySupportRise(raw, supports, {
1118
- subjectX: sceneAnchorX,
1119
- subjectY: targetCharacter.y ?? 0,
1120
- subjectZ: sceneAnchorZ,
1121
- rotationDeg: sceneRotationDeg,
1122
- worldScale: motionScale,
1123
- });
1124
- const staging = drop ?? autoRoofDrop(
1125
- raised,
1126
- { x: sceneAnchorX, z: sceneAnchorZ, y: targetCharacter.y ?? 0, rotationDeg: sceneRotationDeg },
1127
- supports,
1128
- { worldScale: motionScale },
1129
- );
1130
- const decoded = drop ? applyRootDrop(raised, staging, { worldScale: motionScale }) : applyAutoFall(raised, staging, { worldScale: motionScale });
1131
- if (!drop && staging && !preview) {
1132
- appContext.notify((isKo, ko) => ko(
1133
- `Auto drop staged: the take leaves its support at ${staging.fromS.toFixed(1)}s and falls ${staging.meters.toFixed(1)}m`,
1134
- `자동 낙하 적용: ${staging.fromS.toFixed(1)}초에 지지면을 벗어나 ${staging.meters.toFixed(1)}m 낙하`,
1135
- ));
1136
- }
1137
- const targetStillExists = appContext.live.characters.some((entry) => entry.id === targetCharacter.id);
1138
- if (!targetStillExists) throw new Error(`Motion target ${targetCharacterId} no longer exists.`);
1139
- const apply = () => {
1140
- const bufferOwnsTarget = targetCharacter.id === appContext.shared.loadedLayerCharRef.current;
1141
- beginPlaybackOn(rig);
1142
- // THE INVARIANT: the take's travel assumes the character is scaled.
1143
- // Extraction divided the root translation by the filmed person's
1144
- // stature, so the clip and that stature have to be applied together
1145
- // or every stride overshoots by the same factor and the feet skate.
1146
- // The scale rides INSIDE the npz, so this one line covers every path
1147
- // that loads a motion; an ARDY-generated take stores none and is
1148
- // canonical, 1.
1149
- const measuredScale = characterScaleFor(decoded);
1150
- const scale = measuredScale === 1 && Number.isFinite(personScaleFallback) ? characterScaleFor(null, personScaleFallback) : measuredScale;
1151
- const loaded = {
1152
- // Identity calibration retains the legacy frame-zero anchorX: targetCharacter.x
1153
- // and anchorZ: targetCharacter.z contract; calibrated takes use the scene anchor.
1154
- // Capture the exact prompt this motion was generated from; the
1155
- // timeline keeps showing it even if the input field is edited
1156
- // afterwards.
1157
- prompt: typeof prompt === "string" ? prompt : "",
1158
- ...decoded,
1159
- url,
1160
- anchorX: sceneAnchorX,
1161
- anchorZ: sceneAnchorZ,
1162
- anchorFrame: 0,
1163
- rotationDeg: sceneRotationDeg,
1164
- sceneCalibration: normalizedCalibration,
1165
- editSegments: createMotionEdit(decoded.frames),
1166
- };
1167
- const owned = appContext.storeDomain('motion');
1168
- if (preview) owned.preview(targetCharacter.id, url === owned.motionFor(targetCharacter.id)?.url ? null : loaded);
1169
- else {
1170
- commitLoadedTake(targetCharacter.id, loaded, { recipe, job, promptClips: targetPromptClips, scale });
1171
- if (bufferOwnsTarget) { appContext.shared.setTlFrame(0); appContext.shared.setTlPlaying(false); }
1172
- setMotionError('');
1173
- }
1174
- return scale;
1175
- };
1176
- return preview ? apply() : commandContext ? commandContext.commit(apply)
1177
- : appContext.storeDomain('motion').runPrepared(targetCharacterId, apply);
1178
- } catch (err) {
1179
- if (tutorialEpoch !== null && tutorialEpoch !== appContext.shared.tutorialProjectEpochRef.current) return null;
1180
- setMotionError(err?.message || String(err));
1181
- throw err;
1182
- } finally {
1183
- setMotionBusy(false);
1184
- }
1185
- }
659
+
1186
660
 
1187
- /** Drop the ACTIVE character's take, its IK corrections, prompt blocks and
1188
- * the stature the take imposed. One Ctrl+Z entry brings all four back; nothing to clear
1189
- * records nothing, so the shortcut never becomes a dead press.
661
+ /** Drop the ACTIVE character's take and its IK corrections. One Ctrl+Z
662
+ * entry brings both back; nothing to clear records nothing, so the shortcut
663
+ * never becomes a dead press.
1190
664
  *
1191
665
  * The pose flows (studio Apply/Reset, pose tiles, photo pose) call this
1192
666
  * first and then write the pose: the snapshot taken here predates both, so
@@ -1195,40 +669,8 @@ export function useMotion(appContext) {
1195
669
  return appContext.storeDomain('motion').clear(appContext.shared.activeChar.id);
1196
670
  }
1197
671
 
1198
- /** Cut the ACTIVE character's take to [start, end] of the CURRENT view.
1199
- * Offsets compose, so a second cut still slices the original take; a cut
1200
- * take drops its bridge url — its frames no longer match the source npz,
1201
- * and IK-edit regeneration must not pretend they do. Per-character layers
1202
- * mean the cut lands on this layer only; nobody else's clip moves. */
1203
- function applyMotionTrim(start, end) {
1204
- return appContext.storeDomain('motion').edit('motion.trim', { characterId: appContext.shared.activeChar.id, start, end });
1205
- }
1206
-
1207
- function resetMotionTrim() {
1208
- return appContext.storeDomain('motion').run('motion.resetTrim', { characterId: appContext.shared.activeChar.id });
1209
- }
1210
-
1211
- function cutMotionAtPlayhead() {
1212
- return appContext.storeDomain('motion').run('motion.cut', { characterId: appContext.shared.activeChar.id, frame: appContext.shared.tlFrame });
1213
- }
1214
-
1215
- function changeMotionSegmentSpeed(id, speed) {
1216
- return appContext.storeDomain('motion').run('motion.setSegmentSpeed', { characterId: appContext.shared.activeChar.id, id, speed });
1217
- }
1218
-
1219
- /** Drop one Full-Body segment from the take. The removal composes like a
1220
- * trim: the source frames stay untouched, so the trim-reset path (right-click
1221
- * an outer handle) still restores the whole take. */
1222
- function removeMotionSegmentById(id) {
1223
- return appContext.storeDomain('motion').run('motion.removeSegment', { characterId: appContext.shared.activeChar.id, id });
1224
- }
1225
-
1226
672
  // Everyone EXCEPT the active character, posed at an absolute frame from
1227
- // their own session clips and their STORED IK corrections (#77). Factored
1228
- // out of the effect below because the whole-clip Fix Collisions pass needs
1229
- // the same thing: the other bodies are static blockers, and a blocker
1230
- // sampled from a rig still standing at the playhead would block the wrong
1231
- // volume on every other frame of the walk.
673
+ // their own session clips and their STORED IK corrections (#77).
1232
674
  const poseOtherCastMembers = (frame) => {
1233
675
  for (const entry of appContext.shared.characters) {
1234
676
  if (entry.id === appContext.shared.activeChar.id || isProxyFigure(entry)) continue;
@@ -1241,9 +683,8 @@ export function useMotion(appContext) {
1241
683
  // Authoring modes are mutually exclusive: IK mode swaps the main pane to
1242
684
  // the poser camera, which would silently invalidate any pulled path
1243
685
  // anyway. Leaving the line mode explicitly says so instead.
1244
- if (next && appContext.shared.lineEditMode) appContext.shared.exitLineEditMode();
1245
686
  if (next) {
1246
- // Always enter on the safe, detailed IK tool. Trail editing is an
687
+ // Always enter on the safe, detailed IK tool. Range pins are an
1247
688
  // explicit second tool and must never leave the regular handles
1248
689
  // locked when the user re-enters IK mode.
1249
690
  setIkEditTool("ik");
@@ -1291,9 +732,11 @@ export function useMotion(appContext) {
1291
732
  // Exit: the keyed pose stays — the evaluate effect re-applies the
1292
733
  // current frame's keyed rotations the moment ikMode flips, so nothing
1293
734
  // the user authored is lost by toggling. Untracked/unkeyed parts keep
1294
- // their current (FK) pose.
735
+ // their current (FK) pose. A correction still in preview is applied, as
736
+ // Apply range would: leaving the mode (the Stage tab, a mode key) is not
737
+ // a Cancel, and dropping it threw away what the user just posed.
738
+ if (!applyPendingIkEdit()) appContext.notify(ko("Pose mode off — keyed poses keep playing", "포즈 모드 꺼짐 — 키로 찍은 포즈는 계속 재생됩니다"));
1295
739
  leaveIkMode();
1296
- appContext.notify(ko("Pose mode off — keyed poses keep playing", "포즈 모드 꺼짐 — 키로 찍은 포즈는 계속 재생됩니다"));
1297
740
  }
1298
741
 
1299
742
  // Drag solve, routed by handle kind: chain targets solve the two-bone
@@ -1448,283 +891,6 @@ export function useMotion(appContext) {
1448
891
  appContext.notify(isKo ? `${appContext.shared.tlFrame}프레임에 전신 IK 키를 추가했어요` : `Full-body IK key at frame ${appContext.shared.tlFrame}`);
1449
892
  }
1450
893
 
1451
- // Self-collision cleanup: push interpenetrating body parts apart with the
1452
- // IK solver, then bake the fix as an ordinary IK correction key so it
1453
- // survives scrubs, undo, and blends back into the clip outside its range.
1454
- //
1455
- // The result has THREE outcomes, not two, and each gets its own sentence.
1456
- // "supported: false" means the rig has no capsule proxies to build at all
1457
- // — a non-Mixamo skeleton — and reporting that as "no collisions" would
1458
- // be a lie the user cannot act on: they would keep clicking a button that
1459
- // silently does nothing. The buttons below are disabled in that case, so
1460
- // this branch is the belt to that suspenders (a rig can be swapped under
1461
- // a stale render).
1462
- /**
1463
- * Everything OUTSIDE the active character that a limb has to stay out of:
1464
- * the other cast members' bodies (capsules, built from the rigs AS THEY ARE
1465
- * POSED at the moment of the call) and every scene object (an upright box
1466
- * on its footprint). Ids are namespaced `char:<id>:<capsule>` / `obj:<id>`,
1467
- * so a penetration label names the thing that was hit.
1468
- *
1469
- * `frame` re-samples travel paths — a prop walking a route stands somewhere
1470
- * else on every frame — but the CAST is read live from the scene graph, so
1471
- * a caller walking a clip must pose the others at that frame first (see
1472
- * runFixCollisionsRange's blockersAt).
1473
- */
1474
- const externalBlockers = (frame = appContext.shared.tlFrame) => collisionBlockers({
1475
- rigs: appContext.shared.rigs,
1476
- activeId: appContext.shared.activeChar.id,
1477
- // The CAST is the authority on who is on stage, not the rig map: undo can
1478
- // put the cast list back to one subject while the rig mounted for the
1479
- // removed one is still in `rigs`, and a ghost body would go on blocking
1480
- // limbs that pass through empty space.
1481
- characterIds: appContext.shared.characters,
1482
- sceneObjects: appContext.shared.sceneObjects,
1483
- library: OBJECT_LIBRARY,
1484
- frame,
1485
- take: { frameCount: appContext.shared.tlFrameCount, fps: appContext.shared.tlFps },
1486
- });
1487
-
1488
- function runFixCollisions() {
1489
- if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.fixCollisions', { characterId: appContext.shared.activeChar.id, scope: 'frame' });
1490
- if (!ikChains || !appContext.shared.activeRig) return;
1491
- // A rig swap leaves one render where ikChains still describes the old
1492
- // skeleton; solving the new rig with them would push the wrong bones.
1493
- if (appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return;
1494
- // The set as it stands right now: the other bodies at this frame's pose
1495
- // and the props at this frame's placement.
1496
- const result = fixCollisions(appContext.shared.activeRig, ikChains, { ikState: appContext.shared.ikStateRef.current, fkJoints: ikFkJoints, blockers: externalBlockers(appContext.shared.tlFrame) });
1497
- if (!result.supported) {
1498
- appContext.notify(ko("This rig doesn't support collision cleanup", "이 리그는 신체 관통 정리를 지원하지 않아요"));
1499
- return;
1500
- }
1501
- if (!result.changed) {
1502
- appContext.notify(ko("No body collisions at this frame", "이 프레임에는 신체 관통이 없어요"));
1503
- return;
1504
- }
1505
- editIkKeys(() => ikBakeKeyframe(ikChains, appContext.shared.ikStateRef.current, appContext.shared.tlFrame, ikFkJoints, result.touched, null, result.baseQuats));
1506
- setIkTick((n) => n + 1);
1507
- appContext.notify(result.residual > 1e-4
1508
- ? ko(`Collisions reduced (residual ${(result.residual * 100).toFixed(1)} cm)`, `관통을 줄였어요 (잔여 ${(result.residual * 100).toFixed(1)} cm)`)
1509
- : ko(`Collisions fixed at frame ${appContext.shared.tlFrame}`, `프레임 ${appContext.shared.tlFrame}의 관통을 정리했어요`));
1510
- }
1511
-
1512
- // Whole-clip variant: walk the motion frame by frame, clean each pose and
1513
- // key ONLY the frames that changed, so a clean clip stays keyless.
1514
- function runFixCollisionsRange() {
1515
- if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.fixCollisions', { characterId: appContext.shared.activeChar.id, scope: 'clip' });
1516
- if (!ikChains || !appContext.shared.activeRig || !motion) return;
1517
- if (appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return;
1518
- // Screened before the undo entry: an unsupported rig would record an
1519
- // undo step for a walk that keys nothing, leaving a no-op in history.
1520
- if (!appContext.shared.collisionCleanupSupported) {
1521
- appContext.notify(ko("This rig doesn't support collision cleanup", "이 리그는 신체 관통 정리를 지원하지 않아요"));
1522
- return;
1523
- }
1524
- const currentFrame = appContext.shared.tlFrame;
1525
- const applyFrame = (frame) => {
1526
- applyMotionFrame(appContext.shared.activeRig, motion, frame);
1527
- ikEvaluate(ikChains, appContext.shared.ikStateRef.current, frame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
1528
- };
1529
- // The other bodies move too. blockersAt runs AFTER applyFrame(frame), so
1530
- // it poses the rest of the cast at that same frame — their own clips and
1531
- // their own stored IK layers, the very pass the viewport renders with —
1532
- // and only then samples their capsules. Without this the blockers would
1533
- // describe everyone frozen at the playhead, which is a wrong obstacle on
1534
- // every frame but one.
1535
- const blockersAt = (frame) => {
1536
- poseOtherCastMembers(frame);
1537
- return externalBlockers(frame);
1538
- };
1539
- let keyed = [];
1540
- let unresolved = [];
1541
- try {
1542
- const walked = editIkKeys(() => fixCollisionsRange({
1543
- rig: appContext.shared.activeRig,
1544
- chains: ikChains,
1545
- ikState: appContext.shared.ikStateRef.current,
1546
- fkJoints: ikFkJoints,
1547
- startFrame: 0,
1548
- endFrame: motion.frames - 1,
1549
- applyFrame,
1550
- blockersAt,
1551
- }));
1552
- // The frames the walk keyed. `unresolved` — the frames whose residual
1553
- // survived every pass — is ADDITIVE: read it defensively off either
1554
- // shape so this keeps working before and after the driver grows it.
1555
- keyed = Array.isArray(walked) ? walked : walked?.keyed ?? [];
1556
- unresolved = (Array.isArray(walked) ? walked.unresolved : walked?.unresolved) ?? [];
1557
- } finally {
1558
- // The restore is the pass's CLEANUP, not its epilogue: a throw mid-walk
1559
- // would otherwise leave the active rig and the rest of the cast frozen
1560
- // at whatever frame it died on, which is a wrong-looking set the user
1561
- // cannot scrub out of without touching the playhead.
1562
- applyFrame(currentFrame);
1563
- poseOtherCastMembers(currentFrame);
1564
- setIkTick((n) => n + 1);
1565
- }
1566
- // Residual is worth saying out loud: a limb pinned between two blockers
1567
- // (another body and a prop, say) can come out of the walk still touching,
1568
- // and silence would read as "all clean".
1569
- const stillPenetrating = unresolved.length
1570
- ? ko(` · ${unresolved.length} frame(s) still penetrate`, ` · ${unresolved.length}개 프레임은 남아 있어요`)
1571
- : "";
1572
- // "No body collisions" must never share a sentence with "still
1573
- // penetrate": a converged pass over an unfixable clip has nothing more
1574
- // to do, which is a different statement from the clip being clean.
1575
- appContext.notify((keyed.length
1576
- ? ko(`Fixed collisions on ${keyed.length} frame(s)`, `${keyed.length}개 프레임의 관통을 정리했어요`)
1577
- : unresolved.length
1578
- ? ko("Nothing more to fix", "더 고칠 수 있는 게 없어요")
1579
- : ko("No body collisions in the clip", "클립에 신체 관통이 없어요")) + stillPenetrating);
1580
- }
1581
-
1582
- function changePhysicsOptions(next) {
1583
- setPhysicsOptions(next); setPhysicsPreview(null); setIkTick((n) => n + 1);
1584
- }
1585
-
1586
- function showPhysicsPreview(show) { setPhysicsShow(show); setIkTick((n) => n + 1); }
1587
-
1588
- function cancelPhysicsPreview() { setPhysicsPreview(null); setIkTick((n) => n + 1); }
1589
-
1590
- function applyPhysicsPreview() {
1591
- if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.applyPhysics', { characterId: appContext.shared.activeChar.id });
1592
- if (!physicsPreview || physicsPreview.sourceStamp !== physicsKeyStamp(appContext.shared.ikStateRef.current.keys)) return;
1593
- editIkKeys(() => { appContext.shared.ikStateRef.current.keys = copyPhysicsKeys(physicsPreview.candidate.keys); });
1594
- appContext.shared.ikStateRef.current.tracked = new Set(physicsPreview.candidate.tracked);
1595
- appContext.shared.autoPhysicsRunRef.current = { motion, rig: appContext.shared.activeRig, stamp: physicsKeyStamp(appContext.shared.ikStateRef.current.keys) };
1596
- setPhysicsPreview(null); setIkTick((n) => n + 1);
1597
- appContext.notify(ko("AutoPhysics applied · Undo restores the original", "오토피직스를 적용했어요 · 실행 취소로 원본 복구"));
1598
- }
1599
-
1600
- async function runAutoPhysics() {
1601
- if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.autoPhysics', { characterId: appContext.shared.activeChar.id, ...physicsOptions, apply: false });
1602
- if (autoPhysicsRunning || !ikChains || !appContext.shared.activeRig || !motion || appContext.shared.ikStateRef.current.rig !== appContext.shared.activeRig) return null;
1603
- const previous = appContext.shared.autoPhysicsRunRef.current;
1604
- if (previous?.motion === motion && previous.rig === appContext.shared.activeRig && previous.stamp === physicsKeyStamp(appContext.shared.ikStateRef.current.keys)) {
1605
- appContext.notify(ko("Already applied. Undo to review this correction again.", "이미 적용했어요. 실행 취소 후 다시 비교할 수 있어요.")); return null;
1606
- }
1607
- const job = ++appContext.shared.physicsJobRef.current, frame = appContext.shared.tlFrame;
1608
- const sourceKeys = copyPhysicsKeys(appContext.shared.ikStateRef.current.keys), stamp = physicsKeyStamp(sourceKeys);
1609
- let lastYieldAt = Date.now(), yieldWaitMs = 0, yieldCount = 0;
1610
- const restore = () => { appContext.shared.poseMemberAtFrame(appContext.shared.activeRig, motion, appContext.shared.ikStateRef.current, frame, IK_CORRECTION_BLEND_FRAMES); };
1611
- appContext.shared.setTlPlaying(false); setAutoPhysicsRunning(true); setPhysicsProgress(0); setPhysicsPreview(null);
1612
- try {
1613
- const result = await reviewAutoPhysics({ rig: appContext.shared.activeRig, motion, chains: ikChains, fkJoints: ikFkJoints, sourceKeys,
1614
- applyRaw: (f) => appContext.shared.poseMemberAtFrame(appContext.shared.activeRig, motion, null, f), sceneObjects: appContext.shared.sceneObjects, ...physicsOptions,
1615
- cache: appContext.shared.physicsSourceCacheRef.current,
1616
- onProgress: setPhysicsProgress,
1617
- yieldFrame: async () => {
1618
- if (appContext.shared.physicsJobRef.current !== job) throw new Error("Analysis cancelled after changing the character or motion");
1619
- // A batch is a cancellation checkpoint, not necessarily a paint/
1620
- // event-loop boundary. Yield on a time budget, not every 12 frames.
1621
- if (Date.now() - lastYieldAt < 16) return;
1622
- restore();
1623
- const queuedAt = Date.now();
1624
- // Yield CPU work without waiting for a paint. requestAnimationFrame
1625
- // can be throttled/paused in an occluded tab, stretching a seconds-
1626
- // long solve into minutes. MessageChannel also lets input run.
1627
- await new Promise((resolve) => {
1628
- const channel = new MessageChannel();
1629
- channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); };
1630
- channel.port2.postMessage(0);
1631
- });
1632
- lastYieldAt = Date.now(); yieldWaitMs += lastYieldAt - queuedAt; yieldCount += 1;
1633
- if (appContext.shared.physicsJobRef.current !== job) throw new Error("Analysis cancelled after changing the character or motion");
1634
- },
1635
- });
1636
- Object.assign(result.performance, { yieldWaitMs, yieldCount });
1637
- if (appContext.shared.physicsJobRef.current !== job || physicsKeyStamp(appContext.shared.ikStateRef.current.keys) !== stamp) return null;
1638
- setPhysicsPreview(result); setPhysicsShow(true);
1639
- return { before: result.before, after: result.after, warnings: result.warnings, unresolved: result.unresolved, contacts: result.contacts.spans };
1640
- } catch (error) {
1641
- if (appContext.shared.physicsJobRef.current === job) appContext.notify(ko(`AutoPhysics: ${error.message}`, `오토피직스: ${error.message}`));
1642
- return null;
1643
- } finally {
1644
- if (appContext.shared.physicsJobRef.current === job) { restore(); setAutoPhysicsRunning(false); setIkTick((n) => n + 1); }
1645
- }
1646
- }
1647
-
1648
- async function platformFitRun(id, context, { removal = false, preview = false } = {}) {
1649
- const rig = appContext.shared.rigs[id], resolved = rig && resolveIkRig(rig), take = domain.motionFor(id);
1650
- if (!resolved || !take) throw new StudioProtocolError('TARGET_NOT_READY', 'Load a take and character rig first.');
1651
- if (platformFitRunning) throw new StudioProtocolError('TARGET_BUSY', 'A platform fit is already running.');
1652
- const job = ++platformFitJobRef.current;
1653
- const sourceKeys = copyPhysicsKeys(decodeMotionKeys(domain.layer(id).ikKeys));
1654
- const sourceTracked = new Set([...sourceKeys.values()].flatMap(entry => [...entry.keys()]));
1655
- const stamp = physicsKeyStamp(sourceKeys);
1656
- const prior = platformFitAppliedRef.current;
1657
- const stacked = prior && prior.id === id && prior.stamp === stamp ? prior : null;
1658
- const frame = appContext.shared.tlFrame;
1659
- const restore = () => appContext.shared.poseMemberAtFrame(rig, take, appContext.shared.ikStatesRef.current.get(id), frame, 6);
1660
- let lastYield = performance.now();
1661
- const check = () => { context?.check?.(); if (platformFitJobRef.current !== job) throw new StudioProtocolError('STALE_TARGET', 'Platform fit was superseded.'); };
1662
- setPlatformFitRunning(true); platformFitProgressSet(0); setPlatformFitLast(null);
1663
- try {
1664
- const result = await fitPlatforms({
1665
- rig: rig, motion: take, chains: resolved.chains, fkJoints: resolved.fkJoints, sourceKeys,
1666
- applyRaw: at => appContext.shared.poseMemberAtFrame(rig, take, null, at),
1667
- sceneObjects: removal ? [] : (appContext.ports.read?.().objects ?? appContext.shared.sceneObjects),
1668
- onProgress: platformFitProgressSet,
1669
- yieldFrame: async () => {
1670
- check(); if (performance.now() - lastYield < 16) return;
1671
- restore();
1672
- await new Promise(resolve => { const channel = new MessageChannel(); channel.port1.onmessage = () => { channel.port1.close(); channel.port2.close(); resolve(); }; channel.port2.postMessage(0); });
1673
- lastYield = performance.now(); check();
1674
- },
1675
- });
1676
- check();
1677
- const last = { summary: result.summary, steps: result.steps, changedFrames: result.changedFrames };
1678
- if (removal) {
1679
- setPlatformFitLast(last);
1680
- if (result.changedFrames.length) context.commit(() => domain.setKeys(id, result.candidate.keys));
1681
- platformFitAppliedRef.current = null; setPlatformFitApplied(false); setPlatformFitLast(null);
1682
- return { ...last, removed: result.changedFrames.length > 0 };
1683
- }
1684
- if (!preview) {
1685
- if (result.changedFrames.length) context.commit(() => domain.setKeys(id, result.candidate.keys));
1686
- platformFitAppliedRef.current = { id, before: stacked?.before ?? sourceKeys, tracked: stacked?.tracked ?? sourceTracked, stamp: physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys)) };
1687
- setPlatformFitLast(last); setPlatformFitApplied(true);
1688
- return last;
1689
- }
1690
- platformFitAppliedRef.current = { preview: { id, sourceStamp: stamp, sourceKeys, sourceTracked, candidate: result.candidate, last }, before: stacked?.before ?? sourceKeys, tracked: stacked?.tracked ?? sourceTracked, stamp };
1691
- setPlatformFitLast({ ...last, candidate: result.candidate });
1692
- setPlatformFitApplied(false);
1693
- return last;
1694
- } finally {
1695
- if (platformFitJobRef.current === job) { restore(); setPlatformFitRunning(false); }
1696
- }
1697
- }
1698
-
1699
- function platformFitApply(id, context) {
1700
- const preview = platformFitAppliedRef.current?.preview;
1701
- if (!preview || preview.id !== id || platformFitRunning) return { applied: false };
1702
- const current = physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys));
1703
- if (current !== preview.sourceStamp) throw new StudioProtocolError('STALE_TARGET', 'The platform preview is out of date. Analyse this take again before applying.');
1704
- context.commit(() => domain.setKeys(id, copyPhysicsKeys(preview.candidate.keys)));
1705
- platformFitAppliedRef.current = { id, before: preview.sourceKeys, tracked: preview.sourceTracked, stamp: physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys)) };
1706
- setPlatformFitApplied(true);
1707
- setPlatformFitLast({ ...preview.last, candidate: undefined });
1708
- return { applied: true, changedFrames: preview.last.changedFrames };
1709
- }
1710
-
1711
- function platformFitRemove(id, context, { apply = false } = {}) {
1712
- if (apply) return platformFitApply(id, context);
1713
- const applied = platformFitAppliedRef.current;
1714
- if (applied?.preview?.id === id) {
1715
- platformFitAppliedRef.current = null; setPlatformFitLast(null); setPlatformFitApplied(false);
1716
- return { removed: false, cancelled: true };
1717
- }
1718
- if (!applied || applied.id !== id || platformFitRunning) return { removed: false };
1719
- const current = physicsKeyStamp(decodeMotionKeys(domain.layer(id).ikKeys));
1720
- if (current === applied.stamp) {
1721
- context.commit(() => domain.setKeys(id, copyPhysicsKeys(applied.before)));
1722
- platformFitAppliedRef.current = null; setPlatformFitApplied(false); setPlatformFitLast(null);
1723
- return { removed: true };
1724
- }
1725
- return platformFitRun(id, context, { removal: true });
1726
- }
1727
-
1728
894
  function ikDeleteKeyframe(frame) {
1729
895
  if (!appContext.shared.ikStateRef.current.keys.has(frame)) return;
1730
896
  appContext.shared.runStudioAction("character.removeIkKey", { characterId: appContext.shared.activeChar.id, frame });
@@ -1765,966 +931,22 @@ export function useMotion(appContext) {
1765
931
  return true;
1766
932
  }
1767
933
 
1768
- function recheckMotionHealth() {
1769
- setBridgeChecking(true);
1770
- return appContext.shared.bridgeRefreshRef.current().finally(() => setBridgeChecking(false));
1771
- }
1772
-
1773
- function requestMotionGeneration(surface, inputMode, body = {}) {
1774
- const request = startMotionRequest({ surface, input_mode: inputMode });
1775
- const options = { body, lineEditSupported: lineEditBackend };
1776
- // Record known readiness refusals even if existing input validation returns
1777
- // early. A pass waits for the fully packaged payload at the queue boundary.
1778
- // The queue independently checks readiness; telemetry failure cannot
1779
- // enable or disable generation.
1780
- if (motionPreflightReason(bridge, options)) {
1781
- request.preflight(bridge, options);
1782
- const readiness = motionReadiness(bridge, options);
1783
- appContext.notify(motionReadinessMessage(readiness), ({ loading: "Checking motion generation…", ready: "Ready for this motion request", not_configured: "No motion backend configured", unsupported_route: "This route cannot run the selected motion request" })[readiness] ?? "The motion backend is unavailable");
1784
- }
1785
- return request;
1786
- }
1787
-
1788
- // Optional native-ARDY seed. Empty = request omits seed; the raw string
1789
- // is kept as typed (trimmed only). runArdy validates the bridge contract
1790
- // (integer in 0..2**31-1) right before the request and toasts on a
1791
- // violation, so an invalid seed can never reach the bridge silently.
1792
- function changeArdySeed(value) {
1793
- setArdySeed(value.trim());
1794
- }
1795
-
1796
- /** THE SEED RULE (contract C9), enforced in ONE place so no take-creating
1797
- * call site can forget it: an empty field is rolled here, a typed one is
1798
- * kept exactly as typed, and either way a concrete integer comes back to be
1799
- * both SENT and RECORDED. A take whose seed was never written down cannot
1800
- * be rebuilt from its recipe, which is the one promise the whole recipe
1801
- * model rests on. Returns null after toasting when the typed value violates
1802
- * the bridge contract — the caller must then abandon the request. */
1803
- function takeSeed() {
1804
- try {
1805
- return resolveSeed(ardySeed, ARDY_SEED_MAX);
1806
- } catch {
1807
- appContext.notify((isKo, ko) => isKo ? `Seed는 0..${ARDY_SEED_MAX} 범위의 정수여야 해요. 비워 두면 자동으로 선택됩니다` : `Seed must be an integer in 0..${ARDY_SEED_MAX} — clear it to let the box pick one`);
1808
- return null;
1809
- }
1810
- }
1811
-
1812
- /** CONFIRM the pull: the full-quality run of exactly what the preview has
1813
- * been showing. Its own run mode — the body carries lineEdit and NOTHING
1814
- * else authored, because C6 makes it exclusive with preserve, waypoints,
1815
- * segments, regenerateSegments and motionEdit — and it is the one path that
1816
- * commits a recipe and a version. Same builder, same SESSION SEED and same
1817
- * curve as the last preview; only `preview: true` is absent, which is what
1818
- * buys the full step count. */
1819
- function runLineEdit() {
1820
- if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) return;
1821
- if (isProxyFigure(appContext.shared.activeChar)) {
1822
- appContext.notify(kindRefusal("lineEdit", isKo));
1823
- return;
1824
- }
1825
- const generationRequest = requestMotionGeneration("line_edit", "edit", { lineEdit: true });
1826
- if (!appContext.shared.takeSourceUrl) {
1827
- appContext.notify(ko("The current take has no bridge source — generate it once before editing a path", "현재 테이크에 브리지 원본이 없어요 — 궤적을 편집하기 전에 한 번 생성하세요"));
1828
- return;
1829
- }
1830
- // No curve object means no edit — an untouched path is the take's own
1831
- // trajectory, and sending it would ask the box to spend eight seconds
1832
- // reproducing what is already there.
1833
- if (!appContext.shared.lineEditPayload) {
1834
- appContext.notify(ko(
1835
- "Draw along the path, pull a dot, or pin a moment first",
1836
- "먼저 궤적을 따라 그리거나, 점을 잡아당기거나, 순간을 찍어 주세요",
1837
- ));
1838
- return;
1839
- }
1840
- if (!lineEditBackend) {
1841
- appContext.notify(ko(
1842
- "The line-editing backend is not connected yet",
1843
- "라인 편집 백엔드가 아직 연결 전이에요",
1844
- ));
1845
- return;
1846
- }
1847
- const request = appContext.shared.buildLineEditRequest(appContext.shared.lineEditPayload);
1848
- if (!request.ok) {
1849
- if (request.message) appContext.notify(request.message);
1850
- return;
1851
- }
1852
- const { body, lineEdit, seed } = request;
1853
- // The take being edited, not the draft that may be on screen: a preview
1854
- // is a picture and must never become anyone's lineage.
1855
- const source = appContext.shared.linePreviewSource;
1856
- const queued = enqueueMotionJob({
1857
- request: generationRequest,
1858
- charId: source?.charId ?? appContext.shared.activeChar.id,
1859
- charIndex: appContext.shared.activeCharIndex,
1860
- prompt: body.prompt,
1861
- body,
1862
- hasBlockEdits: false,
1863
- committedEditKeys: [],
1864
- rootRotationDeg: source?.rotationDeg ?? motion.rotationDeg ?? appContext.shared.activeChar.rot,
1865
- anchor: { x: motion.anchorX ?? appContext.shared.activeChar.x, z: motion.anchorZ ?? appContext.shared.activeChar.z },
1866
- ikState: null,
1867
- recipeIntent: "lineEdit",
1868
- recipeSeed: seed,
1869
- // sourceMotion is dropped on the way into the recipe: replay rebinds
1870
- // it to whatever take it is re-applied to.
1871
- // (stripSourceMotion keeps only C10's replay keys, so `preview` — when
1872
- // the object came back from a draft build — cannot leak into a recipe.)
1873
- recipeLineEdit: stripSourceMotion({ ...lineEdit, sourceMotion: undefined, seed }),
1874
- recipeLabel: isKo ? `다듬기 · ${lineTrackLabel(appContext.shared.lineTrack)}` : `Refine · ${lineTrackLabel(appContext.shared.lineTrack)}`,
1875
- });
1876
- if (!queued) return;
1877
- appContext.shared.generationPendingRef.current = true;
1878
- // The pull has left the building. The curve stays (it is the reference
1879
- // the next edit starts from) but its deformation is released, so the
1880
- // Generate button goes back to needing a fresh pull instead of inviting
1881
- // a second identical run while the first is still queued. The undo
1882
- // stack goes with it: restoring a pull that is already generating would
1883
- // invite the identical run this reset exists to prevent.
1884
- //
1885
- // This also ENDS THE PREVIEW SESSION: the draft comes off the viewport
1886
- // (the real result will land on it in a couple of seconds, and until it
1887
- // does the take on screen should be the take that exists) and the seed
1888
- // is released, so the next pull is a new piece of work with a new roll.
1889
- appContext.shared.clearLineEdit();
1890
- }
1891
-
1892
- function runAllPromptBlocks(commandContext = null) {
1893
- const characterId = appContext.storeDomain('cast').activeId;
1894
- return commandContext ? generateMotion({ characterId }, commandContext)
1895
- : appContext.bus.run('motion.generate', { characterId });
1896
- }
1897
-
1898
- // The UI's uncommitted prompt/fresh choice is read synchronously at admission,
1899
- // just like its seed and pose controls. It never survives into another run.
1900
- let uiGenerationIntent = null;
1901
- function runArdy({ promptOverride = ardyPrompt, durationOverride = ardyDuration, promptClipsOverride = [], fresh = false } = {}) {
1902
- uiGenerationIntent = { prompt: promptOverride, blocks: promptClipsOverride, fresh };
1903
- try { return appContext.bus.run('motion.generate', { characterId: appContext.storeDomain('cast').activeId, durationSeconds: Math.round(Number(durationOverride)) || ARDY_DURATION_MIN }); }
1904
- finally { uiGenerationIntent = null; }
1905
- }
1906
-
1907
- async function generateMotion(args, commandContext) {
1908
- if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) throw generationRefusal('TARGET_BUSY', 'A motion generation is already running.');
1909
- if (appContext.shared.linePreviewUrl) throw generationRefusal('TARGET_NOT_READY', previewBlockingReason(en => en), previewBlockingReason(ko));
1910
- const character = appContext.storeDomain('cast').read().find(row => row.id === args.characterId);
1911
- refuseRigOnly(character, 'motion');
1912
- const active = character.id === appContext.shared.loadedLayerCharRef.current;
1913
- const posing = active && appContext.shared.posing;
1914
- const rig = posing ? appContext.shared.posedRig() : appContext.shared.rigs[character.id];
1915
- if (!rig) throw generationRefusal('TARGET_NOT_READY', 'Character not loaded yet', ko('Character not loaded yet', '캐릭터가 아직 로드되지 않았어요'));
1916
- const take = domain.motionFor(character.id), state = ikStateFor(character.id);
1917
- const intent = commandContext.origin === 'ui' ? uiGenerationIntent : null;
1918
- const blocks = (intent?.blocks ?? character.layer.promptClips).filter(clip => clip.text.trim()).sort((a, b) => a.startFrame - b.startFrame);
1919
- const prompt = intent?.prompt ?? blocks[0]?.text ?? (active ? ardyPrompt : '');
1920
- let seed;
1921
- try { seed = resolveSeed(args.seed ?? (active ? ardySeed : ''), ARDY_SEED_MAX); }
1922
- catch (error) { throw generationRefusal('INVALID_ARGUMENT', error.message); }
1923
- const input = { character, prompt, blocks, seed, durationSeconds: args.durationSeconds ?? (blocks.length ? Math.max(ARDY_DURATION_MIN, Math.ceil(Math.max(...blocks.map(clip => clip.endFrame)) / TIMELINE_FPS)) : ardyDuration),
1924
- waypoints: character.layer.waypoints, motion: take, ikKeys: state.keys,
1925
- startFromPose: ardyStartFromPose, posePlacement: ardyPosePlacement, frame: appContext.shared.tlFrame,
1926
- preserveStrength, recipe: domain.layer(character.id).takeRecipe, fresh: intent?.fresh ?? false };
1927
- const plan = buildGenerationRequest(input), resolved = resolveIkRig(rig);
1928
- const image = appContext.shared.snapshotExportRig(rig);
1929
- let poses;
1930
- try {
1931
- poses = plan.constraintFrames.map(frame => {
1932
- if (take) applyMotionFrame(rig, take, frame);
1933
- if (resolved && state.keys.size) ikEvaluate(resolved.chains, state, frame, resolved.fkJoints, take ? IK_CORRECTION_BLEND_FRAMES : 0);
1934
- return { frame, pose: buildArdyPose({ rig, camRef: appContext.shared.shotCamRef, look: appContext.shared.look,
1935
- fovDeg: appContext.shared.fovDeg, slate: slateLine(appContext.shared.shot), rigName: posing ? appContext.shared.posingChar?.model ?? character.model : character.model, root: captureArdyRoot(rig) }) };
1936
- });
1937
- } finally { appContext.shared.restoreExportRig(image); }
1938
- const built = buildGenerationRequest({ ...input, poses });
1939
- for (const warning of built.warnings) appContext.notify(warning);
1940
- const request = requestMotionGeneration('timeline', take?.url && state.keys.size ? 'edit' : ardyStartFromPose ? 'pose' : 'prompt');
1941
- commandContext.check();
1942
- return new Promise((resolve, reject) => {
1943
- const job = { request, commandContext, commandCompletion: { resolve, reject }, charId: character.id,
1944
- charIndex: appContext.live.characters.findIndex(row => row.id === character.id), prompt: built.body.prompt,
1945
- body: built.body, hasBlockEdits: built.hasBlockEdits, committedEditKeys: built.committedEditKeys,
1946
- rootRotationDeg: built.rootRotationDeg, anchor: { x: character.x, z: character.z }, ikState: built.hasBlockEdits ? state : null,
1947
- recipeIntent: built.hasBlockEdits ? 'carry' : 'fresh', recipeSeed: seed,
1948
- recipeLabel: built.hasBlockEdits ? ko('Block fix', '블록 수정') : built.hasPromptSchedule ? ko('Blocks', '블록 생성')
1949
- : input.fresh ? ko('New', '새로 만들기') : take?.url ? ko('Again', '다시 뽑기') : ko('Generate', '생성') };
1950
- appContext.shared.generationPendingRef.current = enqueueMotionJob(job) === true;
1951
- if (!appContext.shared.generationPendingRef.current) reject(generationRefusal('TARGET_NOT_READY', 'The motion backend cannot run this request.'));
1952
- });
1953
- }
1954
-
1955
- function onTrailDragStart({ track, grabFrame } = {}) {
1956
- if (!motion) return;
1957
- if (track) {
1958
- setTrailTrackFocus(track);
1959
- focusIkHandle(track);
1960
- }
1961
- const frame = Math.round(grabFrame ?? appContext.shared.tlFrame);
1962
- appContext.shared.setTlFrame(frame);
1963
- // The pre-drag take is both the deformation base (repeated moves re-derive
1964
- // from it, so deltas never accumulate) and the undo snapshot.
1965
- appContext.shared.trailBaseMotionRef.current = motion;
1966
- appContext.storeDomain('motion').beginGesture();
1967
- }
1968
-
1969
- /** World drag delta -> clip delta, shedding the character's stature scale
1970
- * (the trail is drawn scaled by it). */
1971
- function trailClipDelta(base, delta, track = "hips", grabFrame = appContext.shared.tlFrame) {
1972
- const statureScale = appContext.shared.activeChar.scale ?? 1;
1973
- return worldDeltaToTrailClip(base, delta, { track, grabFrame, radiusFrames: trailFalloffFrames, rig: appContext.shared.activeRig, scale: statureScale });
1974
- }
1975
-
1976
- /** Per-rAF drag preview. Deliberately React-free: the deformed take lands in
1977
- * a ref and on the rig directly, so a drag never re-renders the app. The
1978
- * trail/highlight lines are rewritten in place by MotionTrails itself. */
1979
- function onTrailDragPreview({ track, grabFrame, delta }) {
1980
- const base = appContext.shared.trailBaseMotionRef.current;
1981
- if (!base) return;
1982
- const deformed = applyTrailFalloffDelta(base, {
1983
- grabFrame,
1984
- radiusFrames: trailFalloffFrames,
1985
- track, clipDelta: trailClipDelta(base, delta, track, grabFrame),
1986
- });
1987
- appContext.shared.trailPreviewMotionRef.current = deformed;
1988
- const rig = appContext.shared.activeRig;
1989
- if (!rig) return;
1990
- applyMotionFrame(rig, deformed, Math.round(grabFrame));
1991
- if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
1992
- ikEvaluate(ikChains, appContext.shared.ikStateRef.current, Math.round(grabFrame), ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
1993
- }
1994
- }
1995
-
1996
- function onTrailDragEnd({ track, grabFrame, delta }) {
1997
- const base = appContext.shared.trailBaseMotionRef.current;
1998
- const deformed = appContext.shared.trailPreviewMotionRef.current;
1999
- appContext.shared.trailBaseMotionRef.current = null;
2000
- appContext.shared.trailPreviewMotionRef.current = null;
2001
- const size = delta ? Math.hypot(delta.x, delta.y, delta.z) : 0;
2002
- if (!base || !deformed || size < 0.01) {
2003
- // A sub-centimetre nudge is a mis-grab, not an authored edit: the
2004
- // motion state never changed, so only the rig pose needs restoring.
2005
- if (base && appContext.shared.activeRig) {
2006
- applyMotionFrame(appContext.shared.activeRig, base, appContext.shared.tlFrame);
2007
- if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
2008
- ikEvaluate(ikChains, appContext.shared.ikStateRef.current, appContext.shared.tlFrame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
2009
- }
2010
- }
2011
- setTrailEdit(null);
2012
- return;
2013
- }
2014
- const owned = appContext.storeDomain('motion');
2015
- if (base !== owned.motionFor(appContext.shared.activeChar.id)) { owned.project(); throw new StudioProtocolError('STALE_TARGET', 'The take changed during the trail drag.'); }
2016
- owned.run('motion.editTrail', { characterId: appContext.shared.activeChar.id, track, grabFrame, radiusFrames: trailFalloffFrames, delta });
2017
- owned.finishGesture();
2018
- setTrailEdit({ track, grabFrame, radiusFrames: trailFalloffFrames, clipDelta: trailClipDelta(base, delta, track, grabFrame) });
2019
- const { startFrame, endFrame } = trailEditRange(base.frames, grabFrame, trailFalloffFrames);
2020
- const conflictFrames = findAbsoluteIkKeyConflicts({ keys: appContext.shared.ikStateRef.current.keys, track, startFrame, endFrame });
2021
- if (conflictFrames.length) appContext.notify(`${ko("Absolute IK keys in this range override the path edit — remove or re-key them", "이 구간의 절대 IK 키가 궤적 수정을 덮어써요 — 해당 키를 지우거나 다시 찍어주세요")} ${isKo ? `프레임 ${conflictFrames.join(", ")}` : `frames ${conflictFrames.join(", ")}`}`);
2022
- }
2023
-
2024
- /** Send the pending trail edit through the existing motionEdit pipeline:
2025
- * the regen window is auto-derived from the grab + falloff, explicit IK
2026
- * keys inside the window ride as hard constraints (their tracks), and the
2027
- * deformed line contributes the grab-frame pose as a root guide. */
2028
- function runTrailRegeneration() {
2029
- if (appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning) return;
2030
- if (isProxyFigure(appContext.shared.activeChar)) {
2031
- appContext.notify(kindRefusal("trails", isKo));
2032
- return;
2033
- }
2034
- const request = requestMotionGeneration("trail", "edit", { motionEdit: true });
2035
- if (!trailEdit) return;
2036
- // Same rule as runArdy: motionEdit rewrites a span of THE take, and a
2037
- // draft on the viewport is not it.
2038
- if (appContext.shared.linePreviewUrl) {
2039
- appContext.notify(previewBlockingReason());
2040
- return;
2041
- }
2042
- if (!motion?.url) {
2043
- appContext.notify(ko("The current motion has no bridge source; generate the prompt blocks once before regenerating a trail edit", "현재 모션에 브리지 원본이 없어요. 궤적 수정을 재생성하려면 프롬프트 블록을 먼저 한 번 생성하세요"));
2044
- return;
2045
- }
2046
- const rig = appContext.shared.activeRig;
2047
- if (!rig) {
2048
- appContext.notify(ko("Character not loaded yet", "캐릭터가 아직 로드되지 않았어요"));
2049
- return;
2050
- }
2051
- const { startFrame, endFrame } = trailEditRange(motion.frames, trailEdit.grabFrame, trailEdit.radiusFrames);
2052
- const frames = [...new Set([
2053
- trailEdit.grabFrame,
2054
- ...appContext.shared.ikFrames.filter((frame) => frame >= startFrame && frame < endFrame),
2055
- ])].sort((a, b) => a - b);
2056
- const currentFrame = appContext.shared.tlFrame;
2057
- const entries = [];
2058
- for (const frame of frames) {
2059
- applyMotionFrame(rig, motion, frame);
2060
- if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
2061
- ikEvaluate(ikChains, appContext.shared.ikStateRef.current, frame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
2062
- }
2063
- const pose = buildArdyPose({
2064
- rig,
2065
- camRef: appContext.shared.shotCamRef,
2066
- look: appContext.shared.look,
2067
- fovDeg: appContext.shared.fovDeg,
2068
- slate: slateLine(appContext.shared.shot),
2069
- rigName: appContext.shared.activeChar.model,
2070
- root: captureArdyRoot(rig),
2071
- });
2072
- const wireFrame = toArdyFrame(frame);
2073
- if (entries.length && wireFrame <= entries[entries.length - 1].frame) continue;
2074
- const ikTracks = [...(appContext.shared.ikStateRef.current.keys.get(frame)?.keys() || [])];
2075
- if (frame === trailEdit.grabFrame && trailEdit.track && !ikTracks.includes(trailEdit.track)) ikTracks.push(trailEdit.track);
2076
- entries.push({ frame: wireFrame, timelineFrame: frame, tracks: ikTracks.length ? ikTracks : ["hips"], pose });
2077
- }
2078
- applyMotionFrame(rig, motion, currentFrame);
2079
- if (ikChains && appContext.shared.ikStateRef.current.keys.size > 0) {
2080
- ikEvaluate(ikChains, appContext.shared.ikStateRef.current, currentFrame, ikFkJoints, IK_CORRECTION_BLEND_FRAMES);
2081
- }
2082
- const prompt = (motion.prompt || "").trim() || "A person continues the motion naturally.";
2083
- const body = {
2084
- prompt,
2085
- duration: motion.frames / TIMELINE_FPS,
2086
- posePin: true,
2087
- motionEdit: {
2088
- sourceMotion: motion.url,
2089
- startFrame: toArdyFrame(startFrame),
2090
- endFrame: toArdyFrame(endFrame),
2091
- contextBefore: 40,
2092
- contextAfter: 20,
2093
- edits: entries.map(({ frame, tracks, pose }) => ({ frame, tracks, pose })),
2094
- },
2095
- };
2096
- // THE SEED RULE (C9) — a trail regeneration writes a new take too, so its
2097
- // seed is rolled, sent and recorded like every other take-creating run.
2098
- const seed = takeSeed();
2099
- if (seed === null) return;
2100
- body.seed = seed;
2101
- const queued = enqueueMotionJob({
2102
- request,
2103
- charId: appContext.shared.activeChar.id,
2104
- charIndex: appContext.shared.activeCharIndex,
2105
- prompt,
2106
- body,
2107
- hasBlockEdits: true,
2108
- committedEditKeys: entries.map(({ timelineFrame, tracks }) => ({ frame: timelineFrame, tracks })),
2109
- rootRotationDeg: motion.rotationDeg ?? appContext.shared.activeChar.rot,
2110
- anchor: { x: motion.anchorX ?? appContext.shared.activeChar.x, z: motion.anchorZ ?? appContext.shared.activeChar.z },
2111
- ikState: appContext.shared.ikStateRef.current,
2112
- // motionEdit rewrites a span of the loaded take; the recipe travels
2113
- // forward unchanged because there is no recipe field that could
2114
- // describe the splice (C10 excludes motionEdit from replay outright).
2115
- recipeIntent: "carry",
2116
- recipeSeed: seed,
2117
- recipeLabel: ko("Trail fix", "궤적 수정"),
2118
- });
2119
- if (!queued) return;
2120
- appContext.shared.generationPendingRef.current = true;
2121
- setTrailEdit(null);
2122
- }
2123
-
2124
- /* One box, one job at a time: explicit entry points suppress duplicate
2125
- * requests through queueing and execution. The
2126
- * payload is frozen at enqueue time; completion delivers the clip to the
2127
- * REQUESTING character's layer, not whoever happens to be selected then. */
2128
- const [genQueue, setGenQueue] = useState([]);
2129
-
2130
- function enqueueMotionJob(spec) {
2131
- const options = { body: spec.body, lineEditSupported: lineEditBackend };
2132
- spec.request.preflight(bridge, options);
2133
- if (motionPreflightReason(bridge, options)) return;
2134
- const id = `gen-${++appContext.shared.genJobSeq.current}`;
2135
- setGenQueue((queue) => [...queue, { id, status: "queued", ...spec }]);
2136
- appContext.notify((isKo, ko) => isKo ? `인물 ${spec.charIndex + 1} 모션 생성을 대기열에 넣었어요` : `Queued motion generation for Subject ${spec.charIndex + 1}`);
2137
- return true;
2138
- }
2139
-
2140
- async function executeMotionJob(job) {
2141
- job.commandContext?.check();
2142
- const controller = new AbortController();
2143
- const abort = () => controller.abort(job.commandContext.signal.reason);
2144
- job.commandContext?.signal.addEventListener("abort", abort, { once: true });
2145
- appContext.shared.ardyAbortRef.current = controller;
2146
- setArdyRunning(true);
2147
- reportArdyStatus(ko("connecting…", "연결 중…"));
2148
- setArdyReport(null);
2149
- setArdyOutcome(null);
2150
- // Replay notices belong to ONE run; the next run re-earns them.
2151
- setReplayNotices([]);
2152
- const request = job.request;
2153
- request.start();
2154
- controller.signal.addEventListener("abort", () => request.fail(new DOMException("", "AbortError")), { once: true });
2155
- let editCommitReport = null;
2156
- try {
2157
- const done = await ardyGenerate(
2158
- job.body,
2159
- (event) => {
2160
- if (event.event === "status") reportArdyStatus(event.message);
2161
- else if (event.event === "report") {
2162
- setArdyReport(event.report);
2163
- if (job.hasBlockEdits) editCommitReport = event.report;
2164
- // C10's per-entry replay report. Only the entries worth
2165
- // acting on are kept: a refinement that failed outright,
2166
- // or one whose range straddles an internal block
2167
- // boundary (where block N+1 was conditioned on N's
2168
- // PRE-edit tail, so the replay is approximate rather
2169
- // than exact). Both are non-blocking — the take exists.
2170
- if (Array.isArray(event.report.replay)) {
2171
- setReplayNotices(event.report.replay.filter((entry) => entry?.ok === false || entry?.boundaryWarning === true));
2172
- }
2173
- }
2174
- },
2175
- { signal: controller.signal },
2176
- );
2177
- if (
2178
- job.hasBlockEdits &&
2179
- (
2180
- editCommitReport?.commit_verified !== true ||
2181
- !job.body.motionEdit.edits.every((entry) =>
2182
- editCommitReport.committed_keys?.includes(entry.frame)
2183
- )
2184
- )
2185
- ) {
2186
- throw new Error(ko("The generated motion came back without verified authored pose keys", "생성된 모션에 검증된 수동 포즈 키가 없어요"));
2187
- }
2188
- job.commandContext?.check();
2189
- if (!done.motionUrl) throw generationRefusal('TARGET_NOT_READY', 'The generator finished without a motion artifact.');
2190
- setArdyOutcome({ ok: true, output: done.output, bytes: done.bytes, motionUrl: done.motionUrl, rotationDeg: job.rootRotationDeg });
2191
- request.succeed();
2192
- trackActivation("motion");
2193
- // Fetch and decode the real npz right away; decode errors are shown
2194
- // in the card, playback is never faked. The clip lands on the
2195
- // REQUESTING character, not whoever is selected now.
2196
- if (done.motionUrl) {
2197
- await deliverMotion(job, done.motionUrl);
2198
- if (!controller.signal.aborted && appContext.live.characters.some((entry) => entry.id === job.charId)) request.apply();
2199
- if (!appContext.storeDomain('motion')) commitTakeRecipe(job, done.motionUrl);
2200
- }
2201
- if (!appContext.storeDomain('motion') && job.hasBlockEdits && job.ikState) {
2202
- // Timeline frames, not the wire frames in body.motionEdit.edits:
2203
- // these light up the IK markers on the production clock.
2204
- setCommittedIkEdits((current) => [...current, ...job.committedEditKeys]);
2205
- job.ikState.keys.clear();
2206
- job.ikState.tracked.clear();
2207
- job.ikState.plants.clear();
2208
- setIkTick((value) => value + 1);
2209
- }
2210
- appContext.notify((isKo, ko) => isKo ? `인물 ${job.charIndex + 1} 모션 생성됨` : `Motion generated for Subject ${job.charIndex + 1}`);
2211
- } catch (err) {
2212
- // Wave-2 gate, second line of defence. The capability preflight
2213
- // normally stops a line edit before it is sent, but a bridge that
2214
- // advertises the route and then 400s on the field (a half-landed
2215
- // wave 2, an older sidecar behind the proxy) must read as "not
2216
- // connected yet", not as a red generation failure the user could
2217
- // act on.
2218
- if (job.body.lineEdit && isLineEditUnsupported(err?.message)) {
2219
- appContext.notify(ko(
2220
- "The line-editing backend is not connected yet",
2221
- "라인 편집 백엔드가 아직 연결 전이에요",
2222
- ));
2223
- }
2224
- setArdyOutcome({
2225
- ok: false,
2226
- message: err?.name === "AbortError" ? ko("Cancelled", "취소됨") : err?.message || String(err),
2227
- });
2228
- request.fail(err, job.body.lineEdit && isLineEditUnsupported(err?.message) ? "unsupported_route" : undefined);
2229
- throw err;
2230
- } finally {
2231
- setArdyRunning(false);
2232
- appContext.shared.ardyAbortRef.current = null;
2233
- job.commandContext?.signal.removeEventListener("abort", abort);
2234
- }
2235
- }
2236
-
2237
- /* ONE writer for both. Every take that reaches the app came out of a job,
2238
- * so a job's completion is the only place where "what is this take made of"
2239
- * can be answered honestly, and the answer is checkpointed next to the
2240
- * motionUrl in the same breath. Nothing else may write takeRecipeRef except
2241
- * loadTakeVersion, which restores a checkpoint rather than authoring one. */
2242
- function commitTakeRecipe(job, motionUrl) {
2243
- const owned = appContext.storeDomain('motion');
2244
- const base = owned ? owned.layer(job.charId).takeRecipe : appContext.shared.takeRecipeRef.current;
2245
- let next = base;
2246
- if (job.recipeIntent === "fresh") {
2247
- // A regeneration that carried `replay` produced a take that ALREADY
2248
- // contains those edits, so they stay on the recipe. Resetting
2249
- // lineEdits to [] here would make the second regeneration lose what
2250
- // the first one preserved — the exact failure replay exists to fix.
2251
- next = freshRecipe({
2252
- seed: job.recipeSeed,
2253
- blocks: blocksFromRequest(job.body, ARDY_FPS),
2254
- lineEdits: job.body.replay ?? [],
2255
- });
2256
- } else if (job.recipeIntent === "lineEdit") {
2257
- // A take imported by url (?motion=, a reload) has no recipe of its own.
2258
- // The edit's body carries the take's prompt and length, so a
2259
- // best-effort single block is recorded rather than dropping the
2260
- // refinement on the floor; only the SEED is a guess, and it is the
2261
- // one this edit actually ran with. The seedless placeholder recipe an
2262
- // imported take is given (seedLoadedTake) counts as "no recipe" for
2263
- // the seed specifically: adopting this edit's seed is what turns it
2264
- // into something that can replay at all.
2265
- const seeded = Number.isInteger(base?.seed)
2266
- ? base
2267
- : freshRecipe({
2268
- seed: job.recipeSeed,
2269
- blocks: base?.blocks?.length ? base.blocks : blocksFromRequest(job.body, ARDY_FPS),
2270
- lineEdits: base?.lineEdits ?? [],
2271
- });
2272
- next = withLineEdit(seeded, job.recipeLineEdit);
2273
- }
2274
- if (owned) {
2275
- owned.writeLayer(job.charId, { takeRecipe: next, takeVersions: pushTakeVersion(owned.layer(job.charId).takeVersions, {
2276
- motionUrl, recipe: next, savedAt: Date.now(), label: job.recipeLabel ?? '',
2277
- }, TAKE_VERSIONS_MAX) });
2278
- return;
2279
- }
2280
- appContext.shared.takeRecipeRef.current = next;
2281
- setTakeRecipe(next);
2282
- setTakeVersions((list) => pushTakeVersion(list, {
2283
- motionUrl,
2284
- recipe: next,
2285
- savedAt: Date.now(),
2286
- label: job.recipeLabel ?? "",
2287
- }, TAKE_VERSIONS_MAX));
2288
- }
2289
-
2290
- /* A take can also arrive WITHOUT a job behind it — ?motion=<url>, the shipped
2291
- * demo clip, a scene reload that re-fetches a stored motionRef. Those takes
2292
- * used to leave the version strip empty, so the first refinement had nothing
2293
- * to walk back to and the artist's only checkpoint was the thing they had
2294
- * just overwritten. They get a v1 like everything else.
2295
- *
2296
- * WHAT IS HONESTLY KNOWN is the take's url, its prompt and its length —
2297
- * NOT its seed, which was rolled on the box in a session nobody here
2298
- * witnessed. So the placeholder recipe carries `seed: null` and every reader
2299
- * treats that as "this take cannot be rebuilt": no request may attach it as
2300
- * C10 `replay` (a replay whose base is a different random take re-applies
2301
- * refinements to a stranger), and the first real edit adopts its own seed in
2302
- * commitTakeRecipe. A checkpoint you can return to beats a recipe you can
2303
- * replay, and this gets the first without pretending to the second. */
2304
- function seedLoadedTake(url, prompt, frames) {
2305
- if (appContext.storeDomain('motion')) return;
2306
- if (!url || appContext.shared.takeRecipeRef.current) return;
2307
- const recipe = Object.freeze({
2308
- seed: null,
2309
- blocks: Object.freeze([Object.freeze({
2310
- prompt: typeof prompt === "string" ? prompt : "",
2311
- duration: frames > 0 ? frames / TIMELINE_FPS : 0,
2312
- })]),
2313
- lineEdits: Object.freeze([]),
2314
- });
2315
- appContext.shared.takeRecipeRef.current = recipe;
2316
- setTakeRecipe(recipe);
2317
- setTakeVersions((list) => pushTakeVersion(list, {
2318
- motionUrl: url,
2319
- recipe,
2320
- savedAt: Date.now(),
2321
- label: ko("Loaded", "불러옴"),
2322
- }, TAKE_VERSIONS_MAX));
2323
- }
2324
-
2325
- /** Click a chip: that motionUrl becomes the active take through the SAME
2326
- * delivery path a fresh generation result travels, and the recipe saved
2327
- * beside it becomes the current one. Nothing is truncated — editing from an
2328
- * old version pushes a NEW version on top, so no click can destroy work. */
2329
- async function loadTakeVersion(entry) {
2330
- if (!entry?.motionUrl || motionBusy) return;
2331
- if (appContext.storeDomain('motion')) return appContext.storeDomain('motion').run('motion.loadVersion', { characterId: appContext.shared.activeChar.id, motionUrl: entry.motionUrl });
2332
- if (entry.motionUrl === appContext.shared.takeSourceUrl) return;
2333
- // A pull in hand was authored against the take that is leaving. The
2334
- // preview is dropped WITHOUT reverting: this call is already loading a
2335
- // different take, and a revert would race it with a reload of the one
2336
- // being left behind.
2337
- appContext.shared.cancelLinePreview({ revert: false });
2338
- if (appContext.shared.lineEditMode) appContext.shared.clearLineEdit();
2339
- setReplayNotices([]);
2340
- appContext.shared.takeRecipeRef.current = entry.recipe ?? null;
2341
- setTakeRecipe(entry.recipe ?? null);
2342
- try {
2343
- await deliverMotion({
2344
- charId: appContext.shared.activeChar.id,
2345
- prompt: entry.recipe?.blocks?.[0]?.prompt ?? motion?.prompt ?? "",
2346
- rootRotationDeg: motion?.rotationDeg ?? appContext.shared.activeChar.rot,
2347
- anchor: { x: motion?.anchorX ?? appContext.shared.activeChar.x, z: motion?.anchorZ ?? appContext.shared.activeChar.z },
2348
- calibration: motion?.sceneCalibration ?? null,
2349
- }, entry.motionUrl);
2350
- } catch {
2351
- /* loadMotion already surfaced the decode failure in the panel */
2352
- }
2353
- }
2354
-
2355
- /* Scene blocks the shot with Kimodo; Refine pulls one joint's path with
2356
- * ProjFlow. Everything else the pipeline can do is one of those two said
2357
- * more precisely, and both are reachable from the take itself instead of
2358
- * from a foldout the artist has to remember to open.
2359
- *
2360
- * WHY THE REASONS ARE FUNCTIONS, not toasts. An action the artist cannot
2361
- * take must say so BEFORE the click, in place, next to the button. A toast
2362
- * fired after the click teaches nothing: it arrives once, scrolls away, and
2363
- * leaves the button looking identical to the ones that work. Each reason
2364
- * below is rendered as a line under its entry AND as data-disabled-reason,
2365
- * which is also what the CDP surface gate reads. */
2366
- function selectedMotionReadiness({ fresh = false, clips = appContext.shared.promptClips } = {}) {
2367
- const authored = clips.filter((clip) => clip.text.trim()).sort((a, b) => a.startFrame - b.startFrame);
2368
- const prompt = authored[0]?.text.trim() || ardyPrompt.trim();
2369
- const duration = motion && appContext.shared.ikFrames.length > 0 ? motion.frames / motion.fps
2370
- : authored.length ? Math.max(ARDY_DURATION_MIN, Math.ceil(Math.max(...authored.map((clip) => clip.endFrame)) / TIMELINE_FPS))
2371
- : Math.round(Number(ardyDuration)) || ARDY_DURATION_MIN;
2372
- const clipFrames = duration * TIMELINE_FPS;
2373
- const segments = buildPromptSchedule(authored, clipFrames, prompt);
2374
- const hasPromptSchedule = segments.length > 1;
2375
- const editedSegments = motion?.url && hasPromptSchedule
2376
- ? segments.filter((segment) => appContext.shared.ikFrames.some((frame) => frame >= segment.startFrame && frame < segment.endFrame))
2377
- : [];
2378
- const hasBlockEdits = editedSegments.length > 0;
2379
- const pinPlan = planPosePin({
2380
- startFromPose: ardyStartFromPose,
2381
- poseFrame: posePlacementFrame(ardyPosePlacement, clipFrames, appContext.shared.tlFrame),
2382
- hasPromptSchedule, hasBlockEdits, waypointMode: appContext.shared.waypointMode, ikFrames: appContext.shared.ikFrames, clipFrames, segments, editedSegments,
2383
- });
2384
- // Only the capability-bearing fields are needed here; the queue still
2385
- // preflights the actual frozen request before any generation HTTP call.
2386
- const body = { prompt, duration, posePin: pinPlan.pin };
2387
- if (hasBlockEdits) body.motionEdit = {};
2388
- else if (hasPromptSchedule) body.segments = toArdySegments(segments);
2389
- if (appContext.shared.waypointMode) body.waypoints = [{}];
2390
- const recipe = appContext.shared.takeRecipeRef.current;
2391
- if (!fresh && !hasBlockEdits && Number.isInteger(recipe?.seed)) body.replay = replayPayload(recipe);
2392
- if (!fresh && !hasBlockEdits && !hasPromptSchedule && motion?.url && preserveStrength > 0
2393
- && Math.abs(motion.frames / TIMELINE_FPS - duration) <= 1 / ARDY_FPS + 1e-9) {
2394
- const blocks = blocksFromRequest(body, ARDY_FPS);
2395
- if (recipe?.blocks?.length === blocks.length
2396
- && recipe.blocks.every((block, index) => block.prompt.trim() === blocks[index].prompt.trim())) body.preserve = {};
2397
- }
2398
- return motionReadiness(bridge, { body, lineEditSupported: lineEditBackend });
2399
- }
2400
-
2401
- const generationBusy = ardyRunning || genQueue.some((job) => job.status === "queued" || job.status === "running");
2402
-
2403
- function openMotionSetup(kind = "prompt") {
2404
- setMotionSetupKind(kind);
2405
- setMotionSetupReveal((value) => value + 1);
2406
- }
2407
-
2408
- function refineDisabledReason() {
2409
- if (!characterCapabilities(appContext.shared.activeChar).lineEdit) return kindRefusal("lineEdit", isKo);
2410
- if (!motion) return ko("No take yet — block a scene first", "아직 테이크가 없어요 — 먼저 장면을 만들어 주세요");
2411
- if (!motion.url) return ko("This take has no bridge source — generate it once before refining", "이 테이크에는 브리지 원본이 없어요 — 한 번 생성해야 다듬을 수 있어요");
2412
- return "";
2413
- }
2414
-
2415
- function sceneDisabledReason() {
2416
- // A standing capability, so it outranks the bridge probe: a capsule figure
2417
- // cannot generate whatever the backend says.
2418
- if (!characterCapabilities(appContext.shared.activeChar).motion) return kindRefusal("motion", isKo);
2419
- if (bridge === null || bridgeChecking) return motionReadinessMessage("loading");
2420
- if (generationBusy) return ko("A generation is already running", "이미 생성이 돌고 있어요");
2421
- // NOT a line-edit preview, deliberately. Every other reason here is a
2422
- // standing capability the entry should be greyed for; a draft on the
2423
- // viewport lasts a second and a half, and a reason line appearing and
2424
- // vanishing under the Scene button RESIZES THE TAKE BAR — which shortens
2425
- // the stage, which changes the camera aspect, which the drift watcher
2426
- // reads as "the view moved".
2427
- //
2428
- // THE TEETH ARE OUT OF THAT TRAP: drift no longer discards anything, so a
2429
- // take-bar resize now costs at most a flicker of the ghosted paint and the
2430
- // hint while the aspect settles — it used to kill the pull ~400 ms after
2431
- // every draft landed. The refusal still lives in runArdy /
2432
- // runTrailRegeneration rather than here, because a reason line that
2433
- // appears and vanishes under the pointer is its own small nuisance and
2434
- // nothing is gained by moving it back.
2435
- return "";
2436
- }
2437
-
2438
- /** The one sentence every take-consuming action says while a draft is up. */
2439
- function previewBlockingReason(localize = ko) {
2440
- return localize(
2441
- "A line-edit preview is on the viewport — press Generate to keep it, or undo (Ctrl/Cmd+Z) to drop it",
2442
- "라인 편집 미리보기가 떠 있어요 — 생성으로 확정하거나 Ctrl/Cmd+Z로 되돌린 뒤에 쓰세요",
2443
- );
2444
- }
2445
-
2446
- function sceneGenerateDisabledReason() {
2447
- return sceneDisabledReason()
2448
- || (ardyPrompt.trim() || appContext.shared.promptClips.some((clip) => clip.text.trim())
2449
- ? ""
2450
- : ko("Describe the motion first — press 4 and add a prompt with + on the Prompt lane", "먼저 어떤 동작인지 적어 주세요 — 4를 누르고 프롬프트 레인의 +로 프롬프트를 추가하세요"));
2451
- }
2452
-
2453
- /** Taking it AGAIN needs no fresh wording: the loaded take already knows what
2454
- * it was asked for, so its own prompt is the fallback (the same fallback
2455
- * runLineEdit uses). What it does need is a take to re-take. */
2456
- function sceneAgainPrompt() {
2457
- return ardyPrompt.trim() || (motion?.prompt ?? "").trim();
2458
- }
2459
-
2460
- function sceneAgainDisabledReason() {
2461
- return sceneDisabledReason()
2462
- || (motion?.url ? "" : ko("Nothing to redo yet — make a take first", "다시 뽑을 테이크가 없어요 — 먼저 한 번 만들어 주세요"))
2463
- || (sceneAgainPrompt() || appContext.shared.promptClips.some((clip) => clip.text.trim())
2464
- ? ""
2465
- : ko("This take carries no prompt — add a block and describe it", "이 테이크에는 프롬프트가 없어요 — 블록을 추가하고 동작을 적어 주세요"));
2466
- }
2467
-
2468
- /** ONE CLICK from a loaded take into drag mode. The pull itself is authored
2469
- * on the viewport, but its controls live under the character's Inspector, so
2470
- * selecting that character and revealing the panel happen HERE rather than
2471
- * being three clicks the artist has to find first. */
2472
- function enterRefineMode() {
2473
- const reason = refineDisabledReason();
2474
- if (reason) {
2475
- appContext.notify(reason);
2476
- return;
2477
- }
2478
- setSceneMenuOpen(false);
2479
- appContext.shared.selectActiveCharacterInHierarchy();
2480
- appContext.shared.revealPromptBlocks();
2481
- appContext.shared.toggleLineEditMode();
2482
- }
2483
-
2484
- /** Take it again — same blocks, same lineage, refinements replayed. Authored
2485
- * blocks go through the batch path so the schedule survives; a single-prompt
2486
- * take goes straight through runArdy. */
2487
- function runSceneAgain() {
2488
- if (appContext.shared.promptClips.some((clip) => clip.text.trim())) runAllPromptBlocks();
2489
- else runArdy({ promptOverride: sceneAgainPrompt() });
2490
- }
2491
-
2492
- /** Add a block — the timeline's own add-block gesture, said as a button. */
2493
- function addSceneBlock() {
2494
- appContext.shared.addPromptClip(appContext.shared.tlFrame);
2495
- appContext.shared.selectActiveCharacterInHierarchy();
2496
- appContext.shared.revealPromptBlocks();
2497
- }
2498
-
2499
- /** Hand a finished clip to the layer that asked for it: the buffer when
2500
- * the requester is still active, its stored session motion otherwise. A
2501
- * lightweight motionRef is persisted with the entry either way, so the
2502
- * clip can be re-fetched after a reload. */
2503
- async function deliverMotion(job, motionUrl) {
2504
- const calibration = job.calibration ?? job.sceneCalibration ?? null;
2505
- const normalizedCalibration = normalizeMotionCalibration(calibration);
2506
- const sceneAnchorX = job.anchor.x + normalizedCalibration.offsetX;
2507
- const sceneAnchorZ = job.anchor.z + normalizedCalibration.offsetZ;
2508
- const sceneRotationDeg = job.rootRotationDeg + normalizedCalibration.yawDeg;
2509
- const motionRef = {
2510
- url: motionUrl,
2511
- prompt: job.prompt,
2512
- rotationDeg: sceneRotationDeg,
2513
- anchorX: sceneAnchorX,
2514
- anchorZ: sceneAnchorZ,
2515
- };
2516
- if (calibration && typeof calibration === "object") motionRef.calibration = normalizedCalibration;
2517
- if (!appContext.storeDomain('motion') && !job.commandContext) appContext.shared.publishStudioCharacters((list) => list.map((entry) => entry.id === job.charId ? { ...entry, motionRef } : entry));
2518
- if (job.charId === appContext.shared.loadedLayerCharRef.current) {
2519
- await loadMotion(motionUrl, job.prompt, job.rootRotationDeg, null, job.charId, null, { calibration, commandContext: job.commandContext, job });
2520
- if (!appContext.storeDomain('motion') && job.commandContext) appContext.shared.publishStudioCharacters(appContext.live.characters.map(entry => entry.id === job.charId ? { ...entry, motionRef } : entry));
2521
- return;
2522
- }
2523
- // Inbound boundary for a clip delivered to a non-active layer.
2524
- const retimed = retimeMotion(await loadMotionFromUrl(motionUrl), TIMELINE_FPS);
2525
- if (retimed.sourceBytes) retimed.motionId = await sha256Hex(retimed.sourceBytes);
2526
- const decoded = applyMotionCalibration(retimed, { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 }).motion;
2527
- const clip = {
2528
- ...decoded,
2529
- url: motionUrl,
2530
- prompt: job.prompt,
2531
- anchorX: sceneAnchorX,
2532
- anchorZ: sceneAnchorZ,
2533
- anchorFrame: 0,
2534
- rotationDeg: sceneRotationDeg,
2535
- sceneCalibration: normalizedCalibration,
2536
- editSegments: createMotionEdit(decoded.frames),
2537
- };
2538
- if (calibration && typeof calibration === "object") clip.sceneCalibration = normalizedCalibration;
2539
- // Same stature rule as loadMotion, on the layer that asked for the clip.
2540
- const scale = characterScaleFor(decoded);
2541
- const apply = () => {
2542
- if (appContext.storeDomain('motion')) return commitLoadedTake(job.charId, clip, { job });
2543
- appContext.shared.motionFullRef.current.set(job.charId, clip);
2544
- const next = appContext.live.characters.map(entry => entry.id === job.charId ? { ...entry, scale, sessionMotion: clip, motionRef } : entry);
2545
- appContext.shared.publishStudioCharacters(next);
2546
- };
2547
- if (job.commandContext) job.commandContext.commit(apply);
2548
- else if (appContext.storeDomain('motion')) appContext.storeDomain('motion').runPrepared(job.charId, apply);
2549
- else apply();
2550
- }
2551
-
2552
- /** After a scene (re)load, re-fetch every persisted clip reference and
2553
- * rebuild the session motions. The bridge may be gone — failures just
2554
- * leave the character posed, never an error the user must act on. */
2555
- async function restoreMotionRefs(list) {
2556
- const epoch = ++appContext.shared.restoreEpochRef.current;
2557
- const motions = appContext.shared.projectMotionsRef.current;
2558
- try { const db = await openMotionDb(); const ids = [...new Set(list.map((entry) => entry.motionRef?.motionId?.toLowerCase()).filter(Boolean))]; const cached = await Promise.all(ids.map((id) => getMotion(db, id))); cached.filter(Boolean).forEach((record) => motions.set(record.motionId.toLowerCase(), record)); db.close(); } catch (error) { console.warn("[cozyclay] could not restore motion cache", error); }
2559
- for (const entry of list) {
2560
- const source = resolveMotionSource(entry.motionRef, motions);
2561
- if (!entry.motionRef) continue;
2562
- if (source.kind === "missing") {
2563
- appContext.notify(isKo ? `저장된 모션이 누락되었습니다 (${entry.subject || entry.id})` : `Saved motion is missing for ${entry.subject || entry.id}`);
2564
- continue;
2565
- }
2566
- const load = source.kind === "embedded" ? decodeMotionResource(source.record) : loadMotionFromUrl(source.url);
2567
- load.then((raw) => {
2568
- if (epoch !== appContext.shared.restoreEpochRef.current) return;
2569
- // Inbound boundary: a re-fetched clip is retimed exactly like a
2570
- // freshly generated one, so a reload cannot resurrect 20 fps frames.
2571
- const sourceUrl = source.kind === "url" ? source.url : entry.motionRef?.url;
2572
- const retimed = retimeMotion(raw, TIMELINE_FPS);
2573
- const normalizedCalibration = normalizeMotionCalibration(entry.motionRef.calibration);
2574
- const decoded = applyMotionCalibration(retimed, { ...normalizedCalibration, yawDeg: 0, offsetX: 0, offsetZ: 0 }).motion;
2575
- const clip = {
2576
- ...decoded,
2577
- url: sourceUrl,
2578
- sourceBytes: raw.sourceBytes,
2579
- prompt: entry.motionRef.prompt,
2580
- anchorX: entry.motionRef.anchorX,
2581
- anchorZ: entry.motionRef.anchorZ,
2582
- anchorFrame: 0,
2583
- rotationDeg: entry.motionRef.rotationDeg,
2584
- sceneCalibration: normalizedCalibration,
2585
- editSegments: createMotionEdit(decoded.frames),
2586
- };
2587
- if (entry.motionRef.calibration) clip.sceneCalibration = entry.motionRef.calibration;
2588
- if (entry.motionRef.trailEdits) Object.assign(clip, restoreTrailEdits(clip, entry.motionRef.trailEdits));
2589
- if (entry.motionRef.studioTakeId) clip.studioTakeId = entry.motionRef.studioTakeId;
2590
- appContext.storeDomain('motion').hydrate(entry.id, clip, entry.motionRef);
2591
- }).catch((error) => {
2592
- if (epoch !== appContext.shared.restoreEpochRef.current) return;
2593
- appContext.shared.setProjectManifest((current) => {
2594
- const id = entry.motionRef?.motionId?.toLowerCase();
2595
- if (!id || !current?.items?.some((item) => item.kind === "motion" && item.id === id)) return current;
2596
- const items = current.items.map((item) => item.kind === "motion" && item.id === id
2597
- ? { ...item, status: "missing", url: undefined }
2598
- : item);
2599
- const totals = { embedded: 0, external: 0, missing: 0, bytes: 0 };
2600
- for (const item of items) {
2601
- totals[item.status] += 1;
2602
- if (Number.isFinite(item.bytes)) totals.bytes += item.bytes;
2603
- }
2604
- return { items, totals, missing: items.filter((item) => item.status === "missing") };
2605
- });
2606
- // A saved take that fails to refetch used to vanish silently — the
2607
- // user would find a merely posed character and assume their motion
2608
- // was lost. Name it and offer the reload path.
2609
- const subject = entry.subject || entry.id;
2610
- appContext.notify(isKo
2611
- ? `저장된 모션을 다시 불러오지 못했어요 (${subject}) [${error?.code || "decode"}]`
2612
- : `Saved motion could not be restored for ${subject} [${error?.code || "decode"}]`);
2613
- });
2614
- }
2615
- }
2616
-
2617
- function cancelArdy() {
2618
- appContext.shared.ardyAbortRef.current?.abort();
2619
- }
2620
- const {
2621
- chooseMultiModelFile,
2622
- pasteMultiModelUrl,
2623
- useMultiModelUrl,
2624
- ingestPlatformFootage,
2625
- ingestFootage,
2626
- extractMultiModelMotion,
2627
- extractMultiModelMotionGpu,
2628
- deliverExtraTakes,
2629
- captureI2vStill,
2630
- enterI2vFraming,
2631
- markI2vPose,
2632
- clearI2vPose,
2633
- clearI2vMotion,
2634
- restoreI2vCamera,
2635
- framingDistance,
2636
- showI2vMotionLock,
2637
- generateI2vMotion,
2638
- i2vMotionUnavailable,
2639
- generateI2vMotionFromUi,
2640
- updateI2vMotionQuota,
2641
- } = createMotionVideoCapture({
2642
- appContext,
2643
- bridge,
2644
- i2vMotion,
2645
- setI2vMotion,
2646
- multiModelUrl,
2647
- multiModelSource,
2648
- multiModelFootage,
2649
- multiModelExtract,
2650
- setMultiModelUrl,
2651
- setMultiModelSource,
2652
- setMultiModelStatus,
2653
- setMultiModelStage,
2654
- setMultiModelProgress,
2655
- setMultiModelFootage,
2656
- setMultiModelError,
2657
- setMultiModelTake,
2658
- setMultiModelExtract,
2659
- setMultiModelExtractProgress,
2660
- setMultiModelExtractError,
2661
- loadMotion,
2662
- authoredSupportDescriptors,
2663
- applyAuthoredSupportRise,
2664
- });
2665
- domain.setVideoDraft = patch => setI2vMotion(current => ({ ...current, ...patch }));
2666
- domain.requestLineEdit = runLineEdit;
2667
- domain.requestTrailRegeneration = runTrailRegeneration;
2668
- domain.generate = generateMotion;
2669
- domain.isGenerating = () => appContext.shared.generationPendingRef.current || appContext.shared.genRunningRef.current || ardyRunning;
2670
- domain.onPhysicsRunning = setAutoPhysicsRunning;
2671
- domain.onPhysicsProgress = setPhysicsProgress;
2672
- domain.onPhysicsPreview = result => { setPhysicsPreview(result); setPhysicsShow(true); setIkTick(value => value + 1); };
2673
- domain.platformFitRun = platformFitRun;
2674
- domain.platformFitRemove = platformFitRemove;
2675
- domain.beginPlayback = id => { const rig = appContext.shared.rigs[id]; if (rig) beginPlaybackOn(rig); };
2676
- domain.loadRemote = async (args, context) => {
2677
- const entry = domain.layer(args.characterId).takeVersions.find(version => version.motionUrl === args.motionUrl);
2678
- if (args.motionUrl && !entry) throw new StudioProtocolError('TARGET_NOT_READY', 'This take version is no longer available.');
2679
- if (args.drop != null && !normalizeRootDrop(args.drop)) throw new StudioProtocolError('INVALID_ARGUMENT', 'Invalid drop.');
2680
- const toTimeline = frame => Math.round(frame * TIMELINE_FPS / ARDY_FPS);
2681
- const clips = args.blocks?.length ? args.blocks.map(block => {
2682
- if (block.endFrame <= block.startFrame) throw new StudioProtocolError('INVALID_RANGE', 'Prompt blocks must have a positive range.');
2683
- return { id: crypto.randomUUID(), startFrame: toTimeline(block.startFrame), endFrame: toTimeline(block.endFrame), text: block.prompt };
2684
- }) : null;
2685
- const character = appContext.storeDomain('cast').read().find(row => row.id === args.characterId);
2686
- const commandContext = !clips ? context : { ...context, commit: apply => context.commit(() => {
2687
- const result = apply();
2688
- appContext.recordAction('cast', () => appContext.storeDomain('cast').extendTimeline(Math.max(...clips.map(clip => clip.endFrame))), null, true);
2689
- return result;
2690
- }) };
2691
- await loadMotion(args.url ?? args.motionUrl, args.prompt ?? entry?.recipe?.blocks?.map(block => block.prompt).join(' ') ?? domain.motionFor(args.characterId)?.prompt ?? '', character.rot,
2692
- args.drop ?? null, args.characterId, clips, { commandContext, recipe: entry?.recipe });
2693
- };
934
+
2694
935
  appContext.updateActionPorts({ clearMotionNative: clearMotion, setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys });
2695
936
  return {
2696
937
  ...domain,
2697
- i2vMotion, setI2vMotion, captureI2vStill, enterI2vFraming, markI2vPose, clearI2vPose, clearI2vMotion, restoreI2vCamera, framingDistance, showI2vMotionLock, generateI2vMotion, i2vMotionUnavailable, generateI2vMotionFromUi, updateI2vMotionQuota,
2698
938
  ikMode, ikChains, setIkChains, ikFkJoints, setIkFkJoints, ikFocus, setIkFocus, footSnap, setFootSnap,
2699
- bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES, autoPhysicsRunning, setAutoPhysicsRunning,
2700
- physicsPreview, setPhysicsPreview, physicsShow, physicsProgress, physicsOptions, setPhysicsOptions,
2701
- ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, trailFalloffS, setTrailFalloffS, showTrails,
2702
- setShowTrails, ikEditTool, setIkEditTool, trailEdit, trailTrackFocus, selectTrailTrack, clearTrailTrackFocus, trailFalloffFrames, focusIkHandle, snapshotIkKeys,
939
+ bodyContact, setBodyContact, IK_CORRECTION_BLEND_FRAMES,
940
+ ikTick, setIkTick, committedIkEdits, setCommittedIkEdits, ikEditTool, setIkEditTool, focusIkHandle, snapshotIkKeys,
2703
941
  pendingIkEdit, applyPendingIkEdit, cancelPendingIkEdit,
2704
942
  rangePinApplySpec: (pin) => {
2705
943
  domain.run('motion.rangePin.apply', { characterId: appContext.shared.activeChar.id, pin });
2706
944
  return { residuals: decodeRangePinResiduals(domain.layer(appContext.shared.activeChar.id).ikPinResiduals).get(pin.id) };
2707
945
  },
2708
946
  rangePins, rangePinResiduals, rangePinSelection, setRangePinSelection, rangePinPartPick, setRangePinPartPick, rangePinPreview, previewRangePinDraft, applyRangePinDraft, deleteRangePin,
2709
- setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys, bridge, setBridge, bridgeChecking,
2710
- motionSetupReveal, motionSetupKind, setArdyPrompt, setArdyDuration, ardySeed, preserveStrength,
2711
- setPreserveStrength, takeRecipe, takeVersions, replayNotices, sceneMenuOpen, setSceneMenuOpen,
2712
- ardyRunning, ardyStatus, ardyOutcome, lineEditBackend, setLineEditBackend, motion, motionBusy,
2713
- multiModelUrl, setMultiModelUrl, multiModelSource, setMultiModelSource, multiModelStatus,
2714
- multiModelStage, multiModelProgress, multiModelFootage, multiModelError, multiModelTake,
2715
- multiModelExtract, multiModelExtractProgress, multiModelExtractError, advanceFrame, stepFrame,
2716
- leaveIkMode, beginPlaybackOn, chooseMultiModelFile, pasteMultiModelUrl, useMultiModelUrl, ingestFootage,
2717
- extractMultiModelMotion, deliverExtraTakes, loadMotion, clearMotion: () => appContext.bus.run('motion.clear', { characterId: appContext.shared.activeChar.id }), clearMotionNative: clearMotion, applyMotionTrim, resetMotionTrim, cutMotionAtPlayhead,
2718
- changeMotionSegmentSpeed, removeMotionSegmentById, poseOtherCastMembers, toggleIkMode, ikSolve,
2719
- ikDragEnd, ikAddKeyframe, externalBlockers, runFixCollisions, runFixCollisionsRange,
2720
- changePhysicsOptions, showPhysicsPreview, cancelPhysicsPreview, applyPhysicsPreview, runAutoPhysics,
2721
- platformFitRun, platformFitApply, platformFitRemove, platformFitRunning, platformFitLast, platformFitProgress, platformFitApplied,
2722
- ikDeleteKeyframe, ikApplyPoseAsKey, recheckMotionHealth, changeArdySeed, takeSeed,
2723
- runLineEdit: () => domain.run('motion.commitLineEdit', { characterId: appContext.shared.activeChar.id }),
2724
- runAllPromptBlocks, runArdy, onTrailDragStart, onTrailDragPreview, onTrailDragEnd,
2725
- runTrailRegeneration: () => domain.run('motion.regenerateTrail', { characterId: appContext.shared.activeChar.id }),
2726
- genQueue, setGenQueue, executeMotionJob, seedLoadedTake, loadTakeVersion, selectedMotionReadiness,
2727
- generationBusy, openMotionSetup, refineDisabledReason, sceneDisabledReason, sceneGenerateDisabledReason,
2728
- sceneAgainDisabledReason, enterRefineMode, runSceneAgain, addSceneBlock, restoreMotionRefs, cancelArdy,
947
+ setCharacterIkKey, removeCharacterIkKey, clearCharacterIkKeys, motion,
948
+ advanceFrame, stepFrame,
949
+ leaveIkMode, clearMotion: () => appContext.bus.run('motion.clear', { characterId: appContext.shared.activeChar.id }), clearMotionNative: clearMotion,
950
+ poseOtherCastMembers, toggleIkMode, ikSolve, ikDragEnd, ikAddKeyframe, ikDeleteKeyframe, ikApplyPoseAsKey,
2729
951
  };
2730
952
  }