cozyclay 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (583) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +40 -113
  4. package/bin/agent/agent-runner.mjs +9 -10
  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/studio-motion-reference.mjs +23 -0
  14. package/bin/agent/studio-prompt.mjs +1 -1
  15. package/bin/agent/studio-rebase-text.mjs +32 -0
  16. package/bin/agent/studio-rebase.mjs +26 -39
  17. package/bin/agent/studio-tools.mjs +9 -28
  18. package/bin/agent/tool-arguments.mjs +49 -0
  19. package/bin/agent/turn-frames.mjs +66 -0
  20. package/bin/codex-auth.mjs +9 -2
  21. package/bin/cozyclay.mjs +10 -116
  22. package/bin/live/cli.mjs +22 -6
  23. package/bin/mcp-runtime.mjs +2 -12
  24. package/bin/package-signature.mjs +0 -4
  25. package/dist/app/index.html +7 -6
  26. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  27. package/dist/assets/analytics-C9sUnndd.js +1 -0
  28. package/dist/assets/app-CCh--1Hc.js +5087 -0
  29. package/dist/assets/app-f-fkV5uP.css +1 -0
  30. package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
  31. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  32. package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
  34. package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
  35. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  36. package/dist/assets/workflow-C4yKH1p4.js +179 -0
  37. package/dist/cozyclay-package.json +1 -1
  38. package/dist/index.html +3 -3
  39. package/dist/llms.txt +14 -0
  40. package/dist/sw.js +3 -6
  41. package/dist/workflow/index.html +5 -4
  42. package/mcp/LIVE-PROTOCOL.md +7 -47
  43. package/mcp/README.md +16 -26
  44. package/mcp/live-hub.mjs +6 -10
  45. package/mcp/package.json +1 -3
  46. package/mcp/tool-handlers.mjs +4 -168
  47. package/package.json +3 -22
  48. package/src/App.jsx +174 -2639
  49. package/src/agent-progress.js +79 -0
  50. package/src/agent-turn-mark.js +30 -0
  51. package/src/analytics.js +1 -19
  52. package/src/app-context.js +5 -0
  53. package/src/app-stage.jsx +2 -34
  54. package/src/asset-pane.jsx +6 -5
  55. package/src/camera-block.js +21 -0
  56. package/src/camera-direction.js +120 -0
  57. package/src/character-kind.js +3 -9
  58. package/src/command-bus.js +8 -6
  59. package/src/commands/cast.js +5 -34
  60. package/src/commands/elements/character.js +1 -1
  61. package/src/commands/elements/motion.js +1 -6
  62. package/src/commands/elements/object.js +6 -2
  63. package/src/commands/elements/shot.js +4 -1
  64. package/src/commands/elements.js +38 -14
  65. package/src/commands/index.js +3 -1
  66. package/src/commands/kits.js +37 -0
  67. package/src/commands/motion-timing.js +57 -0
  68. package/src/commands/motion.js +9 -152
  69. package/src/commands/motion3-compile.js +65 -0
  70. package/src/commands/motion3.js +207 -0
  71. package/src/commands/objects.js +6 -4
  72. package/src/commands/shot.js +49 -11
  73. package/src/commands/stage.js +14 -1
  74. package/src/commands/view.js +4 -1
  75. package/src/document-mirrors.js +42 -0
  76. package/src/document-store.js +4 -0
  77. package/src/domains/cast.js +19 -194
  78. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  79. package/src/domains/motion/take-source.js +51 -0
  80. package/src/domains/motion.js +60 -1838
  81. package/src/domains/scenes.js +12 -55
  82. package/src/domains/shots.js +2 -2
  83. package/src/domains/stage.js +4 -2
  84. package/src/draft-ghost.js +46 -0
  85. package/src/draft-ghost.jsx +129 -0
  86. package/src/dualview.jsx +4 -3
  87. package/src/effects/EffectsLayer.jsx +112 -0
  88. package/src/effects/effect-meshes.js +232 -0
  89. package/src/effects/effect-record.js +103 -0
  90. package/src/effects/emitter.js +59 -0
  91. package/src/effects/simulate.js +261 -0
  92. package/src/effects/state.js +59 -0
  93. package/src/execution-telemetry.js +2 -3
  94. package/src/first-success-guide.jsx +5 -5
  95. package/src/ghost-set.js +257 -0
  96. package/src/held-object-edit.jsx +113 -0
  97. package/src/held-object-pose.js +175 -0
  98. package/src/hierarchy-model.js +1 -1
  99. package/src/hierarchy-panel.jsx +1 -1
  100. package/src/imported-mesh.jsx +103 -0
  101. package/src/kits/held.js +84 -0
  102. package/src/kits/index.js +70 -0
  103. package/src/kits/smoking.js +57 -0
  104. package/src/light-blockers.js +93 -0
  105. package/src/motion/beat-board-model.js +84 -0
  106. package/src/motion/beat-board.css +124 -0
  107. package/src/motion/beat-board.jsx +59 -0
  108. package/src/motion/center-of-mass.js +66 -0
  109. package/src/motion/fix-collisions.js +850 -0
  110. package/src/motion/physics-review.js +118 -0
  111. package/src/{ardy → motion}/physics-support.js +1 -76
  112. package/src/motion/rig-snapshot.js +22 -0
  113. package/src/motion/take-segments.jsx +140 -0
  114. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  115. package/src/{ardy → motion}/timeline.css +1 -2
  116. package/src/{ardy → motion}/timeline.jsx +48 -419
  117. package/src/motion/waypoints.js +76 -0
  118. package/src/motion3/beats.js +83 -0
  119. package/src/motion3/body.js +158 -0
  120. package/src/motion3/bones.js +47 -0
  121. package/src/motion3/carried.js +47 -0
  122. package/src/motion3/compile-job.js +75 -0
  123. package/src/motion3/compile-worker.js +10 -0
  124. package/src/motion3/compiler/anchors.js +105 -0
  125. package/src/motion3/compiler/arm-orient.js +128 -0
  126. package/src/motion3/compiler/channels.js +125 -0
  127. package/src/motion3/compiler/compile.js +205 -0
  128. package/src/motion3/compiler/gait-rhythm.js +97 -0
  129. package/src/motion3/compiler/grip.js +171 -0
  130. package/src/motion3/compiler/head-steady.js +48 -0
  131. package/src/motion3/compiler/hold.js +134 -0
  132. package/src/motion3/compiler/idle.js +113 -0
  133. package/src/motion3/compiler/limbs.js +170 -0
  134. package/src/motion3/compiler/math.js +72 -0
  135. package/src/motion3/compiler/pose.js +162 -0
  136. package/src/motion3/compiler/quat.js +49 -0
  137. package/src/motion3/compiler/skeleton.js +74 -0
  138. package/src/motion3/compiler/validate-common.js +32 -0
  139. package/src/motion3/compiler/validate-goals.js +118 -0
  140. package/src/motion3/compiler/validate-objects.js +143 -0
  141. package/src/motion3/compiler/validate-parts.js +116 -0
  142. package/src/motion3/compiler/validate.js +122 -0
  143. package/src/motion3/compiler/vocab.js +73 -0
  144. package/src/motion3/compiler/walk-pelvis.js +275 -0
  145. package/src/motion3/compiler/walk.js +207 -0
  146. package/src/motion3/compiler/wrist-clamp.js +32 -0
  147. package/src/motion3/delta.js +47 -0
  148. package/src/motion3/describe.js +174 -0
  149. package/src/motion3/docs.js +27 -0
  150. package/src/motion3/edit-carry.js +21 -0
  151. package/src/motion3/fix/ease.js +16 -0
  152. package/src/motion3/fix/ground.js +37 -0
  153. package/src/motion3/fix/index.js +52 -0
  154. package/src/motion3/fix/push.js +71 -0
  155. package/src/motion3/fix/reach.js +46 -0
  156. package/src/motion3/fix/retime.js +79 -0
  157. package/src/motion3/fix/self.js +211 -0
  158. package/src/motion3/fix/slide.js +70 -0
  159. package/src/motion3/fix/turn.js +51 -0
  160. package/src/motion3/follow.js +39 -0
  161. package/src/motion3/free-motion.js +173 -0
  162. package/src/motion3/grip-choice.js +130 -0
  163. package/src/motion3/hold-plan.js +157 -0
  164. package/src/motion3/json-patch.js +77 -0
  165. package/src/motion3/key-aim.js +105 -0
  166. package/src/motion3/key-edit.js +82 -0
  167. package/src/motion3/lenient.js +127 -0
  168. package/src/motion3/object-keys.js +236 -0
  169. package/src/motion3/object-pose.js +182 -0
  170. package/src/motion3/object-tracks.js +12 -0
  171. package/src/motion3/pipeline.js +102 -0
  172. package/src/motion3/playback.js +51 -0
  173. package/src/motion3/probes.js +91 -0
  174. package/src/motion3/qa/body-checks.js +34 -0
  175. package/src/motion3/qa/check-names.js +14 -0
  176. package/src/motion3/qa/checks.js +121 -0
  177. package/src/motion3/qa/contact-checks.js +51 -0
  178. package/src/motion3/qa/explain.js +80 -0
  179. package/src/motion3/qa/fmt.js +5 -0
  180. package/src/motion3/qa/foot-checks.js +72 -0
  181. package/src/motion3/qa/geometry.js +77 -0
  182. package/src/motion3/qa/goals.js +191 -0
  183. package/src/motion3/qa/ground.js +60 -0
  184. package/src/motion3/qa/hints-contact.js +179 -0
  185. package/src/motion3/qa/hints-goal.js +160 -0
  186. package/src/motion3/qa/hints-knee.js +24 -0
  187. package/src/motion3/qa/hints-motion.js +209 -0
  188. package/src/motion3/qa/hints-object.js +91 -0
  189. package/src/motion3/qa/index.js +42 -0
  190. package/src/motion3/qa/key-checks.js +82 -0
  191. package/src/motion3/qa/near.js +96 -0
  192. package/src/motion3/qa/object-checks.js +135 -0
  193. package/src/motion3/qa/patch.js +161 -0
  194. package/src/motion3/qa/severity.js +38 -0
  195. package/src/motion3/qa/spec-checks.js +65 -0
  196. package/src/motion3/qa/style.js +218 -0
  197. package/src/motion3/qa/wrist-checks.js +69 -0
  198. package/src/motion3/qa/wrist-obs.js +60 -0
  199. package/src/motion3/refs.js +120 -0
  200. package/src/motion3/repair-tune.js +121 -0
  201. package/src/motion3/repair.js +83 -0
  202. package/src/motion3/report.js +67 -0
  203. package/src/motion3/rigs/index.js +14 -0
  204. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  205. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  207. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  208. package/src/motion3/scene.js +60 -0
  209. package/src/motion3/schema/brief.js +89 -0
  210. package/src/motion3/schema/fields.js +44 -0
  211. package/src/motion3/schema/spec-examples.js +46 -0
  212. package/src/motion3/schema/topics.js +111 -0
  213. package/src/motion3/schema/types.js +147 -0
  214. package/src/motion3/search.js +117 -0
  215. package/src/motion3/second-hand.js +112 -0
  216. package/src/motion3/skills/basic.js +82 -0
  217. package/src/motion3/skills/book.js +76 -0
  218. package/src/motion3/skills/car.js +145 -0
  219. package/src/motion3/skills/frames-auto.js +29 -0
  220. package/src/motion3/skills/index.js +83 -0
  221. package/src/motion3/skills/info.js +65 -0
  222. package/src/motion3/skills/smoke-kit.js +109 -0
  223. package/src/motion3/skills/smoke.js +160 -0
  224. package/src/motion3/spec-store.js +59 -0
  225. package/src/motion3/take-edit.js +155 -0
  226. package/src/motion3/take.js +63 -0
  227. package/src/motion3/timeline.js +127 -0
  228. package/src/motion3/views.js +121 -0
  229. package/src/motion3/walk-plan.js +119 -0
  230. package/src/object-catalog.jsx +7 -1
  231. package/src/object-colors.js +98 -0
  232. package/src/object-gizmo-parts.js +115 -0
  233. package/src/object-gizmo.jsx +37 -122
  234. package/src/object-glow.js +139 -0
  235. package/src/object-hidden.js +33 -0
  236. package/src/object-look.js +196 -0
  237. package/src/object-size.js +10 -0
  238. package/src/object-travel.js +71 -6
  239. package/src/panels/CameraPanel.jsx +7 -7
  240. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  241. package/src/panels/LightPanel.jsx +25 -4
  242. package/src/panels/ObjectLookPanel.jsx +88 -0
  243. package/src/panels/PoseImagePanel.jsx +1 -1
  244. package/src/panels/PosePanel.jsx +2 -17
  245. package/src/panels/RigControlPanel.jsx +5 -53
  246. package/src/panels/SubjectBox.jsx +4 -1
  247. package/src/panels/details.css +14 -0
  248. package/src/posestudio.jsx +4 -27
  249. package/src/project-browser.jsx +3 -4
  250. package/src/project-resources.js +18 -31
  251. package/src/project.js +13 -104
  252. package/src/props.jsx +25 -280
  253. package/src/rail-dot-drag.js +23 -0
  254. package/src/render-passes.js +10 -0
  255. package/src/resource-status.jsx +1 -0
  256. package/src/result-modal.jsx +4 -19
  257. package/src/room.jsx +20 -12
  258. package/src/scene-assets.js +8 -6
  259. package/src/scene-light-place.js +72 -0
  260. package/src/scene-lights.jsx +148 -0
  261. package/src/scene-objects.js +31 -139
  262. package/src/scenes.js +15 -105
  263. package/src/semantic-edit.js +2 -5
  264. package/src/set-pieces.jsx +183 -0
  265. package/src/settings-menu.jsx +3 -11
  266. package/src/shell/BottomDock.jsx +11 -53
  267. package/src/shell/CompositionGuideButton.jsx +20 -0
  268. package/src/shell/DetailsSlot.jsx +16 -200
  269. package/src/shell/GhostsMenuGroup.jsx +74 -0
  270. package/src/shell/LibrarySlot.jsx +1 -3
  271. package/src/shell/MenuBar.jsx +3 -3
  272. package/src/shell/PreferencesDialog.jsx +15 -264
  273. package/src/shell/PreferencesSlot.jsx +2 -8
  274. package/src/shell/ShotPreviewHeader.jsx +17 -0
  275. package/src/shell/TopBar.jsx +5 -83
  276. package/src/shell/ViewportToolbar.jsx +23 -93
  277. package/src/shell/agent-glass.css +19 -6
  278. package/src/shell/details-selection.js +42 -0
  279. package/src/shell/ghosts-menu.css +22 -0
  280. package/src/shell/glass-regions.css +29 -3
  281. package/src/shell/preferences.css +6 -1
  282. package/src/shell/viewport.css +12 -0
  283. package/src/shot-authoring.js +4 -7
  284. package/src/soft-shadows.js +37 -0
  285. package/src/stage-attach-frames.js +12 -12
  286. package/src/stage-canvas-rigs.jsx +3 -3
  287. package/src/stage-capture.jsx +42 -8
  288. package/src/stage-characters.jsx +22 -216
  289. package/src/stage-editor-gizmos.jsx +6 -6
  290. package/src/stage-environment.js +93 -0
  291. package/src/stage-hdr-builtins.js +107 -0
  292. package/src/stage-hdr-image.js +116 -0
  293. package/src/stage-hdr-library.js +70 -0
  294. package/src/stage-hdr-map.js +94 -0
  295. package/src/stage-hdr.js +130 -0
  296. package/src/stage-lighting.js +149 -0
  297. package/src/stage-occlusion.js +210 -0
  298. package/src/stage-sky.jsx +159 -0
  299. package/src/stage-startup-defaults.js +5 -9
  300. package/src/studio-actions.js +12 -14
  301. package/src/studio-agent-commands.js +46 -16
  302. package/src/studio-agent-motion.js +9 -9
  303. package/src/studio-agent-protocol.js +46 -73
  304. package/src/studio-app-binding.js +29 -45
  305. package/src/studio-asset-list.js +17 -0
  306. package/src/studio-contact-sheet.js +37 -5
  307. package/src/studio-elements.js +27 -7
  308. package/src/studio-entity-bounds.js +18 -0
  309. package/src/studio-error-codes.js +8 -0
  310. package/src/studio-export-framing.js +152 -0
  311. package/src/studio-lighting-check.js +103 -0
  312. package/src/studio-motion-visual.js +150 -0
  313. package/src/studio-schema-errors.js +80 -0
  314. package/src/studio-support-gap.js +74 -0
  315. package/src/styles.css +1 -19
  316. package/src/timeline-defaults.js +5 -0
  317. package/src/timeline-extent.js +8 -20
  318. package/src/tutorial-walk.js +17 -0
  319. package/src/ui-scrub.js +1 -1
  320. package/src/use-shot-camera-tracks.js +12 -0
  321. package/src/view-ghosts.js +97 -0
  322. package/src/view-ghosts.jsx +49 -0
  323. package/src/workflow/AgentPanel.jsx +47 -166
  324. package/src/workflow/AgentProgressCard.jsx +107 -0
  325. package/src/workflow/AgentReplyText.jsx +37 -0
  326. package/src/workflow/AgentTurnSteps.jsx +84 -0
  327. package/src/workflow/LoginPanel.jsx +140 -0
  328. package/src/workflow/WorkflowBuilder.jsx +7 -23
  329. package/src/workflow/agent-client.js +54 -51
  330. package/src/workflow/agent-panel.css +71 -1
  331. package/src/workflow/agent-progress-card.css +167 -0
  332. package/src/workflow/agent-progress-view.js +47 -0
  333. package/src/workflow/agent-turn-steps.css +113 -0
  334. package/src/workflow/agent-turn.js +191 -0
  335. package/src/workflow/login-client.js +34 -0
  336. package/src/workflow/motion-input.js +18 -31
  337. package/src/workflow/node-schema.js +1 -1
  338. package/src/workflow/reply-markdown.js +69 -0
  339. package/src/workflow/scene-asset-sync.js +8 -8
  340. package/tools/agent-session-stats.mjs +61 -0
  341. package/tools/dev-full.mjs +5 -56
  342. package/tools/eslint-baseline.json +12 -13
  343. package/tools/headless-studio.mjs +85 -0
  344. package/tools/knip-baseline.json +0 -179
  345. package/tools/loc-baseline.json +33 -83
  346. package/tools/motion3/extract-body.mjs +103 -0
  347. package/tools/motion3/extract-rig.mjs +85 -0
  348. package/tools/process-supervisor.mjs +0 -88
  349. package/tools/run-tests.mjs +9 -124
  350. package/bin/agent/motion-runtime.mjs +0 -76
  351. package/dist/ardy/cskel27-rest.json +0 -557
  352. package/dist/assets/analytics-B41FmPYI.js +0 -1
  353. package/dist/assets/app-Do-Og5Pu.css +0 -1
  354. package/dist/assets/app-DwP-tl2x.js +0 -4920
  355. package/dist/assets/config-Bx7pYsD9.js +0 -1
  356. package/dist/assets/config-DeoFTYzb.css +0 -1
  357. package/dist/assets/demo-Dt0qZfS7.js +0 -1
  358. package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
  359. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  360. package/dist/assets/ticket-jqMhqL88.js +0 -1
  361. package/dist/assets/workflow-Br-K7Srs.js +0 -179
  362. package/dist/demo/walk-then-stop.npz +0 -0
  363. package/mcp/ardy-prompts.mjs +0 -218
  364. package/src/ardy/auto-fix-panel.css +0 -108
  365. package/src/ardy/auto-physics.js +0 -1194
  366. package/src/ardy/client.js +0 -153
  367. package/src/ardy/convert.js +0 -313
  368. package/src/ardy/cskel27-neutral.js +0 -39
  369. package/src/ardy/cskel27.js +0 -77
  370. package/src/ardy/export.js +0 -165
  371. package/src/ardy/fix-collisions.js +0 -2257
  372. package/src/ardy/foot-lock.js +0 -230
  373. package/src/ardy/motion-calibration.js +0 -118
  374. package/src/ardy/motion-edit.js +0 -213
  375. package/src/ardy/motion-url.js +0 -55
  376. package/src/ardy/mp4-duration.js +0 -102
  377. package/src/ardy/npz.js +0 -595
  378. package/src/ardy/physics-panel.css +0 -21
  379. package/src/ardy/physics-panel.jsx +0 -89
  380. package/src/ardy/physics-refine.js +0 -111
  381. package/src/ardy/physics-review.js +0 -567
  382. package/src/ardy/physics-temporal.js +0 -54
  383. package/src/ardy/platform-fit-panel.css +0 -64
  384. package/src/ardy/platform-fit-panel.jsx +0 -41
  385. package/src/ardy/platform-fit.js +0 -326
  386. package/src/ardy/playback.js +0 -711
  387. package/src/ardy/pose-pin.js +0 -97
  388. package/src/ardy/prompt-clips.js +0 -30
  389. package/src/ardy/retime.js +0 -211
  390. package/src/ardy/root-drop.js +0 -369
  391. package/src/ardy/timeline-resize.js +0 -13
  392. package/src/ardy/to-cskel27.js +0 -225
  393. package/src/ardy/trim.js +0 -33
  394. package/src/ardy/waypoints.js +0 -369
  395. package/src/ardy-bridge-contract.js +0 -281
  396. package/src/domains/motion/motion-video-capture.js +0 -664
  397. package/src/i2v-motion-client.js +0 -77
  398. package/src/i2v-motion-studio.jsx +0 -180
  399. package/src/line-edit.js +0 -1348
  400. package/src/motion/generation.js +0 -105
  401. package/src/motion-readiness-ui.jsx +0 -67
  402. package/src/motion-readiness.js +0 -40
  403. package/src/motion-resources.js +0 -177
  404. package/src/motion-store.js +0 -65
  405. package/src/motion-trail.js +0 -601
  406. package/src/multimodel-ingest.js +0 -394
  407. package/src/panels/PromptBlocksPanel.jsx +0 -444
  408. package/src/panels/VideoCapturePanel.jsx +0 -181
  409. package/src/pose-extract/browser.js +0 -42
  410. package/src/pose-extract/detector.js +0 -100
  411. package/src/pose-extract/fit.js +0 -227
  412. package/src/pose-extract/image-frame.js +0 -51
  413. package/src/pose-extract/index.js +0 -58
  414. package/src/pose-extract/keyframes.js +0 -74
  415. package/src/pose-extract/landmarks.js +0 -162
  416. package/src/pose-extract/mirror.js +0 -153
  417. package/src/pose-extract/take.js +0 -131
  418. package/src/pose-extract/video-frames.js +0 -91
  419. package/src/take-recipe.js +0 -165
  420. package/src/trail-key-conflicts.js +0 -39
  421. package/src/trail-pick.js +0 -59
  422. package/src/use-i2v-capture-modal.js +0 -15
  423. package/tools/ardy/BRIDGE.md +0 -37
  424. package/tools/ardy/GVHMR-NPZ.md +0 -68
  425. package/tools/ardy/README.md +0 -124
  426. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  427. package/tools/ardy/artifacts.mjs +0 -34
  428. package/tools/ardy/bridge.mjs +0 -1732
  429. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  430. package/tools/ardy/cclay_constrained_generate.py +0 -561
  431. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  432. package/tools/ardy/cclay_motion_edit.py +0 -510
  433. package/tools/ardy/cclay_sequence_generate.py +0 -705
  434. package/tools/ardy/cclay_worker.py +0 -304
  435. package/tools/ardy/dump-npz.py +0 -205
  436. package/tools/ardy/extract-rest.mjs +0 -299
  437. package/tools/ardy/extract.mjs +0 -507
  438. package/tools/ardy/footage.mjs +0 -467
  439. package/tools/ardy/gvhmr-floor.mjs +0 -102
  440. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  441. package/tools/ardy/gvhmr_fastpath.py +0 -168
  442. package/tools/ardy/gvhmr_trajectory.py +0 -240
  443. package/tools/ardy/merge-text-encoder.py +0 -160
  444. package/tools/ardy/mocap-metrics.mjs +0 -200
  445. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  446. package/tools/ardy/motion-stabilize.mjs +0 -348
  447. package/tools/ardy/npz.mjs +0 -414
  448. package/tools/ardy/pose-to-npz.mjs +0 -106
  449. package/tools/ardy/prompt-limits.mjs +0 -5
  450. package/tools/ardy/run-edit-on-box.sh +0 -73
  451. package/tools/ardy/run-local.mjs +0 -233
  452. package/tools/ardy/run-on-box.sh +0 -593
  453. package/tools/ardy/run-sequence-on-box.sh +0 -177
  454. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  455. package/tools/ardy/runners/index.mjs +0 -18
  456. package/tools/ardy/runners/local.mjs +0 -455
  457. package/tools/ardy/runners/proc.mjs +0 -125
  458. package/tools/ardy/runners/remote.mjs +0 -252
  459. package/tools/ardy/setup-local.mjs +0 -189
  460. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  461. package/tools/ardy/setup-text-encoder.py +0 -306
  462. package/tools/ardy/smpl-cskel27.mjs +0 -178
  463. package/tools/ardy/visual-qa.mjs +0 -196
  464. package/tools/ardy/vq-car.mjs +0 -52
  465. package/tools/bench/EXP3.md +0 -61
  466. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  467. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  468. package/tools/bench/cclay_bench_runner.py +0 -42
  469. package/tools/bench/cube-contact.mjs +0 -162
  470. package/tools/bench/exp3.mjs +0 -152
  471. package/tools/bench/extract-bench-lib.mjs +0 -216
  472. package/tools/bench/extract-bench.mjs +0 -302
  473. package/tools/bench/fal-generate.mjs +0 -88
  474. package/tools/bench/fit/README.md +0 -189
  475. package/tools/bench/fit/camera.mjs +0 -59
  476. package/tools/bench/fit/contact.mjs +0 -419
  477. package/tools/bench/fit/footlock.mjs +0 -234
  478. package/tools/bench/fit/motion.mjs +0 -85
  479. package/tools/bench/fit/pin.mjs +0 -27
  480. package/tools/bench/fit/remote.mjs +0 -105
  481. package/tools/bench/fit-bench.mjs +0 -126
  482. package/tools/bench/fit-sanity.mjs +0 -45
  483. package/tools/bench/metrics.mjs +0 -313
  484. package/tools/bench/obs/depth.mjs +0 -258
  485. package/tools/bench/obs/extrinsics.mjs +0 -175
  486. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  487. package/tools/bench/obs/ground.mjs +0 -294
  488. package/tools/bench/obs/heading.mjs +0 -151
  489. package/tools/bench/obs/ladder.mjs +0 -555
  490. package/tools/bench/obs/mannequin-betas.json +0 -124
  491. package/tools/bench/obs/remote.mjs +0 -83
  492. package/tools/bench/obs/rest_joints.py +0 -52
  493. package/tools/bench/obs/ybot-targets.mjs +0 -49
  494. package/tools/bench/obs-bench.mjs +0 -520
  495. package/tools/bench/score.mjs +0 -343
  496. package/tools/bench/summarize.mjs +0 -86
  497. package/tools/demo-worker/README.md +0 -137
  498. package/tools/demo-worker/api-client.mjs +0 -233
  499. package/tools/demo-worker/generate.mjs +0 -301
  500. package/tools/demo-worker/index.mjs +0 -466
  501. package/tools/demo-worker/sign-core.mjs +0 -112
  502. package/tools/demo-worker/sign.mjs +0 -161
  503. package/tools/gt-render/browser.mjs +0 -159
  504. package/tools/gt-render/camera-math.mjs +0 -276
  505. package/tools/gt-render/page.mjs +0 -267
  506. package/tools/gt-render/render.mjs +0 -514
  507. package/tools/gt-render/scene-box.mjs +0 -31
  508. package/tools/gt-render/take-transform.mjs +0 -87
  509. package/tools/kimodo/compare-seam.mjs +0 -108
  510. package/tools/kimodo/constraints.mjs +0 -244
  511. package/tools/kimodo/edit.mjs +0 -259
  512. package/tools/kimodo/effector-constraints.mjs +0 -200
  513. package/tools/kimodo/generate.mjs +0 -646
  514. package/tools/kimodo/local-output.mjs +0 -265
  515. package/tools/kimodo/measure-pose.mjs +0 -65
  516. package/tools/kimodo/measure-preserve.mjs +0 -899
  517. package/tools/kimodo/measure-waypoints.mjs +0 -55
  518. package/tools/kimodo/mlx-generate.py +0 -111
  519. package/tools/kimodo/pose-constraints.mjs +0 -248
  520. package/tools/kimodo/preserve-mask.mjs +0 -543
  521. package/tools/kimodo/read-npz.mjs +0 -189
  522. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  523. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  524. package/tools/kimodo/runner.mjs +0 -284
  525. package/tools/kimodo/seam-batch.mjs +0 -77
  526. package/tools/kimodo/setup-local.mjs +0 -30
  527. package/tools/kimodo/setup-on-box.sh +0 -339
  528. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  529. package/tools/morphgs/assets/gen_truth.py +0 -39
  530. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  531. package/tools/morphgs/assets/playback-check.mjs +0 -30
  532. package/tools/morphgs/demo-gate.sh +0 -26
  533. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  534. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  535. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  536. package/tools/morphgs/setup-on-cluster.sh +0 -126
  537. package/tools/projflow/driver.py +0 -1069
  538. package/tools/projflow/generate.mjs +0 -805
  539. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  540. package/tools/projflow/line-edit-job.mjs +0 -547
  541. package/tools/projflow/replay.mjs +0 -452
  542. package/tools/projflow/runner.mjs +0 -269
  543. package/tools/projflow/service.mjs +0 -742
  544. package/tools/track/DESIGN.md +0 -513
  545. package/tools/track/backfill-provenance.mjs +0 -116
  546. package/tools/track/budget.mjs +0 -8
  547. package/tools/track/check-rig.mjs +0 -209
  548. package/tools/track/diagnostics.schema.json +0 -60
  549. package/tools/track/export-rig.mjs +0 -256
  550. package/tools/track/fallback.mjs +0 -7
  551. package/tools/track/fk-parity-fixture.mjs +0 -161
  552. package/tools/track/gate.mjs +0 -349
  553. package/tools/track/masks.mjs +0 -318
  554. package/tools/track/metrics.mjs +0 -201
  555. package/tools/track/publish-obs.mjs +0 -108
  556. package/tools/track/py/check_env.py +0 -61
  557. package/tools/track/py/eval_lr.py +0 -229
  558. package/tools/track/py/lr_viterbi.py +0 -294
  559. package/tools/track/py/masks.py +0 -405
  560. package/tools/track/py/objective.py +0 -382
  561. package/tools/track/py/rig.py +0 -508
  562. package/tools/track/py/scene.py +0 -326
  563. package/tools/track/py/test_joint_indices.py +0 -123
  564. package/tools/track/py/test_lr_viterbi.py +0 -195
  565. package/tools/track/py/test_masks.py +0 -229
  566. package/tools/track/py/test_rig.py +0 -288
  567. package/tools/track/py/test_scene.py +0 -354
  568. package/tools/track/py/test_track.py +0 -328
  569. package/tools/track/py/track.py +0 -411
  570. package/tools/track/remote.mjs +0 -140
  571. package/tools/track/rig-dump.mjs +0 -227
  572. package/tools/track/run-box-tests.mjs +0 -14
  573. package/tools/track/run-box.mjs +0 -168
  574. package/tools/track/setup-box.sh +0 -59
  575. package/tools/track/study-2d.mjs +0 -728
  576. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  577. /package/src/{ardy → motion}/ground.js +0 -0
  578. /package/src/{ardy → motion}/ik-drag.js +0 -0
  579. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  580. /package/src/{ardy → motion}/ik.js +0 -0
  581. /package/src/{ardy → motion}/physics-surface.js +0 -0
  582. /package/src/{ardy → motion}/playback-clock.js +0 -0
  583. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -0,0 +1,83 @@
