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,63 @@
1
+ // Take v3: what the compiler bakes and the player reads. Bone rotations are LOCAL quaternions
2
+ // on the Mixamo control bones (the first depth-first bone of each name), so playback writes
3
+ // them straight onto the rig; the hips also carry a position. Everything is expressed in the
4
+ // character's placement frame (its base position and yaw), in metres, so moving or turning
5
+ // the character carries the take with it. Scene objects the motion animates ride along in
6
+ // world space: one track per spec.objects key (a group: its root only). A switch entry (C15)
7
+ // bakes only vis and lvl, no pos/quat: its record keeps its own place (object-travel.js). A follow
8
+ // entry (C16) bakes pos/quat with hold on every frame: its track is world space throughout. A free entry
9
+ // (C17) bakes pos/quat (and vis/lvl when it names them), world space, no hold.
10
+ import { toLocal } from "./compiler/skeleton.js";
11
+ import { sub, apply, ry, quatFromMat, mul } from "./compiler/math.js";
12
+ import { isCarried, isFollow, isSwitch } from "./carried.js";
13
+ import { isFree } from "./free-motion.js";
14
+ import { rootPoseAt, spanOf } from "./object-pose.js";
15
+
16
+ const TAKE_SCHEMA = "cozyclay.take.v3";
17
+
18
+ /** out = compileMotion(...) result; placement = { position: [x,y,z], yaw } of the character. */
19
+ export function bakeTake(sk, out, placement = { position: [0, 0, 0], yaw: 0 }, meta = {}) {
20
+ const F = out.frames.length, B = sk.names.length;
21
+ const rot = new Float32Array(F * B * 4), hips = new Float32Array(F * 3);
22
+ const unYaw = ry(-(placement.yaw ?? 0));
23
+ for (let f = 0; f < F; f++) {
24
+ const s = out.frames[f].s;
25
+ const local = { P: s.P.map(p => apply(unYaw, sub(p, placement.position))), R: s.R.map(R => mul(unYaw, R)) };
26
+ const { quats, hips: h } = toLocal(sk, local);
27
+ quats.forEach((q, b) => rot.set(q, (f * B + b) * 4));
28
+ hips.set(h, f * 3);
29
+ }
30
+ // one track per spec.objects key: a body, or a group root its parts ride (object-pose.js);
31
+ // `hold` marks the frames a carried record is in hand, and every frame of a follow entry (world
32
+ // space: object-travel.js cuts the carry of its own parents there); a switch bakes vis/lvl only
33
+ const objects = {}, f0 = out.f0 ?? 0;
34
+ for (const [id, spec] of Object.entries(out.ctx.spec.objects ?? {})) {
35
+ const carried = isCarried(spec), follows = isFollow(spec), rides = carried || follows, moves = !isSwitch(spec);
36
+ const pos = moves ? new Float32Array(F * 3) : null, quat = moves ? new Float32Array(F * 4) : null;
37
+ const named = rides || !moves || isFree(spec), vis = named && spec.visible ? new Uint8Array(F) : null, hold = rides ? new Uint8Array(F) : null;
38
+ const lvl = named && spec.level ? new Float32Array(F) : null;
39
+ const [grab, release] = carried ? spanOf(out, spec) : [0, 0];
40
+ for (let f = 0; f < F; f++) {
41
+ if (vis) vis[f] = (out.baked?.[spec.visible]?.[f + f0] ?? 0) > 0.5 ? 1 : 0;
42
+ if (lvl) lvl[f] = Math.max(0, Math.min(4, out.baked?.[spec.level]?.[f + f0] ?? 1));
43
+ if (!moves) continue;
44
+ const o = rootPoseAt(out, id, f + f0);
45
+ if (!o) continue;
46
+ pos.set(o.pos, f * 3); quat.set(quatFromMat(o.R), f * 4);
47
+ if (hold) hold[f] = follows || (f + f0 >= grab && f + f0 <= release) ? 1 : 0;
48
+ }
49
+ objects[id] = { ...(moves ? { pos, quat } : {}), ...(vis ? { vis } : {}), ...(hold ? { hold } : {}), ...(lvl ? { lvl } : {}) };
50
+ }
51
+ return { schema: TAKE_SCHEMA, fps: out.fps, frameCount: F, rig: sk.model, bones: sk.names.slice(), rot, hips, objects, placement, ...meta };
52
+ }
53
+
54
+ /** Local quaternion [x,y,z,w] of bone b at frame f (nearest frame, clamped). */
55
+ export function takeQuat(take, f, b) {
56
+ const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))), i = (ff * take.bones.length + b) * 4;
57
+ return [take.rot[i], take.rot[i + 1], take.rot[i + 2], take.rot[i + 3]];
58
+ }
59
+
60
+ export function takeHips(take, f) {
61
+ const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))) * 3;
62
+ return [take.hips[ff], take.hips[ff + 1], take.hips[ff + 2]];
63
+ }
@@ -0,0 +1,127 @@
1
+ // C4. The compiled motion as beats, for the agent (output.timeline) and the beats contact sheet.
2
+ // At most 12 entries, ordered by f[0]:
3
+ // { beat: skill name | "walk" | "keys", src: "use[i]" | "generators[i]" | "keys",
4
+ // f: [first, last] (inclusive, absolute frames),
5
+ // end: { root: [x,y,z], yaw, hand_l: [x,y,z], hand_r: [x,y,z], on_l: ref | null, on_r: ref | null },
6
+ // planted: { l: [[f0, f1], ...], r: [...] }, // at most 4 per foot
7
+ // events: [{ f, object, event: "grab" | "release" | "show" | "hide" }] }
8
+ // Metres to 3 decimals, degrees to 1. Beats come from the skills expandUse ran (used), the
9
+ // generators, and the runs of keys no skill or generator covers; a retime (out.timeMap) moves
10
+ // them with the keys. end is the pose at f[1]: root = the Hips joint, hand_* = the wrists,
11
+ // on_* = the "object:<id>" that hand holds there (object-pose.js inHandAt), else the
12
+ // "anchor:<a>" whose w_<a>_<k> > 0.5 (the largest). planted = the heel or the ball below
13
+ // QA_LIMITS.plantH over the ground (the floor or an object's top, qa/ground.js) and still (a foot on its ball, heel up, is planted), for 3 frames or more (the first, the last and the longest others
14
+ // when there are more than 4). Events: grab/release when an object enters or leaves a hand,
15
+ // show/hide when a visibility channel (a carry's or a switch's `visible`, or any *_vis channel)
16
+ // crosses 0.5; `object` is the object id (one event per object the channel shows), or the
17
+ // channel's name when no object owns it.
18
+ import { QA_LIMITS } from "./qa/checks.js";
19
+ import { groundSole } from "./qa/ground.js";
20
+ import { inHandAt } from "./object-pose.js";
21
+
22
+ const MAX_BEATS = 12, PER_FOOT = 4, MIN_PLANT = 3, STILL_Y = 0.0005;
23
+ const r3 = v => Math.round(v * 1000) / 1000, r1 = v => Math.round(v * 10) / 10;
24
+ const vec = p => p.map(r3);
25
+
26
+ /** The beats' frame ranges before their poses are read: [{ beat, src, f }]. */
27
+ function beatsOf(out, used, spec) {
28
+ const last = out.frameCount - 1, map = out.timeMap ?? (f => f);
29
+ const beats = used.map((u, i) => ({ beat: u.skill, src: `use[${i}]`, f: [u.f0, u.end ?? u.f0] }));
30
+ (spec.generators ?? []).forEach((g, i) => beats.push({ beat: g.type, src: `generators[${i}]`, f: [g.f0, g.f0 + 16 + (g.steps ?? 8) * (g.period ?? 20) + 18] }));
31
+ const covered = f => beats.some(b => f >= b.f[0] && f <= b.f[1]);
32
+ const loose = [...new Set((spec.keys ?? []).map(k => k.f))].filter(f => Number.isFinite(f) && !covered(f)).sort((a, b) => a - b);
33
+ let run = null;
34
+ for (const f of loose) {
35
+ if (run && !beats.some(b => b.src !== "keys" && b.f[1] > run.f[1] && b.f[0] < f)) run.f[1] = f;
36
+ else beats.push(run = { beat: "keys", src: "keys", f: [f, f] });
37
+ }
38
+ const clamp = f => Math.max(0, Math.min(last, Math.round(map(f))));
39
+ for (const b of beats) b.f = [clamp(b.f[0]), clamp(b.f[1])];
40
+ return beats.sort((a, b) => a.f[0] - b.f[0]).slice(0, MAX_BEATS);
41
+ }
42
+
43
+ /** planted[side][f]: the heel or the ball low and still at absolute frame f (false outside the window). */
44
+ function plantedFlags(out) {
45
+ const n = out.frameCount, a = out.f0 ?? 0, flags = { l: new Uint8Array(n), r: new Uint8Array(n) };
46
+ for (const [k, S] of [["l", "Left"], ["r", "Right"]]) {
47
+ const sole = out.frames.map((fr, i) => (fr ? groundSole(out, i, S) : null));
48
+ sole.forEach((p, i) => {
49
+ const q = sole[i > 0 ? i - 1 : i + 1];
50
+ if (!p || !q) return;
51
+ const ok = ["heel", "ball"].some(key => p[key][1] < QA_LIMITS.plantH && Math.abs(p[key][1] - q[key][1]) < STILL_Y
52
+ && Math.hypot(p[key][0] - q[key][0], p[key][2] - q[key][2]) <= QA_LIMITS.slide);
53
+ flags[k][a + i] = ok ? 1 : 0;
54
+ });
55
+ }
56
+ return flags;
57
+ }
58
+
59
+ function plantedIn(flags, [f0, f1]) {
60
+ const runs = [];
61
+ let start = null;
62
+ for (let f = f0; f <= f1 + 1; f++) {
63
+ const on = f <= f1 && flags[f] === 1;
64
+ if (on && start === null) start = f;
65
+ if (!on && start !== null) { if (f - start >= MIN_PLANT) runs.push([start, f - 1]); start = null; }
66
+ }
67
+ if (runs.length <= PER_FOOT) return runs;
68
+ const inner = runs.slice(1, -1).sort((x, y) => (y[1] - y[0]) - (x[1] - x[0]) || x[0] - y[0]).slice(0, PER_FOOT - 2);
69
+ return [runs[0], ...inner, runs[runs.length - 1]].sort((x, y) => x[0] - y[0]);
70
+ }
71
+
72
+ /** Every grab/release/show/hide of the take, by absolute frame. */
73
+ function eventsOf(out, spec) {
74
+ const n = out.frameCount, events = [], objects = Object.entries(spec.objects ?? {});
75
+ for (const [id, o] of objects) for (let f = 1; f < n; f++) {
76
+ const was = inHandAt(o, f - 1), is = inHandAt(o, f);
77
+ if (is && !was) events.push({ f, object: id, event: "grab" });
78
+ if (was && !is) events.push({ f: f - 1, object: id, event: "release" });
79
+ }
80
+ // every object a channel shows (a carry's or a switch's visible) gets its own event
81
+ const owners = {};
82
+ for (const [id, o] of objects) if (typeof o?.visible === "string") (owners[o.visible] ??= []).push(id);
83
+ for (const ch of new Set([...Object.keys(owners), ...Object.keys(out.baked ?? {}).filter(c => c.endsWith("_vis"))])) {
84
+ const v = out.baked?.[ch];
85
+ if (!v) continue;
86
+ for (let f = 1; f < n; f++) {
87
+ const was = v[f - 1] > 0.5, is = v[f] > 0.5;
88
+ if (was !== is) for (const object of owners[ch] ?? [ch]) events.push({ f, object, event: is ? "show" : "hide" });
89
+ }
90
+ }
91
+ return events.sort((x, y) => x.f - y.f);
92
+ }
93
+
94
+ /** "object:<id>" held by hand k at f, else the "anchor:<a>" with the largest weight > 0.5, else null. */
95
+ function onOf(spec, c, k, f) {
96
+ for (const [id, o] of Object.entries(spec.objects ?? {})) if (spec.props?.[o?.prop]?.hand === k && inHandAt(o, f)) return `object:${id}`;
97
+ let best = null, bw = 0.5;
98
+ for (const [ch, w] of Object.entries(c)) {
99
+ const m = /^w_(.+)_([lr])$/.exec(ch);
100
+ if (m && m[2] === k && w > bw) { best = m[1]; bw = w; }
101
+ }
102
+ return best && `anchor:${best}`;
103
+ }
104
+
105
+ function endOf(out, spec, f) {
106
+ const fr = out.frames[f - (out.f0 ?? 0)];
107
+ if (!fr) return null;
108
+ const { s, c } = fr, I = out.ctx.sk.index;
109
+ return { root: vec(s.P[I.Hips]), yaw: r1(c.yaw ?? 0), hand_l: vec(s.P[I.LeftHand]), hand_r: vec(s.P[I.RightHand]),
110
+ on_l: onOf(spec, c, "l", f), on_r: onOf(spec, c, "r", f) };
111
+ }
112
+
113
+ /**
114
+ * timelineOf(out, { used, spec }) -> entries
115
+ * @param {object} out compileMotion result
116
+ * @param {{ used: { skill: string, f0: number, end: number }[], spec: object }} ctx used = the
117
+ * skills expandUse ran, spec = the expanded spec
118
+ * @returns {object[]}
119
+ */
120
+ export function timelineOf(out, { used, spec }) {
121
+ const beats = beatsOf(out, used ?? [], spec);
122
+ if (!beats.length) return [];
123
+ const flags = plantedFlags(out), events = eventsOf(out, spec);
124
+ return beats.map(b => ({ ...b, end: endOf(out, spec, b.f[1]),
125
+ planted: { l: plantedIn(flags.l, b.f), r: plantedIn(flags.r, b.f) },
126
+ events: events.filter(e => e.f >= b.f[0] && e.f <= b.f[1]) }));
127
+ }
@@ -0,0 +1,121 @@
1
+ // C11 own views: the cameras of a verify_result contact_sheet with `views`. They are the
2
+ // agent's own cameras around the subject, never the user's shot camera. A view stays at one
3
+ // place for every frame of the sheet and frames what matters over those frames (the focus
4
+ // refs, else the whole character, plus any trail), so a small action fills the tile and
5
+ // motion reads as change inside one still frame. Pure over a compile result (compileSpec .out).
6
+ // front in front of the character, 10 degrees up
7
+ // side from the side the focus is on (the right when centred), 10 degrees up
8
+ // three-quarter between front and that side, 25 degrees up
9
+ // top straight down, the character's forward up the image
10
+ // depthViews adds a view when the asked ones would hide depth (see APART below).
11
+ import { refPoint } from "./refs.js";
12
+ import { objectBoxesAt } from "./object-pose.js";
13
+ import { resolveBone } from "./bones.js";
14
+ import { add, sub, scale, dot, cross, norm, apply, quatFromMat } from "./compiler/math.js";
15
+
16
+ export const VIEW_NAMES = ["front", "side", "three-quarter", "top"];
17
+ // FOV is the widest lens; a deep subject (a body seen from above) moves the camera back until
18
+ // its far side is at most DEPTH_RATIO times as far as its near side, and the lens narrows (down
19
+ // to MIN_FOV) to fill the tile again, so perspective stays mild.
20
+ const FOV = 35, MIN_FOV = 12, DEPTH_RATIO = 1.8, MARGIN = 1.12, PAD = 0.06, MIN_HALF = 0.15, MIN_DEPTH = 0.35, UP = [0, 1, 0];
21
+ const ELEVATION = { front: 10, side: 10, "three-quarter": 25 };
22
+ const BARE_OBJECT = /^object:(.+?)(\.(?:center|top|base))?$/s;
23
+ const rad = d => (d * Math.PI) / 180;
24
+ const mean = points => scale(points.reduce(add, [0, 0, 0]), 1 / Math.max(1, points.length));
25
+
26
+ const jointsAt = (out, f) => out.frames?.[f - (out.f0 ?? 0)]?.s.P ?? [];
27
+ const corners = b => [-1, 1].flatMap(x => [0, 2].flatMap(y => [-1, 1].map(z => add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]])))));
28
+
29
+ /** World points of a ref at frame f: a bare object:<id> is its part boxes' corners, any other ref its point. */
30
+ function refPoints(out, f, ref) {
31
+ const bare = BARE_OBJECT.exec(ref);
32
+ if (bare && !bare[2]) return (objectBoxesAt(out, bare[1], f) ?? []).flatMap(corners);
33
+ const p = refPoint(out, f, ref);
34
+ return p ? [p] : [];
35
+ }
36
+
37
+ /** The character's horizontal left and forward over the frames (the hip line, summed). */
38
+ function facingOf(out, frames) {
39
+ const sk = out.ctx.sk, l = sk.index[resolveBone(sk, "LeftUpLeg").bone], r = sk.index[resolveBone(sk, "RightUpLeg").bone];
40
+ let v = [0, 0, 0];
41
+ for (const f of frames) { const P = jointsAt(out, f); if (P.length) { const d = sub(P[l], P[r]); v = add(v, [d[0], 0, d[2]]); } }
42
+ const left = Math.hypot(v[0], v[2]) > 1e-6 ? norm(v) : [1, 0, 0];
43
+ return { left, forward: cross(left, UP) };
44
+ }
45
+
46
+ /** Path of a ref over absolute frames lo..hi: [{ f, p }] where it is known. */
47
+ export function trailPoints(out, ref, [lo, hi]) {
48
+ const points = [];
49
+ for (let f = lo; f <= hi; f++) { const p = refPoint(out, f, ref); if (p) points.push({ f, p }); }
50
+ return points;
51
+ }
52
+
53
+ /** A camera at `back` (unit, target to eye) whose frustum holds every point (with PAD) at MARGIN. */
54
+ function fit(name, points, back, upHint, aspect) {
55
+ const right = norm(cross(upHint, back)), up = cross(back, right);
56
+ const c0 = mean(points), rel = points.map(p => sub(p, c0)), xs = rel.map(q => dot(q, right)), ys = rel.map(q => dot(q, up)), zs = rel.map(q => dot(q, back));
57
+ const cx = (Math.min(...xs) + Math.max(...xs)) / 2, cy = (Math.min(...ys) + Math.max(...ys)) / 2;
58
+ const target = add(c0, add(scale(right, cx), scale(up, cy)));
59
+ // The vertical half-angle tangent each point needs from distance d (x through the aspect).
60
+ const need = d => Math.max(...rel.map((q, i) => MARGIN * Math.max(Math.max(Math.abs(xs[i] - cx) + PAD, MIN_HALF) / aspect, Math.max(Math.abs(ys[i] - cy) + PAD, MIN_HALF)) / (d - zs[i])));
61
+ const near = Math.max(...zs), far = Math.min(...zs);
62
+ let d = Math.max(near + MIN_DEPTH, near + (near - far) / (DEPTH_RATIO - 1)), tanV = need(d);
63
+ if (tanV > Math.tan(rad(FOV / 2))) {
64
+ // Too close for the widest lens: back off at FOV until every point fits.
65
+ tanV = Math.tan(rad(FOV / 2));
66
+ d = Math.max(d, ...rel.map((q, i) => zs[i] + MARGIN * Math.max(Math.max(Math.abs(xs[i] - cx) + PAD, MIN_HALF) / aspect, Math.max(Math.abs(ys[i] - cy) + PAD, MIN_HALF)) / tanV));
67
+ } else tanV = Math.max(tanV, Math.tan(rad(MIN_FOV / 2)));
68
+ const eye = add(target, scale(back, d)), fovDeg = Math.round((2 * Math.atan(tanV) * 180 / Math.PI) * 100) / 100;
69
+ tanV = Math.tan(rad(fovDeg / 2));
70
+ const quaternion = quatFromMat([0, 1, 2].map(i => [right[i], up[i], back[i]]));
71
+ return { name, eye, target, quaternion, fovDeg, aspect, right, up, back, tanV, tanH: tanV * aspect };
72
+ }
73
+
74
+ /**
75
+ * One camera per view name over `frames`: framed on the focus refs' points at those frames
76
+ * (none, or none known there: every joint of the character) and every trail point. Answers
77
+ * [{ name, eye, target, quaternion [x,y,z,w], fovDeg, aspect, right, up, back, tanV, tanH }].
78
+ * Deterministic: the same compile, frames and arguments give the same cameras.
79
+ */
80
+ export function viewCameras(out, { views, frames, focus = [], trails = [], aspect = 1 }) {
81
+ let points = frames.flatMap(f => focus.flatMap(ref => refPoints(out, f, ref)));
82
+ const body = frames.flatMap(f => jointsAt(out, f));
83
+ if (!points.length) points = body;
84
+ points = [...points, ...trails.flatMap(t => t.points.map(q => q.p))];
85
+ if (!points.length) return [];
86
+ const { left, forward } = facingOf(out, frames), hips = mean(frames.map(f => jointsAt(out, f)[out.ctx.sk.index.Hips]).filter(Boolean));
87
+ const side = focus.length && dot(sub(mean(points), hips), left) > 0.02 ? left : scale(left, -1);
88
+ return views.map(name => {
89
+ if (name === "top") return fit(name, points, UP, forward, aspect);
90
+ const e = rad(ELEVATION[name]), level = name === "front" ? forward : name === "side" ? side : norm(add(forward, side));
91
+ return fit(name, points, add(scale(level, Math.cos(e)), scale(UP, Math.sin(e))), UP, aspect);
92
+ });
93
+ }
94
+
95
+ // A picture hides distance along its own line of sight, so a sheet holds two views at least APART
96
+ // degrees apart. Front and three-quarter (45 degrees apart) look along nearly one line: the
97
+ // guitarist's right hand 20 cm in front of the guitar (session df388a9b) showed in neither.
98
+ // LOOK is each view's nominal direction: azimuth from the character's forward, elevation.
99
+ const LOOK = { front: [0, 10], side: [90, 10], "three-quarter": [45, 25], top: [0, 90] }, APART = 60;
100
+ const lookDir = name => { const [a, e] = LOOK[name].map(rad); return [Math.cos(e) * Math.sin(a), Math.sin(e), Math.cos(e) * Math.cos(a)]; };
101
+ const apartDeg = (a, b) => Math.round(Math.acos(Math.min(1, dot(lookDir(a), lookDir(b)))) * 180 / Math.PI);
102
+ const spread = views => views.some((a, i) => views.slice(i + 1).some(b => apartDeg(a, b) >= APART));
103
+
104
+ /**
105
+ * The views a sheet renders: the asked ones when two of them are APART degrees apart, else the
106
+ * asked ones plus the first of side, front and top that makes it so. Answers { views, added }:
107
+ * added is null, or { view, reason } for visualRefs.
108
+ */
109
+ export function depthViews(views) {
110
+ if (spread(views)) return { views, added: null };
111
+ const view = ["side", "front", "top"].find(name => !views.includes(name) && spread([...views, name]));
112
+ const line = views.length > 1 ? `${views.join(" and ")} look along nearly one line (${apartDeg(views[0], views[1])} degrees apart)` : `${views[0]} alone looks along one line`;
113
+ return { views: [...views, view], added: { view, reason: `${line}, so a gap along it (toward or away from the camera) does not show; ${view} looks across it` } };
114
+ }
115
+
116
+ /** Pixel [x, y] of world point p in a width x height image of cam, y counted from the bottom row; null behind it. */
117
+ export function projectPoint(cam, p, width, height) {
118
+ const rel = sub(p, cam.eye), depth = -dot(rel, cam.back);
119
+ if (depth < 0.05) return null;
120
+ return [((dot(rel, cam.right) / (depth * cam.tanH) + 1) / 2) * width, ((dot(rel, cam.up) / (depth * cam.tanV) + 1) / 2) * height];
121
+ }
@@ -0,0 +1,119 @@
1
+ // walkTo { to, arrive }: the engine picks the cadence instead of the agent sweeping period and step by
2
+ // hand. A use[i] walkTo with arrive (the frame the walk should end by, feet together: f0 + 16 + steps *
3
+ // period + 18) and none of steps, period, step, swing gets them planned here, before the skills expand:
4
+ // candidates every steps n and period p (frames per step) whose walk ends by arrive, the strides
5
+ // covering the distance from where the feet are at f0 to `to` (the first stride ~0.8 of
6
+ // the others, as walkTo's defaults), a stride at most STRIDE_MAX, the foot in the air
7
+ // SWING of the period;
8
+ // order a person's walk ratio first (stride x cadence: stride m * period frames ~ WALK_RATIO, the
9
+ // default 0.5 m every 20 frames), then using the time given (a walk that ends long before
10
+ // arrive stands still the rest), and no stroll slower than SLOW m/s;
11
+ // checked each candidate is compiled on a probe spec (the keys and skills up to the walk, the walk,
12
+ // nothing after it; no goals, checks or moving objects) and the first with no POP, REACH,
13
+ // SLIDE, FLOAT, FLIP or KNEE issue between f0 and its end wins (at most TRIALS compiles): the
14
+ // best on-time ones, then the top few again with a longer swing, a longer first stride or a
15
+ // lower lift (a quick walk often needs one of them), then the late ones, earliest first.
16
+ // When no walk ends by arrive (the distance is too far for the frames), the earliest clean one is
17
+ // taken and the note says the speed the request needs and the frame it ends. The spec keeps arrive (a
18
+ // recompile plans the same walk again, cached); observations.planned reads what was chosen.
19
+ import { expandUse } from "./skills/index.js";
20
+ import { stateAt } from "./compiler/walk.js";
21
+
22
+ const WALK_RATIO = 9.7, STRIDE_MIN = 0.25, STRIDE_MAX = 0.75, SWING = 0.88, SLOW = 0.5, TRIALS = 22, LATE = 4, FPS = 24;
23
+ const PERIODS = Array.from({ length: 16 }, (_, i) => 9 + i);
24
+ const PLANNED = ["steps", "period", "step", "swing"];
25
+ const WALK_KINDS = new Set(["POP", "REACH", "SLIDE", "FLOAT", "FLIP", "KNEE"]);
26
+ const cache = new Map();
27
+ const r2 = x => Math.round(x * 100) / 100;
28
+
29
+ /** The walkTo uses that plan their cadence: [{ i, u }]. */
30
+ const toPlan = spec => (Array.isArray(spec?.use) ? spec.use : []).map((u, i) => ({ i, u }))
31
+ .filter(({ u }) => u?.skill === "walkTo" && Number.isInteger(u.args?.arrive) && Array.isArray(u.args?.to) && !PLANNED.some(k => u.args[k] !== undefined)
32
+ && Object.values(u.args).every(v => !(v && typeof v === "object" && !Array.isArray(v))));
33
+
34
+ /** The strides of an n-step walk over d m: { first, step } or null when no stride fits. */
35
+ function strides(d, n) {
36
+ if (n === 2) return d >= STRIDE_MIN && d <= STRIDE_MAX ? { first: d, step: d } : null;
37
+ const first = Math.min(0.62, Math.max(0.3, (0.8 * d) / (n - 2 + 0.8))), step = (d - first) / (n - 2);
38
+ return step >= STRIDE_MIN && step <= STRIDE_MAX ? { first, step } : null;
39
+ }
40
+
41
+ /** Candidates for a walk of d m from f0, best first: on time (ending by arrive) by their score, then late ones by their end. */
42
+ function candidates(d, f0, arrive) {
43
+ const all = [];
44
+ for (const period of PERIODS) for (let n = 2; n <= 40; n++) {
45
+ const s = strides(d, n);
46
+ if (!s) continue;
47
+ const end = f0 + 16 + n * period + 18, v = (s.step * FPS) / period;
48
+ const ratio = Math.abs(Math.log((s.step * period) / WALK_RATIO)), idle = Math.max(0, arrive - end) / Math.max(1, arrive - f0);
49
+ all.push({ args: { steps: n, period, swing: Math.round(SWING * period), step: r2(s.step), firstStep: r2(s.first) }, end, v, late: end > arrive,
50
+ score: ratio + 1.5 * idle + Math.max(0, SLOW - v) * 4 });
51
+ }
52
+ const onTime = all.filter(c => !c.late).sort((a, b) => a.score - b.score), late = all.filter(c => c.late).sort((a, b) => a.end - b.end || a.score - b.score);
53
+ return { onTime, late };
54
+ }
55
+
56
+ /** A quick walk's other shapes: a longer swing, with a longer first stride, a lower lift, or both. */
57
+ function variants(c, d) {
58
+ const { steps: n, period } = c.args, swing = Math.round(0.8 * period), long = n > 2 && c.args.firstStep < 0.62 ? { firstStep: 0.62, step: r2((d - 0.62) / (n - 2)) } : null;
59
+ return [{}, long, { lift: 0.06 }, long && { ...long, lift: 0.06 }].filter(Boolean).map(x => ({ ...c, args: { ...c.args, swing, ...x } }))
60
+ .filter(v => v.args.step >= STRIDE_MIN && v.args.step <= STRIDE_MAX);
61
+ }
62
+
63
+ /** Issues in the walk's own frames (the kinds a cadence causes) of a compiled probe: a count. */
64
+ const walkIssues = (qa, f0, end) => qa.groups.filter(g => WALK_KINDS.has(g.kind) && g.last >= f0 && g.first <= end).reduce((n, g) => n + g.count, 0);
65
+
66
+ /**
67
+ * planWalks(spec, { sk, scene, trial }) -> [{ i, args, note }] for every walkTo with arrive (applyWalkPlans puts
68
+ * the args in). trial(spec) -> QA of a compile of that spec (pipeline.js), or throws.
69
+ */
70
+ export function planWalks(spec, { sk, scene, trial }) {
71
+ const plans = [];
72
+ for (const { i, u } of toPlan(spec)) {
73
+ const key = JSON.stringify([spec.keys, spec.use.slice(0, i + 1), spec.anchors, spec.props, spec.fits, spec.fix, scene?.objects ?? null]);
74
+ if (cache.has(key)) { plans.push(cache.get(key)); continue; }
75
+ const before = spec.use.slice(0, i).filter(x => x.f0 < u.f0);
76
+ let state, ends = [];
77
+ try {
78
+ const { spec: exp, used } = expandUse({ ...spec, frames: 20000, use: before }, { sk, scene, lenient: true });
79
+ state = stateAt(exp.keys ?? [], u.f0); ends = used.map(x => x.end ?? 0);
80
+ } catch { state = null; }
81
+ if (!state?.foot_l || !state?.foot_r) continue;
82
+ const c0 = [(state.foot_l[0] + state.foot_r[0]) / 2, (state.foot_l[2] + state.foot_r[2]) / 2], to = u.args.to, d = Math.hypot(to[0] - c0[0], to[to.length - 1] - c0[1]);
83
+ const { onTime, late } = candidates(d, u.f0, u.args.arrive);
84
+ const probe = c => {
85
+ const frames = Math.max(c.end, ...ends) + 1;
86
+ return { schema: spec.schema, fps: spec.fps ?? FPS, frames, keys: (spec.keys ?? []).filter(k => k.f < frames), use: [...before, { ...u, args: { ...u.args, ...c.args } }],
87
+ ...(spec.anchors ? { anchors: spec.anchors } : {}), ...(spec.props ? { props: spec.props } : {}), ...(spec.fits ? { fits: spec.fits } : {}), ...(spec.fix !== undefined ? { fix: spec.fix } : {}) };
88
+ };
89
+ let chosen = null, tried = 0, best = null, onTried = 0;
90
+ const order = [...onTime.slice(0, 6), ...onTime.slice(0, 3).flatMap(c => variants(c, d))].slice(0, TRIALS - LATE);
91
+ for (const c of [...order, ...late.slice(0, TRIALS - order.length)]) {
92
+ let issues;
93
+ try { issues = walkIssues(trial(probe(c)), u.f0, c.end); } catch { issues = Infinity; }
94
+ tried++; onTried += c.late ? 0 : 1;
95
+ if (!best || issues < best.issues) best = { c, issues };
96
+ if (issues === 0) { chosen = c; break; }
97
+ }
98
+ chosen ??= best?.issues < Infinity ? best.c : onTime[0] ?? late[0];
99
+ if (!chosen) continue;
100
+ const a = chosen.args, A = u.args.arrive;
101
+ const what = `use[${i}] walkTo arrive f${A}: steps ${a.steps}, period ${a.period}, swing ${a.swing}, step ${a.step} m, firstStep ${a.firstStep} m${a.lift ? `, lift ${a.lift} m` : ""} (${r2(chosen.v)} m/s), ends f${chosen.end}`;
102
+ const why = onTime.length ? `the ${onTried} walks tried that end by f${A} pop or miss a foot`
103
+ : `even the quickest walk over ${r2(d)} m (a step every ${PERIODS[0]} frames, strides up to ${STRIDE_MAX} m, 34 frames to start and stop) ends f${late[0]?.end}`;
104
+ const late1 = chosen.late ? `; f${A} is too soon: ${why}; give arrive >= f${chosen.end} or a shorter distance` : "";
105
+ const dirty = best && best.c === chosen && best.issues > 0 ? `; no candidate walked clean (${best.issues} issues in its frames)` : "";
106
+ const plan = { i, args: a, end: chosen.end, note: `${what}${late1}${dirty} (${tried} checked)` };
107
+ if (cache.size >= 64) cache.delete(cache.keys().next().value);
108
+ cache.set(key, plan);
109
+ plans.push(plan);
110
+ }
111
+ return plans;
112
+ }
113
+
114
+ /** The spec with every planned walk's args filled in (arrive leaves them: walkTo ignores it); the spec itself when there is none. */
115
+ export function applyWalkPlans(spec, plans) {
116
+ if (!plans.length) return spec;
117
+ const use = spec.use.map((u, i) => { const p = plans.find(x => x.i === i); return p && u?.skill === "walkTo" ? { ...u, args: { ...u.args, ...p.args } } : u; });
118
+ return { ...spec, use };
119
+ }
@@ -5,6 +5,8 @@ import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
5
5
  const GROUP_LABELS_KO = {
6
6
  Primitives: "기본 도형",
7
7
  "Set pieces": "세트 소품",
8
+ Lights: "조명",
9
+ Effects: "효과",
8
10
  };
