cozyclay 2.1.1 → 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 (577) hide show
  1. package/CHANGELOG.md +60 -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 +3 -3
  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-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
  33. package/dist/assets/{landing-ChUvB_zL.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/sw.js +3 -6
  40. package/dist/workflow/index.html +5 -4
  41. package/mcp/LIVE-PROTOCOL.md +7 -47
  42. package/mcp/README.md +16 -26
  43. package/mcp/live-hub.mjs +6 -10
  44. package/mcp/package.json +1 -3
  45. package/mcp/tool-handlers.mjs +4 -168
  46. package/package.json +3 -22
  47. package/src/App.jsx +150 -2624
  48. package/src/agent-progress.js +79 -0
  49. package/src/agent-turn-mark.js +30 -0
  50. package/src/analytics.js +1 -19
  51. package/src/app-context.js +5 -0
  52. package/src/app-stage.jsx +2 -34
  53. package/src/asset-pane.jsx +6 -5
  54. package/src/camera-block.js +21 -0
  55. package/src/camera-direction.js +120 -0
  56. package/src/character-kind.js +3 -9
  57. package/src/command-bus.js +8 -6
  58. package/src/commands/cast.js +5 -34
  59. package/src/commands/elements/character.js +1 -1
  60. package/src/commands/elements/motion.js +1 -6
  61. package/src/commands/elements/object.js +6 -2
  62. package/src/commands/elements/shot.js +4 -1
  63. package/src/commands/elements.js +38 -14
  64. package/src/commands/index.js +3 -1
  65. package/src/commands/kits.js +37 -0
  66. package/src/commands/motion-timing.js +57 -0
  67. package/src/commands/motion.js +9 -152
  68. package/src/commands/motion3-compile.js +65 -0
  69. package/src/commands/motion3.js +207 -0
  70. package/src/commands/objects.js +6 -4
  71. package/src/commands/shot.js +49 -11
  72. package/src/commands/stage.js +14 -1
  73. package/src/commands/view.js +4 -1
  74. package/src/document-mirrors.js +42 -0
  75. package/src/document-store.js +4 -0
  76. package/src/domains/cast.js +19 -194
  77. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  78. package/src/domains/motion/take-source.js +51 -0
  79. package/src/domains/motion.js +60 -1838
  80. package/src/domains/scenes.js +12 -55
  81. package/src/domains/shots.js +2 -2
  82. package/src/domains/stage.js +4 -2
  83. package/src/draft-ghost.js +46 -0
  84. package/src/draft-ghost.jsx +129 -0
  85. package/src/dualview.jsx +4 -3
  86. package/src/effects/EffectsLayer.jsx +112 -0
  87. package/src/effects/effect-meshes.js +232 -0
  88. package/src/effects/effect-record.js +103 -0
  89. package/src/effects/emitter.js +59 -0
  90. package/src/effects/simulate.js +261 -0
  91. package/src/effects/state.js +59 -0
  92. package/src/execution-telemetry.js +2 -3
  93. package/src/first-success-guide.jsx +5 -5
  94. package/src/ghost-set.js +257 -0
  95. package/src/held-object-edit.jsx +113 -0
  96. package/src/held-object-pose.js +175 -0
  97. package/src/hierarchy-model.js +1 -1
  98. package/src/hierarchy-panel.jsx +1 -1
  99. package/src/imported-mesh.jsx +103 -0
  100. package/src/kits/held.js +84 -0
  101. package/src/kits/index.js +70 -0
  102. package/src/kits/smoking.js +57 -0
  103. package/src/light-blockers.js +93 -0
  104. package/src/motion/beat-board-model.js +84 -0
  105. package/src/motion/beat-board.css +124 -0
  106. package/src/motion/beat-board.jsx +59 -0
  107. package/src/motion/center-of-mass.js +66 -0
  108. package/src/motion/fix-collisions.js +850 -0
  109. package/src/motion/physics-review.js +118 -0
  110. package/src/{ardy → motion}/physics-support.js +1 -76
  111. package/src/motion/rig-snapshot.js +22 -0
  112. package/src/motion/take-segments.jsx +140 -0
  113. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  114. package/src/{ardy → motion}/timeline.css +1 -2
  115. package/src/{ardy → motion}/timeline.jsx +42 -413
  116. package/src/motion/waypoints.js +76 -0
  117. package/src/motion3/beats.js +83 -0
  118. package/src/motion3/body.js +158 -0
  119. package/src/motion3/bones.js +47 -0
  120. package/src/motion3/carried.js +47 -0
  121. package/src/motion3/compile-job.js +75 -0
  122. package/src/motion3/compile-worker.js +10 -0
  123. package/src/motion3/compiler/anchors.js +105 -0
  124. package/src/motion3/compiler/arm-orient.js +128 -0
  125. package/src/motion3/compiler/channels.js +125 -0
  126. package/src/motion3/compiler/compile.js +205 -0
  127. package/src/motion3/compiler/gait-rhythm.js +97 -0
  128. package/src/motion3/compiler/grip.js +171 -0
  129. package/src/motion3/compiler/head-steady.js +48 -0
  130. package/src/motion3/compiler/hold.js +134 -0
  131. package/src/motion3/compiler/idle.js +113 -0
  132. package/src/motion3/compiler/limbs.js +170 -0
  133. package/src/motion3/compiler/math.js +72 -0
  134. package/src/motion3/compiler/pose.js +162 -0
  135. package/src/motion3/compiler/quat.js +49 -0
  136. package/src/motion3/compiler/skeleton.js +74 -0
  137. package/src/motion3/compiler/validate-common.js +32 -0
  138. package/src/motion3/compiler/validate-goals.js +118 -0
  139. package/src/motion3/compiler/validate-objects.js +143 -0
  140. package/src/motion3/compiler/validate-parts.js +116 -0
  141. package/src/motion3/compiler/validate.js +122 -0
  142. package/src/motion3/compiler/vocab.js +73 -0
  143. package/src/motion3/compiler/walk-pelvis.js +275 -0
  144. package/src/motion3/compiler/walk.js +207 -0
  145. package/src/motion3/compiler/wrist-clamp.js +32 -0
  146. package/src/motion3/delta.js +47 -0
  147. package/src/motion3/describe.js +174 -0
  148. package/src/motion3/docs.js +27 -0
  149. package/src/motion3/edit-carry.js +21 -0
  150. package/src/motion3/fix/ease.js +16 -0
  151. package/src/motion3/fix/ground.js +37 -0
  152. package/src/motion3/fix/index.js +52 -0
  153. package/src/motion3/fix/push.js +71 -0
  154. package/src/motion3/fix/reach.js +46 -0
  155. package/src/motion3/fix/retime.js +79 -0
  156. package/src/motion3/fix/self.js +211 -0
  157. package/src/motion3/fix/slide.js +70 -0
  158. package/src/motion3/fix/turn.js +51 -0
  159. package/src/motion3/follow.js +39 -0
  160. package/src/motion3/free-motion.js +173 -0
  161. package/src/motion3/grip-choice.js +130 -0
  162. package/src/motion3/hold-plan.js +157 -0
  163. package/src/motion3/json-patch.js +77 -0
  164. package/src/motion3/key-aim.js +105 -0
  165. package/src/motion3/key-edit.js +82 -0
  166. package/src/motion3/lenient.js +127 -0
  167. package/src/motion3/object-keys.js +236 -0
  168. package/src/motion3/object-pose.js +182 -0
  169. package/src/motion3/object-tracks.js +12 -0
  170. package/src/motion3/pipeline.js +102 -0
  171. package/src/motion3/playback.js +51 -0
  172. package/src/motion3/probes.js +91 -0
  173. package/src/motion3/qa/body-checks.js +34 -0
  174. package/src/motion3/qa/check-names.js +14 -0
  175. package/src/motion3/qa/checks.js +121 -0
  176. package/src/motion3/qa/contact-checks.js +51 -0
  177. package/src/motion3/qa/explain.js +80 -0
  178. package/src/motion3/qa/fmt.js +5 -0
  179. package/src/motion3/qa/foot-checks.js +72 -0
  180. package/src/motion3/qa/geometry.js +77 -0
  181. package/src/motion3/qa/goals.js +191 -0
  182. package/src/motion3/qa/ground.js +60 -0
  183. package/src/motion3/qa/hints-contact.js +179 -0
  184. package/src/motion3/qa/hints-goal.js +160 -0
  185. package/src/motion3/qa/hints-knee.js +24 -0
  186. package/src/motion3/qa/hints-motion.js +209 -0
  187. package/src/motion3/qa/hints-object.js +91 -0
  188. package/src/motion3/qa/index.js +42 -0
  189. package/src/motion3/qa/key-checks.js +82 -0
  190. package/src/motion3/qa/near.js +96 -0
  191. package/src/motion3/qa/object-checks.js +135 -0
  192. package/src/motion3/qa/patch.js +161 -0
  193. package/src/motion3/qa/severity.js +38 -0
  194. package/src/motion3/qa/spec-checks.js +65 -0
  195. package/src/motion3/qa/style.js +218 -0
  196. package/src/motion3/qa/wrist-checks.js +69 -0
  197. package/src/motion3/qa/wrist-obs.js +60 -0
  198. package/src/motion3/refs.js +120 -0
  199. package/src/motion3/repair-tune.js +121 -0
  200. package/src/motion3/repair.js +83 -0
  201. package/src/motion3/report.js +67 -0
  202. package/src/motion3/rigs/index.js +14 -0
  203. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  204. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  205. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  206. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  207. package/src/motion3/scene.js +60 -0
  208. package/src/motion3/schema/brief.js +89 -0
  209. package/src/motion3/schema/fields.js +44 -0
  210. package/src/motion3/schema/spec-examples.js +46 -0
  211. package/src/motion3/schema/topics.js +111 -0
  212. package/src/motion3/schema/types.js +147 -0
  213. package/src/motion3/search.js +117 -0
  214. package/src/motion3/second-hand.js +112 -0
  215. package/src/motion3/skills/basic.js +82 -0
  216. package/src/motion3/skills/book.js +76 -0
  217. package/src/motion3/skills/car.js +145 -0
  218. package/src/motion3/skills/frames-auto.js +29 -0
  219. package/src/motion3/skills/index.js +83 -0
  220. package/src/motion3/skills/info.js +65 -0
  221. package/src/motion3/skills/smoke-kit.js +109 -0
  222. package/src/motion3/skills/smoke.js +160 -0
  223. package/src/motion3/spec-store.js +59 -0
  224. package/src/motion3/take-edit.js +155 -0
  225. package/src/motion3/take.js +63 -0
  226. package/src/motion3/timeline.js +127 -0
  227. package/src/motion3/views.js +121 -0
  228. package/src/motion3/walk-plan.js +119 -0
  229. package/src/object-catalog.jsx +7 -1
  230. package/src/object-colors.js +98 -0
  231. package/src/object-gizmo-parts.js +115 -0
  232. package/src/object-gizmo.jsx +37 -122
  233. package/src/object-glow.js +139 -0
  234. package/src/object-hidden.js +33 -0
  235. package/src/object-look.js +196 -0
  236. package/src/object-size.js +10 -0
  237. package/src/object-travel.js +71 -6
  238. package/src/panels/CameraPanel.jsx +7 -7
  239. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  240. package/src/panels/LightPanel.jsx +25 -4
  241. package/src/panels/ObjectLookPanel.jsx +88 -0
  242. package/src/panels/PosePanel.jsx +2 -17
  243. package/src/panels/RigControlPanel.jsx +5 -53
  244. package/src/panels/SubjectBox.jsx +4 -1
  245. package/src/panels/details.css +14 -0
  246. package/src/posestudio.jsx +4 -27
  247. package/src/project-browser.jsx +3 -4
  248. package/src/project-resources.js +18 -31
  249. package/src/project.js +13 -104
  250. package/src/props.jsx +25 -280
  251. package/src/rail-dot-drag.js +23 -0
  252. package/src/render-passes.js +10 -0
  253. package/src/resource-status.jsx +1 -0
  254. package/src/result-modal.jsx +4 -19
  255. package/src/room.jsx +20 -12
  256. package/src/scene-assets.js +8 -6
  257. package/src/scene-light-place.js +72 -0
  258. package/src/scene-lights.jsx +148 -0
  259. package/src/scene-objects.js +31 -139
  260. package/src/scenes.js +15 -105
  261. package/src/semantic-edit.js +2 -5
  262. package/src/set-pieces.jsx +183 -0
  263. package/src/settings-menu.jsx +3 -11
  264. package/src/shell/BottomDock.jsx +9 -51
  265. package/src/shell/DetailsSlot.jsx +16 -200
  266. package/src/shell/GhostsMenuGroup.jsx +74 -0
  267. package/src/shell/LibrarySlot.jsx +1 -3
  268. package/src/shell/MenuBar.jsx +3 -3
  269. package/src/shell/PreferencesDialog.jsx +15 -264
  270. package/src/shell/PreferencesSlot.jsx +2 -8
  271. package/src/shell/TopBar.jsx +5 -84
  272. package/src/shell/ViewportToolbar.jsx +10 -70
  273. package/src/shell/agent-glass.css +19 -6
  274. package/src/shell/details-selection.js +42 -0
  275. package/src/shell/ghosts-menu.css +22 -0
  276. package/src/shell/glass-regions.css +29 -3
  277. package/src/shell/preferences.css +6 -1
  278. package/src/shot-authoring.js +4 -7
  279. package/src/soft-shadows.js +37 -0
  280. package/src/stage-attach-frames.js +12 -12
  281. package/src/stage-canvas-rigs.jsx +3 -3
  282. package/src/stage-capture.jsx +42 -8
  283. package/src/stage-characters.jsx +22 -216
  284. package/src/stage-editor-gizmos.jsx +6 -6
  285. package/src/stage-environment.js +93 -0
  286. package/src/stage-hdr-builtins.js +107 -0
  287. package/src/stage-hdr-image.js +116 -0
  288. package/src/stage-hdr-library.js +70 -0
  289. package/src/stage-hdr-map.js +94 -0
  290. package/src/stage-hdr.js +130 -0
  291. package/src/stage-lighting.js +149 -0
  292. package/src/stage-occlusion.js +210 -0
  293. package/src/stage-sky.jsx +159 -0
  294. package/src/stage-startup-defaults.js +5 -9
  295. package/src/studio-actions.js +12 -14
  296. package/src/studio-agent-commands.js +19 -16
  297. package/src/studio-agent-motion.js +9 -9
  298. package/src/studio-agent-protocol.js +41 -71
  299. package/src/studio-app-binding.js +25 -45
  300. package/src/studio-asset-list.js +17 -0
  301. package/src/studio-contact-sheet.js +37 -5
  302. package/src/studio-elements.js +27 -7
  303. package/src/studio-entity-bounds.js +18 -0
  304. package/src/studio-error-codes.js +8 -0
  305. package/src/studio-export-framing.js +152 -0
  306. package/src/studio-lighting-check.js +103 -0
  307. package/src/studio-motion-visual.js +150 -0
  308. package/src/studio-schema-errors.js +80 -0
  309. package/src/studio-support-gap.js +74 -0
  310. package/src/styles.css +1 -19
  311. package/src/timeline-defaults.js +5 -0
  312. package/src/timeline-extent.js +8 -20
  313. package/src/tutorial-walk.js +17 -0
  314. package/src/ui-scrub.js +1 -1
  315. package/src/use-shot-camera-tracks.js +12 -0
  316. package/src/view-ghosts.js +97 -0
  317. package/src/view-ghosts.jsx +49 -0
  318. package/src/workflow/AgentPanel.jsx +47 -166
  319. package/src/workflow/AgentProgressCard.jsx +107 -0
  320. package/src/workflow/AgentReplyText.jsx +37 -0
  321. package/src/workflow/AgentTurnSteps.jsx +84 -0
  322. package/src/workflow/LoginPanel.jsx +140 -0
  323. package/src/workflow/WorkflowBuilder.jsx +7 -23
  324. package/src/workflow/agent-client.js +54 -51
  325. package/src/workflow/agent-panel.css +71 -1
  326. package/src/workflow/agent-progress-card.css +167 -0
  327. package/src/workflow/agent-progress-view.js +47 -0
  328. package/src/workflow/agent-turn-steps.css +113 -0
  329. package/src/workflow/agent-turn.js +191 -0
  330. package/src/workflow/login-client.js +34 -0
  331. package/src/workflow/motion-input.js +18 -31
  332. package/src/workflow/node-schema.js +1 -1
  333. package/src/workflow/reply-markdown.js +69 -0
  334. package/src/workflow/scene-asset-sync.js +8 -8
  335. package/tools/agent-session-stats.mjs +61 -0
  336. package/tools/dev-full.mjs +5 -56
  337. package/tools/eslint-baseline.json +12 -13
  338. package/tools/knip-baseline.json +0 -179
  339. package/tools/loc-baseline.json +31 -81
  340. package/tools/motion3/extract-body.mjs +103 -0
  341. package/tools/motion3/extract-rig.mjs +85 -0
  342. package/tools/process-supervisor.mjs +0 -88
  343. package/tools/run-tests.mjs +8 -124
  344. package/bin/agent/motion-runtime.mjs +0 -76
  345. package/dist/ardy/cskel27-rest.json +0 -557
  346. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  347. package/dist/assets/app-B_2XB9rw.css +0 -1
  348. package/dist/assets/app-DAuvKPP8.js +0 -4920
  349. package/dist/assets/config-Bx7pYsD9.js +0 -1
  350. package/dist/assets/config-DeoFTYzb.css +0 -1
  351. package/dist/assets/demo-CvKuBgy5.js +0 -1
  352. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  353. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  354. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  355. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  356. package/dist/demo/walk-then-stop.npz +0 -0
  357. package/mcp/ardy-prompts.mjs +0 -218
  358. package/src/ardy/auto-fix-panel.css +0 -108
  359. package/src/ardy/auto-physics.js +0 -1194
  360. package/src/ardy/client.js +0 -153
  361. package/src/ardy/convert.js +0 -313
  362. package/src/ardy/cskel27-neutral.js +0 -39
  363. package/src/ardy/cskel27.js +0 -77
  364. package/src/ardy/export.js +0 -165
  365. package/src/ardy/fix-collisions.js +0 -2257
  366. package/src/ardy/foot-lock.js +0 -230
  367. package/src/ardy/motion-calibration.js +0 -118
  368. package/src/ardy/motion-edit.js +0 -213
  369. package/src/ardy/motion-url.js +0 -55
  370. package/src/ardy/mp4-duration.js +0 -102
  371. package/src/ardy/npz.js +0 -595
  372. package/src/ardy/physics-panel.css +0 -21
  373. package/src/ardy/physics-panel.jsx +0 -89
  374. package/src/ardy/physics-refine.js +0 -111
  375. package/src/ardy/physics-review.js +0 -567
  376. package/src/ardy/physics-temporal.js +0 -54
  377. package/src/ardy/platform-fit-panel.css +0 -64
  378. package/src/ardy/platform-fit-panel.jsx +0 -41
  379. package/src/ardy/platform-fit.js +0 -326
  380. package/src/ardy/playback.js +0 -711
  381. package/src/ardy/pose-pin.js +0 -97
  382. package/src/ardy/prompt-clips.js +0 -30
  383. package/src/ardy/retime.js +0 -211
  384. package/src/ardy/root-drop.js +0 -369
  385. package/src/ardy/timeline-resize.js +0 -13
  386. package/src/ardy/to-cskel27.js +0 -225
  387. package/src/ardy/trim.js +0 -33
  388. package/src/ardy/waypoints.js +0 -369
  389. package/src/ardy-bridge-contract.js +0 -281
  390. package/src/domains/motion/motion-video-capture.js +0 -664
  391. package/src/i2v-motion-client.js +0 -77
  392. package/src/i2v-motion-studio.jsx +0 -180
  393. package/src/line-edit.js +0 -1348
  394. package/src/motion/generation.js +0 -105
  395. package/src/motion-readiness-ui.jsx +0 -67
  396. package/src/motion-readiness.js +0 -40
  397. package/src/motion-resources.js +0 -177
  398. package/src/motion-store.js +0 -65
  399. package/src/motion-trail.js +0 -601
  400. package/src/multimodel-ingest.js +0 -394
  401. package/src/panels/PromptBlocksPanel.jsx +0 -444
  402. package/src/panels/VideoCapturePanel.jsx +0 -181
  403. package/src/pose-extract/browser.js +0 -42
  404. package/src/pose-extract/detector.js +0 -100
  405. package/src/pose-extract/fit.js +0 -227
  406. package/src/pose-extract/image-frame.js +0 -51
  407. package/src/pose-extract/index.js +0 -58
  408. package/src/pose-extract/keyframes.js +0 -74
  409. package/src/pose-extract/landmarks.js +0 -162
  410. package/src/pose-extract/mirror.js +0 -153
  411. package/src/pose-extract/take.js +0 -131
  412. package/src/pose-extract/video-frames.js +0 -91
  413. package/src/take-recipe.js +0 -165
  414. package/src/trail-key-conflicts.js +0 -39
  415. package/src/trail-pick.js +0 -59
  416. package/src/use-i2v-capture-modal.js +0 -15
  417. package/tools/ardy/BRIDGE.md +0 -37
  418. package/tools/ardy/GVHMR-NPZ.md +0 -68
  419. package/tools/ardy/README.md +0 -124
  420. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  421. package/tools/ardy/artifacts.mjs +0 -34
  422. package/tools/ardy/bridge.mjs +0 -1732
  423. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  424. package/tools/ardy/cclay_constrained_generate.py +0 -561
  425. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  426. package/tools/ardy/cclay_motion_edit.py +0 -510
  427. package/tools/ardy/cclay_sequence_generate.py +0 -705
  428. package/tools/ardy/cclay_worker.py +0 -304
  429. package/tools/ardy/dump-npz.py +0 -205
  430. package/tools/ardy/extract-rest.mjs +0 -299
  431. package/tools/ardy/extract.mjs +0 -507
  432. package/tools/ardy/footage.mjs +0 -467
  433. package/tools/ardy/gvhmr-floor.mjs +0 -102
  434. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  435. package/tools/ardy/gvhmr_fastpath.py +0 -168
  436. package/tools/ardy/gvhmr_trajectory.py +0 -240
  437. package/tools/ardy/merge-text-encoder.py +0 -160
  438. package/tools/ardy/mocap-metrics.mjs +0 -200
  439. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  440. package/tools/ardy/motion-stabilize.mjs +0 -348
  441. package/tools/ardy/npz.mjs +0 -414
  442. package/tools/ardy/pose-to-npz.mjs +0 -106
  443. package/tools/ardy/prompt-limits.mjs +0 -5
  444. package/tools/ardy/run-edit-on-box.sh +0 -73
  445. package/tools/ardy/run-local.mjs +0 -233
  446. package/tools/ardy/run-on-box.sh +0 -593
  447. package/tools/ardy/run-sequence-on-box.sh +0 -177
  448. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  449. package/tools/ardy/runners/index.mjs +0 -18
  450. package/tools/ardy/runners/local.mjs +0 -455
  451. package/tools/ardy/runners/proc.mjs +0 -125
  452. package/tools/ardy/runners/remote.mjs +0 -252
  453. package/tools/ardy/setup-local.mjs +0 -189
  454. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  455. package/tools/ardy/setup-text-encoder.py +0 -306
  456. package/tools/ardy/smpl-cskel27.mjs +0 -178
  457. package/tools/ardy/visual-qa.mjs +0 -196
  458. package/tools/ardy/vq-car.mjs +0 -52
  459. package/tools/bench/EXP3.md +0 -61
  460. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  461. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  462. package/tools/bench/cclay_bench_runner.py +0 -42
  463. package/tools/bench/cube-contact.mjs +0 -162
  464. package/tools/bench/exp3.mjs +0 -152
  465. package/tools/bench/extract-bench-lib.mjs +0 -216
  466. package/tools/bench/extract-bench.mjs +0 -302
  467. package/tools/bench/fal-generate.mjs +0 -88
  468. package/tools/bench/fit/README.md +0 -189
  469. package/tools/bench/fit/camera.mjs +0 -59
  470. package/tools/bench/fit/contact.mjs +0 -419
  471. package/tools/bench/fit/footlock.mjs +0 -234
  472. package/tools/bench/fit/motion.mjs +0 -85
  473. package/tools/bench/fit/pin.mjs +0 -27
  474. package/tools/bench/fit/remote.mjs +0 -105
  475. package/tools/bench/fit-bench.mjs +0 -126
  476. package/tools/bench/fit-sanity.mjs +0 -45
  477. package/tools/bench/metrics.mjs +0 -313
  478. package/tools/bench/obs/depth.mjs +0 -258
  479. package/tools/bench/obs/extrinsics.mjs +0 -175
  480. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  481. package/tools/bench/obs/ground.mjs +0 -294
  482. package/tools/bench/obs/heading.mjs +0 -151
  483. package/tools/bench/obs/ladder.mjs +0 -555
  484. package/tools/bench/obs/mannequin-betas.json +0 -124
  485. package/tools/bench/obs/remote.mjs +0 -83
  486. package/tools/bench/obs/rest_joints.py +0 -52
  487. package/tools/bench/obs/ybot-targets.mjs +0 -49
  488. package/tools/bench/obs-bench.mjs +0 -520
  489. package/tools/bench/score.mjs +0 -343
  490. package/tools/bench/summarize.mjs +0 -86
  491. package/tools/demo-worker/README.md +0 -137
  492. package/tools/demo-worker/api-client.mjs +0 -233
  493. package/tools/demo-worker/generate.mjs +0 -301
  494. package/tools/demo-worker/index.mjs +0 -466
  495. package/tools/demo-worker/sign-core.mjs +0 -112
  496. package/tools/demo-worker/sign.mjs +0 -161
  497. package/tools/gt-render/browser.mjs +0 -182
  498. package/tools/gt-render/camera-math.mjs +0 -276
  499. package/tools/gt-render/page.mjs +0 -267
  500. package/tools/gt-render/render.mjs +0 -514
  501. package/tools/gt-render/scene-box.mjs +0 -31
  502. package/tools/gt-render/take-transform.mjs +0 -87
  503. package/tools/kimodo/compare-seam.mjs +0 -108
  504. package/tools/kimodo/constraints.mjs +0 -244
  505. package/tools/kimodo/edit.mjs +0 -259
  506. package/tools/kimodo/effector-constraints.mjs +0 -200
  507. package/tools/kimodo/generate.mjs +0 -646
  508. package/tools/kimodo/local-output.mjs +0 -265
  509. package/tools/kimodo/measure-pose.mjs +0 -65
  510. package/tools/kimodo/measure-preserve.mjs +0 -899
  511. package/tools/kimodo/measure-waypoints.mjs +0 -55
  512. package/tools/kimodo/mlx-generate.py +0 -111
  513. package/tools/kimodo/pose-constraints.mjs +0 -248
  514. package/tools/kimodo/preserve-mask.mjs +0 -543
  515. package/tools/kimodo/read-npz.mjs +0 -189
  516. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  517. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  518. package/tools/kimodo/runner.mjs +0 -284
  519. package/tools/kimodo/seam-batch.mjs +0 -77
  520. package/tools/kimodo/setup-local.mjs +0 -30
  521. package/tools/kimodo/setup-on-box.sh +0 -339
  522. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  523. package/tools/morphgs/assets/gen_truth.py +0 -39
  524. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  525. package/tools/morphgs/assets/playback-check.mjs +0 -30
  526. package/tools/morphgs/demo-gate.sh +0 -26
  527. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  528. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  529. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  530. package/tools/morphgs/setup-on-cluster.sh +0 -126
  531. package/tools/projflow/driver.py +0 -1069
  532. package/tools/projflow/generate.mjs +0 -805
  533. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  534. package/tools/projflow/line-edit-job.mjs +0 -547
  535. package/tools/projflow/replay.mjs +0 -452
  536. package/tools/projflow/runner.mjs +0 -269
  537. package/tools/projflow/service.mjs +0 -742
  538. package/tools/track/DESIGN.md +0 -513
  539. package/tools/track/backfill-provenance.mjs +0 -116
  540. package/tools/track/budget.mjs +0 -8
  541. package/tools/track/check-rig.mjs +0 -209
  542. package/tools/track/diagnostics.schema.json +0 -60
  543. package/tools/track/export-rig.mjs +0 -256
  544. package/tools/track/fallback.mjs +0 -7
  545. package/tools/track/fk-parity-fixture.mjs +0 -161
  546. package/tools/track/gate.mjs +0 -349
  547. package/tools/track/masks.mjs +0 -318
  548. package/tools/track/metrics.mjs +0 -201
  549. package/tools/track/publish-obs.mjs +0 -108
  550. package/tools/track/py/check_env.py +0 -61
  551. package/tools/track/py/eval_lr.py +0 -229
  552. package/tools/track/py/lr_viterbi.py +0 -294
  553. package/tools/track/py/masks.py +0 -405
  554. package/tools/track/py/objective.py +0 -382
  555. package/tools/track/py/rig.py +0 -508
  556. package/tools/track/py/scene.py +0 -326
  557. package/tools/track/py/test_joint_indices.py +0 -123
  558. package/tools/track/py/test_lr_viterbi.py +0 -195
  559. package/tools/track/py/test_masks.py +0 -229
  560. package/tools/track/py/test_rig.py +0 -288
  561. package/tools/track/py/test_scene.py +0 -354
  562. package/tools/track/py/test_track.py +0 -328
  563. package/tools/track/py/track.py +0 -411
  564. package/tools/track/remote.mjs +0 -140
  565. package/tools/track/rig-dump.mjs +0 -227
  566. package/tools/track/run-box-tests.mjs +0 -14
  567. package/tools/track/run-box.mjs +0 -168
  568. package/tools/track/setup-box.sh +0 -59
  569. package/tools/track/study-2d.mjs +0 -728
  570. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  571. /package/src/{ardy → motion}/ground.js +0 -0
  572. /package/src/{ardy → motion}/ik-drag.js +0 -0
  573. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  574. /package/src/{ardy → motion}/ik.js +0 -0
  575. /package/src/{ardy → motion}/physics-surface.js +0 -0
  576. /package/src/{ardy → motion}/playback-clock.js +0 -0
  577. /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
+ }