1
+ // Skill library: parameterised spec builders the agent calls with spec.use instead of writing
2
+ // every key. use: [{ skill, args, f0 }] expands, in order, into keys, anchors, props, fits,
3
+ // objects, checks and channels (the extra channels a skill keys) before compile; each skill reads the pose the earlier keys leave at its
4
+ // f0. The agent's own keys come after the skill keys (a key of the agent wins on the same
5
+ // frame), and the agent's own anchors/props/fits replace a skill's of the same name.
6
+ import { stateAt } from "../compiler/walk.js";
7
+ import { didYouMean, suggestField } from "../compiler/vocab.js";
8
+ import { USE_FIELDS } from "../schema/fields.js";
9
+ import { SKILL_INFO } from "./info.js";
10
+ import { standIdle, walkTo, reachGrip } from "./basic.js";
11
+ import { sitInSeat, exitCar } from "./car.js";
12
+ import { smokeCigarette } from "./smoke.js";
13
+
14
+ const SKILLS = { standIdle, sitInSeat, exitCar, walkTo, reachGrip, smokeCigarette };
15
+ export const SKILL_NAMES = Object.keys(SKILLS);
16
+ const bad = msg => { throw new Error(`MOTION_SPEC_INVALID: ${msg}`); };
17
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
18
+
19
+ function mergeNamed(into, part, what, tag) {
20
+ for (const [name, v] of Object.entries(part ?? {})) {
21
+ if (into[name] && !same(into[name], v)) bad(`${tag} defines ${what}.${name} differently from an earlier skill; rename one or define it yourself in spec.${what}`);
22
+ into[name] = v;
23
+ }
24
+ }
25
+
26
+ /** Lists merge without duplicates; anything else (an unknown check, a non-list) passes on for the validator to name. */
27
+ function mergeChecks(into, part) {
28
+ for (const [name, v] of Object.entries(part ?? {})) {
29
+ if (name === "limits" && v && typeof v === "object" && !Array.isArray(v)) { into.limits = { ...into.limits, ...v }; continue; }
30
+ if (!Array.isArray(v) || (into[name] !== undefined && !Array.isArray(into[name]))) { into[name] = v; continue; }
31
+ const list = into[name] ?? (into[name] = []);
32
+ for (const item of v) if (!list.some(x => same(x, item))) list.push(item);
33
+ }
34
+ }
35
+
36
+ /** use[i].args against the skill's args (skills/info.js); lenient ignores unknown ones. */
37
+ function argsOk(u, i, lenient) {
38
+ if (u.args !== undefined && (!u.args || typeof u.args !== "object" || Array.isArray(u.args))) bad(`use[${i}].args must be an object, got ${JSON.stringify(u.args)}`);
39
+ const known = Object.keys(SKILL_INFO[u.skill].args);
40
+ if (!lenient) for (const a of Object.keys(u.args ?? {})) if (!known.includes(a)) bad(`use[${i}].args.${a} is not an arg of ${u.skill}${suggestField(a, known)} (args: ${known.join(", ")})`);
41
+ }
42
+
43
+ /**
44
+ * spec with `use` -> { spec: plain spec (no use), used: [{ skill, f0, end, paced?, cut? }] }; lenient ignores
45
+ * unknown use[i] fields and args. auto (frames-auto.js) lets a skill that holds to the last
46
+ * frame end where its own motion ends. A skill whose release or settle the last frame cuts ends
47
+ * there holding (cut says so); one that needs frames past the last for its own motion is refused.
48
+ */
49
+ export function expandUse(spec, { sk, scene, lenient = false, auto = false }) {
50
+ if (!spec.use?.length) return { spec, used: [] };
51
+ if (!Array.isArray(spec.use)) bad("spec.use must be an array of { skill, args, f0 }");
52
+ const own = (spec.keys ?? []).map((k, i) => ({ ...k, src: `keys[${i}]` }));
53
+ const merged = { anchors: {}, props: {}, objects: {}, fits: [], checks: {}, channels: [] }, skillKeys = [], used = [];
54
+ spec.use.forEach((u, i) => {
55
+ const tag = `use[${i}] ${u?.skill}`;
56
+ if (!SKILLS[u?.skill]) bad(`use[${i}].skill "${u?.skill}" is not a skill${didYouMean(String(u?.skill), SKILL_NAMES)} (skills: ${SKILL_NAMES.join(", ")})`);
57
+ for (const f of Object.keys(u)) if (!USE_FIELDS.includes(f) && !lenient) bad(`use[${i}] has unknown field "${f}"${suggestField(f, USE_FIELDS)}; an entry is { skill, args, f0 }`);
58
+ argsOk(u, i, lenient);
59
+ if (!Number.isInteger(u.f0) || u.f0 < 0 || u.f0 > spec.frames - 1) bad(`use[${i}].f0 must be an integer frame within 0..${spec.frames - 1}, got ${JSON.stringify(u.f0)}`);
60
+ const keys = [...skillKeys, ...own];
61
+ const ctx = { sk, scene, frames: spec.frames, auto, self: spec.fix !== false && spec.fix?.self !== false, f0: u.f0, args: structuredClone(u.args ?? {}), tag, keys, state: structuredClone(stateAt(keys, u.f0)), anchors: { ...merged.anchors, ...spec.anchors } };
62
+ const part = SKILLS[u.skill](ctx), last = Math.max(part.end ?? u.f0, ...part.keys.map(k => k.f));
63
+ if (last > spec.frames - 1) bad(`${tag} needs ${last - u.f0} frames from f${u.f0} (to f${last}), past the last frame f${spec.frames - 1}; set frames >= ${last + 1} or frames: "auto"`);
64
+ skillKeys.push(...part.keys.map(k => ({ ...k, src: k.src ?? tag })));
65
+ mergeNamed(merged.anchors, part.anchors, "anchors", tag);
66
+ mergeNamed(merged.props, part.props, "props", tag);
67
+ mergeNamed(merged.objects, part.objects, "objects", tag);
68
+ for (const fit of part.fits ?? []) if (!merged.fits.some(x => same(x, fit))) merged.fits.push(fit);
69
+ for (const ch of part.channels ?? []) if (!merged.channels.includes(ch)) merged.channels.push(ch);
70
+ mergeChecks(merged.checks, part.checks);
71
+ used.push({ skill: u.skill, f0: u.f0, end: part.end, ...(part.paced ? { paced: part.paced } : {}), ...(part.cut ? { cut: part.cut } : {}) });
72
+ });
73
+ const out = { ...spec, keys: [...skillKeys, ...own], anchors: { ...merged.anchors, ...spec.anchors }, props: { ...merged.props, ...spec.props },
74
+ objects: { ...merged.objects, ...spec.objects } };
75
+ const channels = [...new Set([...merged.channels, ...(Array.isArray(spec.channels) ? spec.channels : [])])];
76
+ if (channels.length) out.channels = channels;
77
+ const mine = spec.fits ?? [];
78
+ out.fits = [...merged.fits.filter(f => !mine.some(m => m.side === f.side && m.anchor === f.anchor)), ...mine];
79
+ mergeChecks(merged.checks, spec.checks);
80
+ out.checks = merged.checks;
81
+ delete out.use;
82
+ return { spec: out, used };
83
+ }
@@ -0,0 +1,65 @@
1
+ // What each skill takes and does, for motion.describe { topic: "skills" } and the expander (an
2
+ // arg not listed here is refused; a lenient reload ignores it): args (JSON Schema fragments),
3
+ // required args, what must come before it (needs), how many frames it runs from f0 and what it
4
+ // adds to the spec. A test keeps the names equal to SKILL_NAMES (skills/index.js). `tune` names
5
+ // the numeric args repair may nudge (arg -> one step) when a near-limit QA group names the skill
6
+ // as its cause (repair-tune.js); the start value is the arg as sent, else its schema default.
7
+ // `place` names the args that set where the body stands (arg -> one step, m): repair-tune.js
8
+ // steps them when a GOAL group misses and this skill placed the body last.
9
+ import { WALK_FIELDS } from "../schema/fields.js";
10
+ import { WALK_TYPES, num, int, str, side, ref } from "../schema/types.js";
11
+ import { FINGER_PRESETS } from "../compiler/vocab.js";
12
+
13
+ /** The last frame of a walk (generator or walkTo): feet together 18 frames after the last step. */
14
+ export const walkEnd = gen => gen.f0 + 16 + (gen.steps ?? 8) * (gen.period ?? 20) + 18;
15
+
16
+ const xz = d => ({ type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: d });
17
+ const carArgs = names => Object.fromEntries(names.map(n => [n, str(n === "car" ? "car prefix: fills <car>_Seat_Driver, _Seat_Driver_Back, _Door_L/R, _Door_Handle, _Steering_Wheel" : "scene object id (overrides the car default)")]));
18
+ const walkArgs = Object.fromEntries(WALK_FIELDS.filter(f => f !== "type" && f !== "f0").map(f => [f, WALK_TYPES[f]]));
19
+
20
+ export const SKILL_INFO = {
21
+ standIdle: {
22
+ args: { at: xz("[x, z] world m; default the root keyed before f0"), heading: num("degrees (yaw); default the keyed yaw"), until: int("hold the pose to this frame") },
23
+ needs: "args.at, or a root keyed before f0", frames: "1 (to args.until when given)",
24
+ adds: ["keys: a standing pose, FK arms down"], example: { f0: 0, args: { at: [0, 0], heading: 0 } },
25
+ place: { at: 0.03 },
26
+ },
27
+ walkTo: {
28
+ args: { to: xz("[x, z] world m: the heading turns toward it and the steps cover the distance"), ...walkArgs,
29
+ heading: { oneOf: [num(), WALK_TYPES.heading], description: "degrees: h1, or [h0, h1]" },
30
+ arrive: int("with to and no steps/period/step/swing: the frame to end by (feet together); the engine picks the cadence (observations.planned)") },
31
+ needs: "a standing pose keyed before f0", frames: "16 + steps * period + 18",
32
+ adds: ["keys: a walk ending feet together"], example: { f0: 10, args: { to: [0.5, 1.5] } },
33
+ tune: { lift: 0.01, halfWidth: 0.01, firstStep: 0.01, step: 0.01 }, place: { firstStep: 0.03, step: 0.03 },
34
+ },
35
+ reachGrip: {
36
+ args: { anchor: str("not ending in _l/_r"), side: side("the hand"), reach: int("frames to reach", { default: 20 }), hold: int("frames held", { default: 22 }),
37
+ release: int("frames to let go", { default: 12 }), elbow: ref("vec3", "elbow pole"), fingers: { enum: FINGER_PRESETS, default: "grip" },
38
+ define: { type: "object", description: "an anchor (topic anchors) named args.anchor" }, orient: { type: "boolean", description: "false: no grip turn" } },
39
+ required: ["anchor", "side"], needs: "args.anchor in spec.anchors or args.define", frames: "reach + hold + release (54 by default)",
40
+ adds: ["props.<anchor>Grip (fingers point)", "fits: fingers onto the anchor", "checks.contacts \"fingers on <anchor>\"", "checks.allow hand on the anchor's object",
41
+ "keys: w_<anchor>_<side>, fingers_<side>, armw_<side>"],
42
+ example: { f0: 10, args: { anchor: "bar", side: "r", define: { kind: "world", p: [-0.2, 1.2, 0.33] } } },
43
+ tune: { reach: 2, release: 2 },
44
+ },
45
+ sitInSeat: {
46
+ args: { ...carArgs(["car", "seat", "seatBack", "wheel"]), offset: { ...ref("vec3", "hips from the seat top, seat frame"), default: [0, 0.17, 0.02] }, floor: num("floor y under the feet, m") },
47
+ needs: "args.car or args.seat (a scene box)", frames: "1", adds: ["keys: seated, hands at the wheel", "checks.supports seat", "checks.allow seat, back, wheel"],
48
+ example: { f0: 0, args: { car: "Car" } },
49
+ },
50
+ exitCar: {
51
+ args: { ...carArgs(["car", "seat", "door", "handle"]), side: side("driver door side, default l"), channel: str("the door hinge channel", { default: "door" }) },
52
+ needs: "seated in the car before f0 (sitInSeat); a car on the ground (seat base y 0.4)", frames: "246",
53
+ adds: ["objects: door and handle hinges", "anchors handle, lthigh, rthigh", "props.grip", "fits, checks.contacts, checks.allow"],
54
+ example: { f0: 0, args: { car: "Car" } },
55
+ },
56
+ smokeCigarette: {
57
+ args: { until: int("hold to this frame; default the last frame; under f0+264 the beats are paced to fit"),
58
+ kit: { type: "object", properties: { cig: str("the cigarette kit's rootId (object.createKit)"), lighter: str("the lighter kit's rootId"), exhale: str("the exhale kit's rootId (or an effect): it follows the mouth") },
59
+ required: ["cig", "lighter"], additionalProperties: false, description: "kits the skill carries and lights: it adds their spec.objects entries itself" } },
60
+ needs: "a stand or squat keyed before f0", frames: "265 nominal; fits 199..264 by pacing",
61
+ adds: ["anchors lpocket, rpocket, hold, mouth, flame", "props cig, lighter", "channels cig_vis, lit_vis, flame_vis, ember_vis, exhale, ember",
62
+ "checks contacts, orient, below, appear, mouthPoints"],
63
+ example: { f0: 0, args: {} },
64
+ },
65
+ };
@@ -0,0 +1,109 @@
1
+ // smokeCigarette from any posture, and dressed with kits (docs/motion-api-v3.1.md C15).
2
+ // args.kit = { cig, lighter, exhale? }: the kit roots object.createKit made (src/kits/) ride the
3
+ // props cig and lighter at the grip (at/along pin the root to the palm), and their lit parts
4
+ // switch with the skill's channels. A part is found by what it is, not by its name: under the
5
+ // cigarette a glowing body and a light (the ember: ember_vis, level ember) and an effect (the
6
+ // tip smoke: ember_vis); under the lighter a glowing body (the flame) and a light (flame_vis).
7
+ // exhale is the exhale kit's root (or any effect): it follows the mouth, facing forward, and shows
8
+ // while the exhale channel is on (C16: { follow: "landmark:mouth", aim: "face", visible: "exhale" }).
9
+ // squat: with the hips keyed low before f0 the cigarette rests over the right knee, not beside
10
+ // the hip where the thigh is, and the hand leaves (and comes back to) the lips straight ahead
11
+ // first (anchor mouthOut): a straight move between the lips and the knee would cross the hand's
12
+ // detour circle around the leaning torso going down, where the detour jumps.
13
+ import { skillError } from "./book.js";
14
+ import { groupsOf } from "../scene.js";
15
+ import { pose } from "../compiler/pose.js";
16
+ import { bonePoint } from "../compiler/skeleton.js";
17
+ import { add, sub, scale, len, apply, ry } from "../compiler/math.js";
18
+ import { HAND_CLEAR, keepOut, wristBody } from "../body.js";
19
+
20
+ const KIT_FIELDS = ["cig", "lighter", "exhale"];
21
+ /** The exhale emitter's entry: at the lips, its +Y (the puff) along the face, shown while breathing out. */
22
+ const EXHALE = { follow: "landmark:mouth", aim: "face", visible: "exhale" };
23
+ const KIT_OF = { cig: "cigarette", lighter: "lighter" };
24
+ /** Below this share of the rig's standing hips height the pose before f0 is a squat. */
25
+ const SQUAT_HIPS = 0.7;
26
+ /** The resting cigarette hand over the right knee, from the knee joint in the body frame (m): out, up, forward. */
27
+ const OVER_KNEE = [-0.05, 0.07, 0.1];
28
+ /** A knee farther than this share of the arm's length from the shoulder: the hand rests on the thigh short of it. */
29
+ const REST_REACH = 0.88;
30
+ /** Where the hand goes first from the lips in a squat: this far straight ahead of the mouth (m, body frame). */
31
+ const OUT_AHEAD = 0.2;
32
+ /** A squat's side pocket from the root (body frame, the left side; m): out, up, ahead. */
33
+ const SIDE_POCKET = [0.245, 0.08, 0.02];
34
+ /** The pockets on the standing body (body frame: left +x), where the hands fetch the kit. */
35
+ export const POCKET = { l: [0.13, -0.035, 0.08], r: [-0.13, -0.035, 0.08] };
36
+
37
+ /** [id, entry, body] of every record under group `root` in a motion scene (bodies and box-less records). */
38
+ function partsUnder(scene, root) {
39
+ const rows = Object.entries(scene?.objects ?? {}).map(([id, o]) => [id, o, true]).concat(Object.entries(scene?.others ?? {}).map(([id, o]) => [id, o, false]));
40
+ return rows.filter(([, o]) => o.ancestors?.includes(root));
41
+ }
42
+
43
+ function rootOf(ctx, kit, role) {
44
+ const id = kit[role];
45
+ if (typeof id !== "string" || !groupsOf(ctx.scene)[id]) skillError(ctx.tag, `args.kit.${role} ${JSON.stringify(id)} is not a group in the scene; object.createKit { kit: "${KIT_OF[role]}" } answers its rootId`);
46
+ return id;
47
+ }
48
+
49
+ /** How deep a kit may sit in the thigh its pocket is on, and in the hip over it (a squat folds the pocket into the hip crease): it comes out of (and goes back into) the pocket there. */
50
+ const POCKET_DEPTH = { cig: 0.03, lighter: 0.045 };
51
+ /** Squatting, the kit comes out of a side pocket past the fat leg capsules (thigh, hips, shin) the scene check reads. */
52
+ const SQUAT_POCKET = 0.07;
53
+ /** ...and the hand in a side pocket touches the thigh and the hip that deep (m). */
54
+ const POCKET_TOUCH = 0.04;
55
+
56
+ /**
57
+ * The spec.objects entries args.kit asks for and the pocket allowances (null without it). at:
58
+ * absolute frames { cig, lit, unlit } the cigarette and the lighter appear and the lighter goes back; squat: the squat pockets.
59
+ */
60
+ export function kitObjects(ctx, at) {
61
+ const kit = ctx.args.kit;
62
+ if (kit === undefined) return null;
63
+ if (!kit || typeof kit !== "object" || Array.isArray(kit)) skillError(ctx.tag, `args.kit must be { cig, lighter, exhale? } (kit root ids), got ${JSON.stringify(kit)}`);
64
+ for (const f of Object.keys(kit)) if (!KIT_FIELDS.includes(f)) skillError(ctx.tag, `args.kit has unknown field "${f}"; fields: ${KIT_FIELDS.join(", ")}`);
65
+ const cig = rootOf(ctx, kit, "cig"), lighter = rootOf(ctx, kit, "lighter");
66
+ const objects = {
67
+ [cig]: { prop: "cig", grab: at.cig, at: [0, 0, 0], along: [0, 0, 1], visible: "cig_vis" },
68
+ [lighter]: { prop: "lighter", grab: at.lit, release: at.unlit, at: [0, 0, 0], along: [0, 1, 0], visible: "lit_vis" },
69
+ };
70
+ for (const [id, o, body] of partsUnder(ctx.scene, cig)) {
71
+ if ((body && o.glow) || (!body && o.renderer === "light")) objects[id] = { visible: "ember_vis", level: "ember" };
72
+ else if (!body && o.renderer === "effect") objects[id] = { visible: "ember_vis" };
73
+ }
74
+ for (const [id, o, body] of partsUnder(ctx.scene, lighter)) if ((body && o.glow) || (!body && o.renderer === "light")) objects[id] = { visible: "flame_vis" };
75
+ if (kit.exhale !== undefined) {
76
+ if (typeof kit.exhale !== "string" || !(ctx.scene?.others?.[kit.exhale] || ctx.scene?.objects?.[kit.exhale] || groupsOf(ctx.scene)[kit.exhale])) {
77
+ skillError(ctx.tag, `args.kit.exhale ${JSON.stringify(kit.exhale)} is not a scene object; object.createKit { kit: "exhale" } answers its rootId (it follows the mouth)`);
78
+ }
79
+ objects[kit.exhale] = { ...EXHALE };
80
+ }
81
+ const deep = d => (at.squat ? SQUAT_POCKET : d);
82
+ const allow = [["thigh_r", cig, deep(POCKET_DEPTH.cig)], ["thigh_l", lighter, deep(POCKET_DEPTH.lighter)], ["hips", cig, deep(POCKET_DEPTH.cig)], ["hips", lighter, deep(POCKET_DEPTH.lighter)],
83
+ ...(at.squat ? [["shin", cig, SQUAT_POCKET], ["shin", lighter, SQUAT_POCKET],
84
+ // the hands in the side pockets brush the thigh and the hip (qa/body-checks.js)
85
+ ["hand_l", "thigh_l", POCKET_TOUCH], ["hand_r", "thigh_r", POCKET_TOUCH], ["hand", "hips", POCKET_TOUCH]] : [])].map(([part, object, depth]) => ({ part, object, depth }));
86
+ return { objects, allow };
87
+ }
88
+
89
+ /**
90
+ * For a squat keyed before f0, the body-frame anchors { hold, mouthOut }: the resting hand over the
91
+ * right knee and the point the hand leaves the lips toward; null when the pose is not a squat.
92
+ */
93
+ export function squatAnchors(ctx) {
94
+ const { sk, state } = ctx, I = sk.index, P = sk.restWorldPos;
95
+ if (!(state.root[1] < SQUAT_HIPS * P[I.Hips][1])) return null;
96
+ const { s } = pose(sk, state), yaw = state.yaw ?? 0, shoulder = s.P[I.RightArm], body = p => apply(ry(-yaw), sub(p, state.root));
97
+ const arm = len(sub(P[I.RightForeArm], P[I.RightArm])) + len(sub(P[I.RightHand], P[I.RightForeArm]));
98
+ const over = add(s.P[I.RightLeg], apply(ry(yaw), OVER_KNEE)), far = len(sub(over, shoulder));
99
+ const hold = body(far > REST_REACH * arm ? add(shoulder, scale(sub(over, shoulder), REST_REACH * arm / far)) : over);
100
+ const out = add(body(bonePoint(s, I.Head, sk.landmarks.mouth)), [0, 0, OUT_AHEAD]);
101
+ // squatting, the front pockets fold into the hip crease under the thighs: with fix.self (smoke.js) the hands go to the side pockets,
102
+ // SIDE_POCKET from the root (body frame), kept off the skin (the hand comes in from the side)
103
+ const skin = wristBody(sk), pocket = (side, sg) => {
104
+ const at = add(state.root, apply(ry(yaw), [sg * SIDE_POCKET[0], SIDE_POCKET[1], SIDE_POCKET[2]]));
105
+ return body(skin ? keepOut(skin.body, sk, s, at, HAND_CLEAR, skin.skip) : at);
106
+ };
107
+ const r3 = p => p.map(v => Math.round(v * 1000) / 1000);
108
+ return { hold: r3(hold), mouthOut: r3(out), lpocket: r3(pocket("Left", 1)), rpocket: r3(pocket("Right", -1)) };
109
+ }
@@ -0,0 +1,160 @@
1
+ // smokeCigarette, extracted from the car-exit fixture: the right hand takes a cigarette from the
2
+ // right pocket to the lips, the left hand brings a lighter from the left pocket under the tip,
3
+ // lights it, puts it back; exhale, look around, a second drag, hold until `until`. Everything is
4
+ // body-relative (pockets, holding point) or measured (mouth), so it works wherever the character
5
+ // stands, and from any pose keyed before f0 (a squat): the head turns from its own pitch, the
6
+ // right arm's poles lean with the torso (pitch + spine_pitch), and a squat rests the hand over
7
+ // the knee (smoke-kit.js). Channels exhale (1 while breathing out) and ember (the tip's level:
8
+ // 0.6 idle, 2 on a drag) drive kits; args.kit binds them (smoke-kit.js). It runs 265 frames from
9
+ // f0 (f0..f0+264); a shorter span down to 75% of that (199 frames) paces every beat to fit.
10
+ import { keyBook, skillError } from "./book.js";
11
+ import { kitObjects, squatAnchors, POCKET } from "./smoke-kit.js";
12
+ import { apply, rx } from "../compiler/math.js";
13
+
14
+ // Wrist twist / flex per hand pose (degrees), found by sweeping them against the QA checks.
15
+ const WR = { TW_M: 40, FX_M: 0, TW_H: 0, FX_H: 10, TW_L: -80, FX_L: -50 };
16
+ // with fix.self on, the wrist at the mouth bends toward the back of the hand (15 deg standing, 20 squatting over the
17
+ // leaning chest): with the filter on the lips that brings the forearm off the chest (straight, it ran 3-6 cm into the
18
+ // body on the fitted skin, qa/body-checks.js); past 20 standing the knuckles reach the face
19
+ const FX_M_SELF = 15, FX_M_SQUAT = 20;
20
+ /** Lighting brings the hands together: the lighter's hand under the cigarette's, touching it (qa/body-checks.js). */
21
+ const LIGHTING = [["hand_l", "hand_r", 0.045], ["hand_r", "hand_l", 0.045], ["farm_r", "hand_l", 0.02], ["hand_l", "farm_r", 0.02]].map(([part, object, depth]) => ({ part, object, depth }));
22
+ const EL_MOUTH_R = [0.4, -1, 0.6], EL_HOLD_R = [-0.4, -1, -0.3], EL_LIGHT_L = [0.9, -0.6, 0.3];
23
+ const ALL = ["armw_r", "armw_l", "w_rpocket_r", "w_lpocket_l", "w_hold_r", "w_mouth_r", "w_flame_l", "elbow_r", "elbow_l",
24
+ "wtwist_r", "wflex_r", "wtwist_l", "wflex_l", "head_pitch", "head_yaw", "spine_pitch", "root", "fingers_l", "fingers_r",
25
+ "cig_vis", "lit_vis", "flame_vis", "ember_vis", "exhale", "ember"];
26
+ /** The last key at its own pace is f0 + NOMINAL; a span down to PACE_MIN of it is paced to fit. */
27
+ const NOMINAL = 264, PACE_MIN = 0.75, MIN_SPAN = Math.ceil(NOMINAL * PACE_MIN);
28
+ /** motion.compile takes only the timeline rate (motion3.js), so seconds in a refusal are at 24 fps. */
29
+ const secs = n => (n / 24).toFixed(1);
30
+ const DEFAULTS = { armw_r: 0, armw_l: 0, w_rpocket_r: 0, w_lpocket_l: 0, w_hold_r: 0, w_mouth_r: 0, w_flame_l: 0, elbow_r: [-0.5, -0.4, -1], elbow_l: [0.5, -0.4, -1],
31
+ wtwist_r: 0, wflex_r: 0, wtwist_l: 0, wflex_l: 0, head_pitch: 0, head_yaw: 0, spine_pitch: 0, fingers_l: { grip: 0, fist: 0 }, fingers_r: { cigarette: 0 },
32
+ cig_vis: 0, lit_vis: 0, flame_vis: 0, ember_vis: 0, exhale: 0, ember: 0 };
33
+
34
+ export function smokeCigarette(ctx) {
35
+ // by default it holds to the last frame (or ends with its own motion while frames is "auto"); a
36
+ // span short of NOMINAL down to MIN_SPAN is paced, a shorter one refused with what it needs
37
+ const until = ctx.args.until ?? (ctx.auto ? ctx.f0 + NOMINAL : ctx.frames - 1), span = until - ctx.f0;
38
+ if (span < MIN_SPAN) skillError(ctx.tag, `needs frames f${ctx.f0}..f${ctx.f0 + MIN_SPAN} at least: ${MIN_SPAN + 1} frames, ${secs(MIN_SPAN + 1)} s at 24 fps, paced ${PACE_MIN} `
39
+ + `(${NOMINAL + 1} frames, ${secs(NOMINAL + 1)} s, at its own pace); it has f${ctx.f0}..f${until}, `
40
+ + (ctx.args.until === undefined ? `so set frames >= ${ctx.f0 + MIN_SPAN + 1} or frames: "auto"` : `so set args.until >= ${ctx.f0 + MIN_SPAN}`));
41
+ if (!Array.isArray(ctx.state.root)) skillError(ctx.tag, "needs a pose keyed before f0 (root): standing or squatting");
42
+ const fresh = Object.fromEntries(Object.entries(DEFAULTS).filter(([c]) => ctx.state[c] === undefined));
43
+ if (Object.keys(fresh).length) Object.assign(ctx.state, structuredClone(fresh));
44
+ ctx.state.head_yaw = 0;
45
+ // the pose before f0 stays: the head pitches from its own angle, the right arm's poles lean with the torso
46
+ const head0 = ctx.state.head_pitch, lean = (ctx.state.pitch ?? 0) + ctx.state.spine_pitch, H = v => v + head0;
47
+ const EL_MOUTH = lean ? apply(rx(lean), EL_MOUTH_R) : EL_MOUTH_R, EL_HOLD = lean ? apply(rx(lean), EL_HOLD_R) : EL_HOLD_R;
48
+ const squat = squatAnchors(ctx), FX_M = ctx.self ? (squat ? FX_M_SQUAT : FX_M_SELF) : WR.FX_M;
49
+ if (squat) ctx.state.w_mouthOut_r = 0;
50
+ // a short span scales every beat's frame by `pace` (>= 0.75): rounding keeps the keys in order,
51
+ // and a gap of 2 frames or more stays at least 1 (the lighter reaches the tip 3 frames before
52
+ // the flame shows, the pocket 2 before it hides), so each switch still happens with the hand
53
+ // where the contacts and appear checks want it; the steps themselves are not stretched
54
+ const pace = span < NOMINAL ? span / NOMINAL : 1, t = f => Math.round(f * pace), book = keyBook(ctx.state, ctx.f0);
55
+ const k = pace === 1 ? book : { ...book, at: (f, set) => book.at(t(f), set), hold: (f, ...names) => book.hold(t(f), ...names), step: (f, ch, v) => book.step(t(f), ch, v) };
56
+ const root0 = ctx.state.root.slice(), yaw = ctx.state.yaw ?? 0;
57
+ /** The hand from the lips to the rest over f0+a..f0+b, and back; in a squat through mouthOut (smoke-kit.js). */
58
+ const raise = (a, b) => {
59
+ k.hold(a, "w_mouth_r", "w_hold_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch", "root", ...(squat ? ["w_mouthOut_r"] : []));
60
+ if (squat) k.at(Math.round((a + b) / 2), { w_hold_r: 0, w_mouthOut_r: 1, w_mouth_r: 0 });
61
+ k.at(b, { w_mouth_r: 1, w_hold_r: 0, ...(squat ? { w_mouthOut_r: 0 } : {}), wtwist_r: WR.TW_M, wflex_r: FX_M, elbow_r: EL_MOUTH, head_pitch: H(6), root: root0 });
62
+ };
63
+ const lower = (a, b) => {
64
+ k.hold(a, "w_mouth_r", "w_hold_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch", ...(squat ? ["w_mouthOut_r"] : []));
65
+ if (squat) k.at(Math.round((a + b) / 2), { w_mouth_r: 0, w_mouthOut_r: 1, w_hold_r: 0 });
66
+ k.at(b, { w_mouth_r: 0, w_hold_r: 1, ...(squat ? { w_mouthOut_r: 0 } : {}), wtwist_r: WR.TW_H, wflex_r: WR.FX_H, elbow_r: EL_HOLD, head_pitch: H(0) });
67
+ };
68
+ k.hold(0, ...ALL);
69
+ // right hand: pocket -> the cigarette appears -> mouth
70
+ k.at(2, { w_rpocket_r: 1, elbow_r: [-0.5, -0.4, -1] });
71
+ k.hold(6, "armw_r", "w_rpocket_r", "elbow_r", "fingers_r");
72
+ k.at(20, { armw_r: 1 });
73
+ k.at(28, { fingers_r: { cigarette: 1 } });
74
+ k.step(28, "cig_vis", 1);
75
+ k.hold(36, "w_rpocket_r", "w_mouth_r", "wtwist_r", "wflex_r", "elbow_r", "head_pitch");
76
+ k.at(58, { w_rpocket_r: 0, w_mouth_r: 1, wtwist_r: WR.TW_M, wflex_r: FX_M, elbow_r: EL_MOUTH, head_pitch: H(6) });
77
+ // left hand: pocket -> the lighter appears -> under the cigarette tip
78
+ k.at(2, { w_lpocket_l: 1, elbow_l: [0.5, -0.4, -1] });
79
+ k.hold(12, "armw_l", "w_lpocket_l", "elbow_l", "fingers_l");
80
+ k.at(26, { armw_l: 1 });
81
+ k.at(34, { fingers_l: { grip: 0, fist: 1 } });
82
+ k.step(35, "lit_vis", 1);
83
+ k.hold(44, "w_lpocket_l", "w_flame_l", "wtwist_l", "wflex_l", "elbow_l");
84
+ k.at(60, { w_lpocket_l: 0, w_flame_l: 1, wtwist_l: WR.TW_L, wflex_l: WR.FX_L, elbow_l: EL_LIGHT_L });
85
+ // light it: flame on, the ember appears, inhale
86
+ k.step(63, "flame_vis", 1);
87
+ k.step(73, "ember_vis", 1);
88
+ k.step(79, "flame_vis", 0);
89
+ // the ember catches, glows up while she draws (f80-f90, f200-f212) and settles to 0.6; she breathes out twice
90
+ k.step(73, "ember", 1);
91
+ k.at(80, { ember: 2 });
92
+ k.hold(90, "ember");
93
+ k.at(104, { ember: 0.6 });
94
+ k.hold(194, "ember");
95
+ k.at(200, { ember: 2 });
96
+ k.hold(212, "ember");
97
+ k.at(224, { ember: 0.6 });
98
+ k.step(116, "exhale", 1);
99
+ k.step(138, "exhale", 0);
100
+ k.step(238, "exhale", 1);
101
+ k.step(260, "exhale", 0);
102
+ // the left hand puts the lighter back and drops
103
+ k.hold(80, "w_flame_l", "w_lpocket_l", "wtwist_l", "wflex_l", "elbow_l");
104
+ // a squat brings the lighter back to the pocket a little slower (its path crosses the hand's detour circle on the way
105
+ // down); with fix.self to the side pocket slower still (at 96 the forearm pops), the lighter going in when the hand gets there
106
+ const back = squat && ctx.self ? 8 : 0;
107
+ k.at(back ? 96 + back : squat ? 100 : 96, { w_flame_l: 0, w_lpocket_l: 1, wtwist_l: 0, wflex_l: 0, elbow_l: [0.5, -0.4, -1] });
108
+ k.hold(101 + back, "fingers_l");
109
+ k.step(102 + back, "lit_vis", 0);
110
+ k.hold(104 + back, "armw_l", "w_lpocket_l");
111
+ k.at(112, { fingers_l: { grip: 0, fist: 0 } });
112
+ k.at(120, { armw_l: 0, w_lpocket_l: 1 });
113
+ k.at(123, { w_lpocket_l: 0 });
114
+ // the right hand lowers to the holding position; exhale with the head up
115
+ lower(92, 114);
116
+ k.at(124, { head_pitch: H(-14) });
117
+ k.at(140, { head_pitch: H(0) });
118
+ // idle: weight onto the left leg, look around
119
+ const lv = [Math.cos(yaw * Math.PI / 180), 0, -Math.sin(yaw * Math.PI / 180)];
120
+ k.hold(120, "root", "head_yaw");
121
+ k.at(146, { root: root0.map((v, i) => v + 0.03 * lv[i] - (i === 1 ? 0.005 : 0)), head_yaw: -22 });
122
+ k.at(168, { head_yaw: 0 });
123
+ // second drag
124
+ raise(170, 194);
125
+ lower(212, 236);
126
+ k.at(246, { head_pitch: H(-14) });
127
+ k.at(264, { head_pitch: H(0) });
128
+ book.hold(span, ...ALL, ...(squat ? ["w_mouthOut_r"] : []));
129
+ const side = Boolean(squat) && ctx.self;
130
+ const kit = kitObjects(ctx, { cig: ctx.f0 + t(28), lit: ctx.f0 + t(35), unlit: ctx.f0 + t(side ? 110 : 102), squat: side });
131
+ return {
132
+ keys: book.keys, end: until, ...(pace < 1 ? { paced: Math.floor(pace * 100) / 100 } : {}), channels: ["exhale", "ember"], ...(kit ? { objects: kit.objects } : {}),
133
+ anchors: {
134
+ lpocket: { kind: "body", p: (ctx.self && squat?.lpocket) || POCKET.l }, rpocket: { kind: "body", p: (ctx.self && squat?.rpocket) || POCKET.r }, hold: { kind: "body", p: squat?.hold ?? [-0.24, 0.03, 0.06] },
135
+ ...(squat ? { mouthOut: { kind: "body", p: squat.mouthOut } } : {}),
136
+ mouth: { kind: "landmark", name: "mouth" }, flame: { kind: "prop", prop: "cig", point: "tip", offset: [0, -0.02, 0] },
137
+ },
138
+ props: {
139
+ cig: { hand: "r", origin: ["RightHandIndex2", "RightHandMiddle2"], points: { filter: [0, 0, -0.026], tip: [0, 0, 0.058] } },
140
+ lighter: { hand: "l", origin: ["LeftHand", "LeftHandMiddle1"], points: { body: [0.01, 0, 0], flame: [0.01, 0.036, 0] } },
141
+ },
142
+ fits: [{ side: "r", anchor: "mouth", effector: { prop: "cig", point: "filter" } }, { side: "l", anchor: "flame", effector: { prop: "lighter", point: "flame" } }],
143
+ checks: {
144
+ contacts: [
145
+ { name: "filter on the lips", point: "prop:cig.filter", target: "anchor:mouth", tol: 0.01, when: "w_mouth_r" },
146
+ { name: "flame under the cigarette tip", point: "prop:lighter.flame", target: "anchor:flame", tol: 0.01, when: "flame_vis" },
147
+ ],
148
+ orient: [
149
+ { name: "lit end points away from the face", from: "prop:cig.filter", to: "prop:cig.tip", along: "face", minCos: 0.64, when: "w_mouth_r" },
150
+ { name: "lighter upright", from: "prop:lighter.body", to: "prop:lighter.flame", along: "up", minCos: 0.85, when: "flame_vis" },
151
+ ],
152
+ below: [{ name: "no chicken-wing elbow", joint: "RightForeArm", ref: "RightArm", min: 0.08, when: "w_mouth_r" }],
153
+ appear: [
154
+ { name: "cigarette from the right pocket", channel: "cig_vis", joint: "RightHand", anchor: "rpocket", tol: 0.035 },
155
+ { name: "lighter from the left pocket", channel: "lit_vis", joint: "LeftHand", anchor: "lpocket", tol: 0.035 },
156
+ ],
157
+ mouthPoints: ["cig.filter"], allow: [...(kit?.allow ?? []), ...LIGHTING],
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,59 @@
1
+ // C7. Spec ids and the editor's spec store. specIdOf is final (W0). The store is an LRU of
2
+ // SPEC_STORE_SIZE entries (lane D2): put, get, keepParts and parts all count as a use, and the
3
+ // least recently used entry goes first. It lives in the editor page's memory: a reload forgets
4
+ // it, and the agent then sends the whole spec again (motion3.js answers an unknown base or
5
+ // specId with that advice).
6
+ // frames: "auto" is an intent the stored spec cannot carry: the spec a specId names (and a take
7
+ // keeps) holds the resolved number. The pipeline marks such a spec (markFramesAuto) and the store
8
+ // remembers the mark beside the entry, so get() answers frames: "auto" again and base + patch
9
+ // re-resolves the length (keys appended past the old end extend it instead of failing).
10
+
11
+ export const SPEC_STORE_SIZE = 64;
12
+ const resolvedFromAuto = new WeakSet();
13
+
14
+ /** Marks a spec whose numeric frames were resolved from frames: "auto" (pipeline.js); answers it. */
15
+ export function markFramesAuto(spec) {
16
+ resolvedFromAuto.add(spec);
17
+ return spec;
18
+ }
19
+
20
+ /** Canonical JSON: object keys sorted at every level (arrays keep their order). */
21
+ const canonical = v => (Array.isArray(v) ? `[${v.map(canonical).join(",")}]`
22
+ : v && typeof v === "object" ? `{${Object.keys(v).sort().filter(k => v[k] !== undefined).map(k => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`
23
+ : JSON.stringify(v) ?? "null");
24
+
25
+ /** "spec-" + the first 12 hex digits of the FNV-1a 64-bit hash of the spec's canonical JSON. */
26
+ export function specIdOf(spec) {
27
+ let h = 0xcbf29ce484222325n;
28
+ for (const byte of new TextEncoder().encode(canonical(spec))) h = BigInt.asUintN(64, (h ^ BigInt(byte)) * 0x100000001b3n);
29
+ return `spec-${h.toString(16).padStart(16, "0").slice(0, 12)}`;
30
+ }
31
+
32
+ /**
33
+ * The editor's store of compiled specs and their full output parts (C7).
34
+ * put(spec) -> specId remember a compiled spec (the most recent SPEC_STORE_SIZE)
35
+ * get(specId) -> spec | null a copy of a remembered spec; frames: "auto" when it was
36
+ * resolved from "auto" (the mark sticks to the specId)
37
+ * keepParts(specId, parts) the overflow hook: { groups, timeline, probes } in full,
38
+ * before fitOutput cuts them (report.js)
39
+ * parts(specId) -> parts | null what motion.report pages and delta compares against
40
+ */
41
+ export function createSpecStore(max = SPEC_STORE_SIZE) {
42
+ const entries = new Map(), size = Math.max(1, max);
43
+ // a Map keeps insertion order: re-inserting moves an entry to the end, the first is the oldest
44
+ const touch = (id, entry) => { entries.delete(id); entries.set(id, entry); while (entries.size > size) entries.delete(entries.keys().next().value); return entry; };
45
+ const seen = id => (entries.has(id) ? touch(id, entries.get(id)) : null);
46
+ return {
47
+ put(spec) {
48
+ const id = specIdOf(spec), was = entries.get(id);
49
+ touch(id, { ...was, spec: structuredClone(spec), framesAuto: resolvedFromAuto.has(spec) || was?.framesAuto === true });
50
+ return id;
51
+ },
52
+ get(id) {
53
+ const entry = seen(id);
54
+ return entry?.spec ? { ...structuredClone(entry.spec), ...(entry.framesAuto ? { frames: "auto" } : {}) } : null;
55
+ },
56
+ keepParts(id, parts) { touch(id, { ...entries.get(id), parts }); },
57
+ parts(id) { return seen(id)?.parts ?? null; },
58
+ };
59
+ }
@@ -0,0 +1,155 @@
1
+ // Timing edits on an installed take v3: the take is cut into segments, each playing a SOURCE
2
+ // frame range at its own speed (0.1x..4x), and segments can be removed or the take trimmed.
3
+ // The edit never touches the compiled take: renderTakeEdit resamples the source arrays (local
4
+ // bone quaternions, hips, object tracks) onto the timeline, so undo, reload and a new compile
5
+ // all start from the same untouched source. Frame numbers the timeline shows are edited frames;
6
+ // keys pinned to them (IK corrections) ride remapFrameKeys when the edit changes.
7
+
8
+ const SPEED_MIN = 0.1, SPEED_MAX = 4, SPEED_STEP = 0.1;
9
+ export const TRIM_MIN_FRAMES = 12;
10
+ const normalizeSpeed = speed => Number((Math.round(Number(speed) / SPEED_STEP) * SPEED_STEP).toFixed(1));
11
+ const sourceFrames = segment => segment.sourceEnd - segment.sourceStart + 1;
12
+ const timelineFrames = segment => Math.max(1, Math.round(sourceFrames(segment) / segment.speed));
13
+
14
+ export function createTakeEdit(frames) {
15
+ if (!Number.isInteger(frames) || frames < 1) throw new Error("createTakeEdit: frames must be a positive integer");
16
+ return [{ id: "motion-0", sourceStart: 0, sourceEnd: frames - 1, speed: 1 }];
17
+ }
18
+
19
+ export const takeEditDuration = edit => (Array.isArray(edit) ? edit.reduce((total, segment) => total + timelineFrames(segment), 0) : 0);
20
+
21
+ /** Each segment with where it sits on the timeline (inclusive frames). */
22
+ export function takeEditLayout(edit) {
23
+ let cursor = 0;
24
+ return edit.map(segment => {
25
+ const frames = timelineFrames(segment), item = { ...segment, timelineStart: cursor, timelineEnd: cursor + frames - 1, timelineFrames: frames };
26
+ cursor += frames;
27
+ return item;
28
+ });
29
+ }
30
+
31
+ /** The (fractional) source frame a timeline frame shows. */
32
+ export function timelineToSource(edit, frame) {
33
+ const layout = takeEditLayout(edit);
34
+ const at = Math.max(0, Math.min(Number.isFinite(frame) ? frame : 0, layout.at(-1).timelineEnd));
35
+ const segment = layout.find(item => at <= item.timelineEnd) ?? layout.at(-1);
36
+ if (segment.timelineFrames <= 1) return segment.sourceStart;
37
+ return segment.sourceStart + ((at - segment.timelineStart) / (segment.timelineFrames - 1)) * (segment.sourceEnd - segment.sourceStart);
38
+ }
39
+
40
+ /** The timeline frame that shows a source frame; null when no segment covers it (cut away). */
41
+ export function sourceToTimeline(edit, source) {
42
+ const segment = takeEditLayout(edit).find(item => source >= item.sourceStart && source <= item.sourceEnd);
43
+ if (!segment) return null;
44
+ if (segment.sourceEnd === segment.sourceStart) return segment.timelineStart;
45
+ return Math.round(segment.timelineStart + ((source - segment.sourceStart) / (segment.sourceEnd - segment.sourceStart)) * (segment.timelineFrames - 1));
46
+ }
47
+
48
+ const remapTimelineFrame = (previous, next, frame) => sourceToTimeline(next, timelineToSource(previous, frame));
49
+
50
+ /** Encoded frame keys ([{ frame, ... }]) through an edit change: the earlier old frame wins a
51
+ * shared new frame, and keys whose source frames were cut away drop out. */
52
+ export function remapFrameKeys(keys, previous, next) {
53
+ const out = new Map();
54
+ for (const key of [...keys].sort((a, b) => a.frame - b.frame)) {
55
+ const frame = remapTimelineFrame(previous, next, key.frame);
56
+ if (frame !== null && !out.has(frame)) out.set(frame, { ...key, frame });
57
+ }
58
+ return [...out.values()];
59
+ }
60
+
61
+ /** Split the segment under timeline frame `frame` there. */
62
+ export function splitTakeEdit(edit, frame) {
63
+ const layout = takeEditLayout(edit), cut = Math.round(frame);
64
+ if (cut <= 0 || cut >= takeEditDuration(edit)) return edit;
65
+ const index = layout.findIndex(segment => cut > segment.timelineStart && cut <= segment.timelineEnd);
66
+ if (index < 0) return edit;
67
+ const segment = layout[index];
68
+ const sourceCut = Math.max(segment.sourceStart + 1, Math.min(segment.sourceEnd, Math.round(timelineToSource(edit, cut))));
69
+ if (sourceCut > segment.sourceEnd) return edit;
70
+ const next = [...edit];
71
+ next.splice(index, 1,
72
+ { id: `${segment.id}-a`, sourceStart: segment.sourceStart, sourceEnd: sourceCut - 1, speed: segment.speed },
73
+ { id: `${segment.id}-b`, sourceStart: sourceCut, sourceEnd: segment.sourceEnd, speed: segment.speed });
74
+ return next;
75
+ }
76
+
77
+ export function setSegmentSpeed(edit, id, speed) {
78
+ const normalized = normalizeSpeed(speed);
79
+ if (!Number.isFinite(normalized) || normalized < SPEED_MIN || normalized > SPEED_MAX) throw new Error(`speed must be between ${SPEED_MIN} and ${SPEED_MAX} in ${SPEED_STEP} steps`);
80
+ return edit.map(segment => (segment.id === id ? { ...segment, speed: normalized } : segment));
81
+ }
82
+
83
+ /** Drop one segment; the last one stays (no motion is motion.clear's job). */
84
+ export function removeSegment(edit, id) {
85
+ if (edit.length < 2) return edit;
86
+ const next = edit.filter(segment => segment.id !== id);
87
+ return next.length === edit.length ? edit : next;
88
+ }
89
+
90
+ /** The speed that plays a segment across `frames` timeline frames (the stretch grip). */
91
+ export function segmentSpeedForFrames(segment, frames) {
92
+ const raw = sourceFrames(segment) / Math.max(1, Math.round(Number(frames) || 1));
93
+ return normalizeSpeed(Math.min(SPEED_MAX, Math.max(SPEED_MIN, raw)));
94
+ }
95
+
96
+ /** Keep timeline frames start..end (inclusive) of the edited take. */
97
+ export function trimTakeEdit(edit, start, end) {
98
+ const duration = takeEditDuration(edit);
99
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end >= duration || start > end) throw new Error(`trim range ${start}..${end} is outside 0..${duration - 1}`);
100
+ return takeEditLayout(edit).flatMap(segment => {
101
+ const from = Math.max(start, segment.timelineStart), to = Math.min(end, segment.timelineEnd);
102
+ if (from > to) return [];
103
+ const sourceStart = Math.max(segment.sourceStart, Math.ceil(timelineToSource(edit, from) - 1e-9));
104
+ const sourceEnd = Math.min(segment.sourceEnd, Math.floor(timelineToSource(edit, to) + 1e-9));
105
+ return sourceStart <= sourceEnd ? [{ id: `${segment.id}-t${sourceStart}`, sourceStart, sourceEnd, speed: segment.speed }] : [];
106
+ });
107
+ }
108
+
109
+ export const isIdentityEdit = (edit, frames) => !edit || (edit.length === 1 && edit[0].sourceStart === 0 && edit[0].sourceEnd === frames - 1 && edit[0].speed === 1);
110
+
111
+ /** A saved edit checked against a take of `frames` source frames; null when it is not one. */
112
+ export function normalizeTakeEdit(value, frames) {
113
+ if (!Array.isArray(value) || !value.length) return null;
114
+ const ok = value.every(segment => segment && typeof segment.id === "string" && Number.isInteger(segment.sourceStart) && Number.isInteger(segment.sourceEnd)
115
+ && segment.sourceStart >= 0 && segment.sourceStart <= segment.sourceEnd && (!Number.isFinite(frames) || segment.sourceEnd < frames)
116
+ && Number.isFinite(segment.speed) && segment.speed >= SPEED_MIN && segment.speed <= SPEED_MAX);
117
+ return ok ? value.map(({ id, sourceStart, sourceEnd, speed }) => ({ id, sourceStart, sourceEnd, speed })) : null;
118
+ }
119
+
120
+ function nlerp(out, o, a, ia, b, ib, t) {
121
+ const sign = a[ia] * b[ib] + a[ia + 1] * b[ib + 1] + a[ia + 2] * b[ib + 2] + a[ia + 3] * b[ib + 3] < 0 ? -1 : 1;
122
+ let n = 0;
123
+ for (let k = 0; k < 4; k++) { out[o + k] = a[ia + k] * (1 - t) + b[ib + k] * sign * t; n += out[o + k] ** 2; }
124
+ n = Math.sqrt(n) || 1;
125
+ for (let k = 0; k < 4; k++) out[o + k] /= n;
126
+ }
127
+
128
+ function lerp(out, o, a, ia, b, ib, t, width) {
129
+ for (let k = 0; k < width; k++) out[o + k] = a[ia + k] + (b[ib + k] - a[ia + k]) * t;
130
+ }
131
+
132
+ /** The take as the edit plays it: the same take object when there is no edit. */
133
+ export function renderTakeEdit(take) {
134
+ const edit = take && !take.editRendered ? normalizeTakeEdit(take.edit, take.frameCount) : null;
135
+ if (!edit || isIdentityEdit(edit, take.frameCount)) return take;
136
+ const D = takeEditDuration(edit), B = take.bones.length, F = take.frameCount;
137
+ const at = Array.from({ length: D }, (_, f) => {
138
+ const s = timelineToSource(edit, f), i0 = Math.min(F - 1, Math.floor(s));
139
+ return { i0, i1: Math.min(F - 1, i0 + 1), t: s - i0, near: Math.min(F - 1, Math.round(s)) };
140
+ });
141
+ const rot = new Float32Array(D * B * 4), hips = new Float32Array(D * 3);
142
+ at.forEach(({ i0, i1, t }, f) => {
143
+ for (let b = 0; b < B; b++) nlerp(rot, (f * B + b) * 4, take.rot, (i0 * B + b) * 4, take.rot, (i1 * B + b) * 4, t);
144
+ lerp(hips, f * 3, take.hips, i0 * 3, take.hips, i1 * 3, t, 3);
145
+ });
146
+ const objects = Object.fromEntries(Object.entries(take.objects ?? {}).map(([id, track]) => {
147
+ const next = {};
148
+ if (track.pos) { next.pos = new Float32Array(D * 3); at.forEach(({ i0, i1, t }, f) => lerp(next.pos, f * 3, track.pos, i0 * 3, track.pos, i1 * 3, t, 3)); }
149
+ if (track.quat) { next.quat = new Float32Array(D * 4); at.forEach(({ i0, i1, t }, f) => nlerp(next.quat, f * 4, track.quat, i0 * 4, track.quat, i1 * 4, t)); }
150
+ if (track.lvl) { next.lvl = new Float32Array(D); at.forEach(({ i0, i1, t }, f) => lerp(next.lvl, f, track.lvl, i0, track.lvl, i1, t, 1)); }
151
+ for (const key of ["vis", "hold"]) if (track[key]) { next[key] = new Uint8Array(D); at.forEach(({ near }, f) => { next[key][f] = track[key][near]; }); }
152
+ return [id, next];
153
+ }));
154
+ return { ...take, frameCount: D, frames: D, rot, hips, objects, edit, editRendered: true };
155
+ }