9
11
 
10
12
  const OBJECT_LABELS_KO = {
@@ -18,6 +20,10 @@ const OBJECT_LABELS_KO = {
18
20
  Chair: "의자",
19
21
  Car: "자동차",
20
22
  "Plane (aircraft)": "비행기",
23
+ "Point light": "점 조명",
24
+ "Spot light": "스포트 조명",
25
+ Light: "조명",
26
+ Effect: "효과",
21
27
  };
22
28
 
23
29
  function displayObjectGroupName(name) {
@@ -67,7 +73,7 @@ export function CatalogueEntries({ onPick }) {
67
73
  <span className={`add-object-swatch ${entry.kind}`} style={{ background: entry.color }} aria-hidden="true" />
68
74
  <span>{displayObjectLabel(entry.label)}</span>
69
75
  {/* An empty has no size to quote: say what it is instead of "0 × 0 m". */}
70
- <small>{entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
76
+ <small>{entry.light ? ko("practical light", "실제 조명") : entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : entry.kind === "effect" ? ko("smoke, fire, rain…", "연기, 불, 비…") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
71
77
  </button>
72
78
  ))}
73
79
  </div>
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The inspector's object colours: the blockout palette, what a typed colour
3
+ * means, and the per-browser memory of hand-mixed tints. Pure, so the record
4
+ * normalizers and the light/glow fields read the same hex rules.
5
+ */
6
+
7
+ /** Blockout grey: what a primitive is until someone says otherwise. */
8
+ export const GREY_BOX = "#c2c6c8";
9
+
10
+ /** Blockout greys first, then the clay accents, then the blocking chromatics,
11
+ * for re-tinting from the inspector. The greys lead because a blockout starts
12
+ * grey; the saturated four exist so a shot can say "the red ball, the blue
13
+ * box" at a glance — the inspector had no way to say that at all (#88). They
14
+ * are muted off pure RGB so they still read as clay under the set's lighting.
15
+ * Any other colour is authored through the inspector's free colour input. */
16
+ export const OBJECT_COLORS = [
17
+ "#e2e5e6",
18
+ GREY_BOX,
19
+ "#9aa1a5",
20
+ "#767d81",
21
+ "#d9b18c",
22
+ "#8fae9b",
23
+ "#d94a4a",
24
+ "#4a7bd9",
25
+ "#e2c04a",
26
+ "#4fa86a",
27
+ ];
28
+
29
+ /** How many hand-mixed colours the inspector remembers. Six is one palette
30
+ * row: enough to keep a scene's custom tints reachable, short enough that the
31
+ * popover never turns into a second, unruly palette. */
32
+ const RECENT_OBJECT_COLORS_MAX = 6;
33
+ /** Session memory of hand-mixed tints; versioned like every other key here. */
34
+ const RECENT_OBJECT_COLORS_KEY = "cozyclay.objectColors.recent";
35
+
36
+ const HEX_SHORT = /^#?([0-9a-f]{3})$/i;
37
+ const HEX_LONG = /^#?([0-9a-f]{6})$/i;
38
+
39
+ /**
40
+ * What a colour input typed by a human means, or null if it means nothing.
41
+ *
42
+ * The hex field accepts what people actually type — `#f36`, `ff3366`, upper
43
+ * case, stray spaces — and folds it to the one form the record stores, so a
44
+ * colour typed by hand and the same colour picked from the native swatch are
45
+ * byte-identical (and therefore compare equal against the palette). Anything
46
+ * else returns null rather than throwing or guessing: a half-typed `#12` is a
47
+ * keystroke on the way somewhere, not an instruction to repaint the prop.
48
+ */
49
+ export function normalizeObjectColor(value) {
50
+ if (typeof value !== "string") return null;
51
+ const text = value.trim();
52
+ const short = HEX_SHORT.exec(text);
53
+ if (short) return `#${short[1].toLowerCase().replace(/./g, (digit) => digit + digit)}`;
54
+ const long = HEX_LONG.exec(text);
55
+ return long ? `#${long[1].toLowerCase()}` : null;
56
+ }
57
+
58
+ /**
59
+ * The recent-colours list after mixing `hex`: newest first, no duplicates,
60
+ * capped, presets excluded.
61
+ *
62
+ * Pure so the list can be proved without a browser, and so a corrupt stored
63
+ * list is repaired on the way through instead of painting a swatch with junk.
64
+ * Presets are skipped because they are already one row up — remembering them
65
+ * would push the author's own colours off the end with colours they never
66
+ * mixed. Returns the SAME array when nothing changes, so a re-pick of an
67
+ * unchanged colour cannot trigger a storage write or a re-render.
68
+ */
69
+ export function rememberObjectColor(list, hex) {
70
+ const clean = Array.isArray(list)
71
+ ? list.map(normalizeObjectColor).filter((color, index, all) => color && all.indexOf(color) === index && !OBJECT_COLORS.includes(color))
72
+ : [];
73
+ const color = normalizeObjectColor(hex);
74
+ const next = color && !OBJECT_COLORS.includes(color) ? [color, ...clean.filter((entry) => entry !== color)] : clean;
75
+ const capped = next.slice(0, RECENT_OBJECT_COLORS_MAX);
76
+ const unchanged = Array.isArray(list) && capped.length === list.length && capped.every((entry, index) => entry === list[index]);
77
+ return unchanged ? list : capped;
78
+ }
79
+
80
+ /** Recent colours as last left by this browser; storage can be unavailable
81
+ * (private mode), and a corrupt body must not take the inspector down with
82
+ * it — either way the palette simply starts without a memory. */
83
+ export function readStoredObjectColors(storage) {
84
+ try {
85
+ const parsed = JSON.parse(storage?.getItem(RECENT_OBJECT_COLORS_KEY) ?? "[]");
86
+ return rememberObjectColor(Array.isArray(parsed) ? parsed : [], null);
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+
92
+ export function writeStoredObjectColors(storage, list) {
93
+ try {
94
+ storage?.setItem(RECENT_OBJECT_COLORS_KEY, JSON.stringify(list));
95
+ } catch {
96
+ // Storage can be unavailable (private mode); the colours simply stop persisting.
97
+ }
98
+ }