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,116 @@
1
+ // Environment files to the half-float equirectangular maps the stage renders
2
+ // (stage-hdr-map.js): a Radiance .hdr through three's HDRLoader (the r180+ name
3
+ // of RGBELoader), an OpenEXR through EXRLoader (loaded on first use), and an LDR
4
+ // .png / .jpg through the browser's own decoder. Every map comes out the same
5
+ // way: RGBA half floats in linear light, rows from straight down (row 0) to
6
+ // straight up, at most HDR_MAX_WIDTH wide (a box filter by a whole factor), plus
7
+ // its horizon colour (the deck fades into it) and its brightest direction (the
8
+ // agent aims hdrRotation with it). No DOM at import, so Node tests decode too.
9
+ import { DataUtils } from "three";
10
+ import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
11
+
12
+ /** Widest map kept: the background's sharpness against the GPU memory of its cube and PMREM. */
13
+ export const HDR_MAX_WIDTH = 2048;
14
+ const fromHalf = DataUtils.fromHalfFloat;
15
+ const toHalf = DataUtils.toHalfFloat;
16
+
17
+ /** RGBA floats (rows bottom-up) as a half-float map. */
18
+ export function halfMapFromFloats(width, height, floats) {
19
+ const data = new Uint16Array(floats.length);
20
+ for (let i = 0; i < floats.length; i++) data[i] = toHalf(floats[i]);
21
+ return { width, height, data };
22
+ }
23
+
24
+ /**
25
+ * A decoded image as a map: rows flipped bottom-up when the source runs top-down,
26
+ * and box-filtered by the smallest whole factor that fits maxWidth.
27
+ */
28
+ export function fitMap({ width, height, data, topDown = false }, maxWidth = HDR_MAX_WIDTH) {
29
+ const factor = Math.max(1, Math.ceil(width / maxWidth));
30
+ if (factor === 1 && !topDown) return { width, height, data };
31
+ const w = Math.floor(width / factor), h = Math.max(1, Math.floor(height / factor)), out = new Uint16Array(w * h * 4);
32
+ const sourceRow = (row) => (topDown ? height - 1 - row : row);
33
+ if (factor === 1) {
34
+ for (let row = 0; row < h; row++) out.set(data.subarray(sourceRow(row) * width * 4, (sourceRow(row) + 1) * width * 4), row * width * 4);
35
+ return { width: w, height: h, data: out };
36
+ }
37
+ const sum = new Float64Array(4), area = factor * factor;
38
+ for (let row = 0; row < h; row++) {
39
+ for (let column = 0; column < w; column++) {
40
+ sum.fill(0);
41
+ for (let dy = 0; dy < factor; dy++) {
42
+ const base = (sourceRow(row * factor + dy) * width + column * factor) * 4;
43
+ for (let i = 0; i < factor * 4; i++) sum[i & 3] += fromHalf(data[base + i]);
44
+ }
45
+ for (let channel = 0; channel < 4; channel++) out[(row * w + column) * 4 + channel] = toHalf(sum[channel] / area);
46
+ }
47
+ }
48
+ return { width: w, height: h, data: out };
49
+ }
50
+
51
+ /** Mean linear RGB of the band within 3 degrees of the horizon. */
52
+ export function mapHorizon({ width, height, data }) {
53
+ const band = Math.max(1, Math.round((3 / 180) * height)), first = Math.max(0, Math.floor(height / 2) - band), last = Math.min(height, Math.ceil(height / 2) + band);
54
+ const sum = [0, 0, 0];
55
+ for (let row = first; row < last; row++) for (let i = row * width * 4; i < (row + 1) * width * 4; i += 4) for (let c = 0; c < 3; c++) sum[c] += fromHalf(data[i + c]);
56
+ return sum.map((value) => Number((value / ((last - first) * width)).toFixed(5)));
57
+ }
58
+
59
+ /**
60
+ * World direction [x, y, z] of the map's brightest light (y up, azimuth 0 = +x,
61
+ * 90 = +z), rounded: the solid-angle-weighted mean direction of every texel
62
+ * within 10% of the peak, so a flat sun disc or a softbox reads at its centre.
63
+ */
64
+ export function mapBrightest({ width, height, data }) {
65
+ const luma = new Float32Array(width * height);
66
+ let peak = 0;
67
+ for (let i = 0; i < luma.length; i++) peak = Math.max(peak, (luma[i] = 0.2126 * fromHalf(data[i * 4]) + 0.7152 * fromHalf(data[i * 4 + 1]) + 0.0722 * fromHalf(data[i * 4 + 2])));
68
+ const sum = [0, 0, 0];
69
+ for (let i = 0; i < luma.length; i++) {
70
+ if (luma[i] < 0.9 * peak) continue;
71
+ const az = (((i % width) + 0.5) / width - 0.5) * 2 * Math.PI, el = ((Math.floor(i / width) + 0.5) / height - 0.5) * Math.PI, cos = Math.cos(el);
72
+ sum[0] += cos * cos * Math.cos(az);
73
+ sum[1] += cos * Math.sin(el);
74
+ sum[2] += cos * cos * Math.sin(az);
75
+ }
76
+ const length = Math.hypot(...sum) || 1;
77
+ return sum.map((value) => Number((value / length).toFixed(2)) || 0);
78
+ }
79
+
80
+ /** A map with its analysis, as the map cache stores it. */
81
+ export const analyzedMap = (map) => ({ ...map, horizon: mapHorizon(map), brightest: mapBrightest(map) });
82
+
83
+ /** A Radiance RGBE file (ArrayBuffer) as a fitted map. Throws on a malformed file. */
84
+ export function decodeRadiance(buffer) {
85
+ const { width, height, data } = new HDRLoader().parse(buffer);
86
+ return analyzedMap(fitMap({ width, height, data, topDown: true }));
87
+ }
88
+
89
+ async function decodeExr(buffer) {
90
+ const { EXRLoader } = await import("three/examples/jsm/loaders/EXRLoader.js");
91
+ const { width, height, data } = new EXRLoader().parse(buffer);
92
+ return analyzedMap(fitMap({ width, height, data }));
93
+ }
94
+
95
+ const SRGB_TO_LINEAR = Array.from({ length: 256 }, (_, i) => { const c = i / 255; return toHalf(c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); });
96
+ /** An LDR equirectangular picture through the browser's decoder, scaled to fit, in linear light. */
97
+ async function decodePicture(buffer, type) {
98
+ const bitmap = await createImageBitmap(new Blob([buffer], { type }));
99
+ try {
100
+ const width = Math.min(HDR_MAX_WIDTH, bitmap.width), height = Math.max(1, Math.round((bitmap.height * width) / bitmap.width));
101
+ const canvas = new OffscreenCanvas(width, height), context = canvas.getContext("2d");
102
+ context.drawImage(bitmap, 0, 0, width, height);
103
+ const pixels = context.getImageData(0, 0, width, height).data, data = new Uint16Array(pixels.length);
104
+ for (let i = 0; i < pixels.length; i++) data[i] = (i & 3) === 3 ? toHalf(1) : SRGB_TO_LINEAR[pixels[i]];
105
+ return analyzedMap(fitMap({ width, height, data, topDown: true }));
106
+ } finally {
107
+ bitmap.close?.();
108
+ }
109
+ }
110
+
111
+ /** A stored environment record (stage-hdr.js normalizeHdrAsset) decoded to a map with its analysis. */
112
+ export function decodeEnvironment({ type, bytes }) {
113
+ if (type === "image/vnd.radiance") return Promise.resolve().then(() => decodeRadiance(bytes));
114
+ if (type === "image/x-exr") return decodeExr(bytes);
115
+ return decodePicture(bytes, type);
116
+ }
@@ -0,0 +1,70 @@
1
+ // The HDR environments the stage can use: the built-ins and every environment
2
+ // file loaded into this browser's asset store. The Light panel lists them and
3
+ // inspect_studio { scope: "catalogue" } answers them as `environments`, so the
4
+ // agent, which cannot upload a file, can pick a map the user already loaded and
5
+ // aim it. Loading a file goes through here too: checked, decoded once, stored
6
+ // under its content address and remembered decoded.
7
+ import { declaredHdrSize, HDR_BUILTINS, HDR_MAX_BYTES, hdrFileProblem, hdrIdForBytes, isHdrAssetId, sniffHdrType } from "./stage-hdr.js";
8
+ import { getAsset, listAssetIds, openAssetDb, putAsset } from "./scene-assets.js";
9
+ import { decodeEnvironment } from "./stage-hdr-image.js";
10
+ import { rememberHdrMap } from "./stage-hdr-map.js";
11
+
12
+ const listeners = new Set();
13
+ const builtins = HDR_BUILTINS.map(({ id, name, brightest, note }) => ({ id, name, source: "builtin", brightest, note }));
14
+ let files = [], catalogue = Object.freeze([...builtins]);
15
+
16
+ function publish(next) {
17
+ files = next;
18
+ catalogue = Object.freeze([...builtins, ...files.map(({ id, name, width, height, megabytes, brightest }) => ({
19
+ id, name, source: "file", ...(width ? { size: `${width}x${height}` } : {}), megabytes, ...(brightest ? { brightest } : {}),
20
+ }))]);
21
+ for (const listener of listeners) listener();
22
+ }
23
+
24
+ /** Every environment, built-ins first: { id, name, source: "builtin" | "file", brightest?, note?, size?, megabytes? }. */
25
+ export const hdrEnvironmentCatalogue = () => catalogue;
26
+ export function subscribeHdrLibrary(listener) {
27
+ listeners.add(listener);
28
+ return () => listeners.delete(listener);
29
+ }
30
+
31
+ /** Re-read the stored environment files' names and sizes. Without IndexedDB the list is the built-ins. */
32
+ export async function refreshHdrLibrary() {
33
+ let db = null;
34
+ try {
35
+ db = await openAssetDb();
36
+ const ids = (await listAssetIds(db)).filter(isHdrAssetId);
37
+ const records = (await Promise.all(ids.map((id) => getAsset(db, id)))).filter(Boolean);
38
+ publish(records.map(({ id, name, width, height, brightest, bytes }) => ({ id, name: name || id, width, height, brightest, megabytes: Number((bytes.byteLength / 1048576).toFixed(2)) }))
39
+ .sort((a, b) => a.name.localeCompare(b.name)));
40
+ } catch {
41
+ publish([]);
42
+ } finally {
43
+ db?.close?.();
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Load one environment file the user picked: at most HDR_MAX_BYTES (32 MB) and
49
+ * HDR_MAX_PIXELS, decoded before it is stored so a broken file never reaches
50
+ * the store. Answers { id, name }; throws with a sentence for the user.
51
+ */
52
+ export async function importHdrFile(file) {
53
+ const early = file.size > HDR_MAX_BYTES ? hdrFileProblem(file.size, null, null) : null;
54
+ if (early) throw new Error(early);
55
+ const buffer = await file.arrayBuffer(), bytes = new Uint8Array(buffer), type = sniffHdrType(bytes), size = declaredHdrSize(type, bytes);
56
+ const problem = hdrFileProblem(bytes.length, type, size);
57
+ if (problem) throw new Error(problem);
58
+ let map;
59
+ try { map = await decodeEnvironment({ type, bytes: buffer }); }
60
+ catch (error) { throw new Error(`that file could not be decoded (${error.message})`, { cause: error }); }
61
+ const id = await hdrIdForBytes(bytes), db = await openAssetDb();
62
+ try {
63
+ await putAsset(db, { id, type, bytes: buffer, name: file.name || id, ...(size ?? { width: map.width, height: map.height }), brightest: map.brightest });
64
+ } finally {
65
+ db.close();
66
+ }
67
+ rememberHdrMap(id, map);
68
+ await refreshHdrLibrary();
69
+ return { id, name: file.name || id };
70
+ }
@@ -0,0 +1,94 @@
1
+ // The stage's HDR environment maps as textures, decoded once per session and
2
+ // shared by every scene that names them. A built-in map is drawn on first use,
3
+ // synchronously, so a look that names one renders it in the same frame; a
4
+ // loaded file is read from the asset store and decoded off the render path,
5
+ // and the stage keeps its own sky light until it lands (or if it never does).
6
+ // stage-sky.jsx prefilters the texture with PMREMGenerator for the light and
7
+ // draws it as the background.
8
+ import { useEffect, useSyncExternalStore } from "react";
9
+ import * as THREE from "three";
10
+ import { isBuiltinHdrId, isHdrAssetId } from "./stage-hdr.js";
11
+ import { BUILTIN_HEIGHT, BUILTIN_WIDTH, builtinEnvironmentPixels } from "./stage-hdr-builtins.js";
12
+ import { analyzedMap, decodeEnvironment, halfMapFromFloats } from "./stage-hdr-image.js";
13
+ import { getAsset, openAssetDb } from "./scene-assets.js";
14
+
15
+ // Decoded files kept besides the built-ins; an older one is disposed (three
16
+ // uploads it again if a scene still draws it).
17
+ const KEPT_FILES = 3;
18
+ // A stored file that fails to load is tried again a few times (5 s apart), then left failed until a reload.
19
+ const RETRIES = 3;
20
+ const entries = new Map(), listeners = new Set(), failures = new Map();
21
+ const notify = () => { for (const listener of listeners) listener(); };
22
+ const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); };
23
+
24
+ function textureOf({ width, height, data }) {
25
+ const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.HalfFloatType);
26
+ texture.mapping = THREE.EquirectangularReflectionMapping;
27
+ texture.colorSpace = THREE.LinearSRGBColorSpace;
28
+ texture.magFilter = THREE.LinearFilter;
29
+ texture.minFilter = THREE.LinearFilter;
30
+ texture.needsUpdate = true;
31
+ return texture;
32
+ }
33
+
34
+ function store(id, map) {
35
+ const entry = Object.freeze({ id, width: map.width, height: map.height, horizon: map.horizon, brightest: map.brightest, texture: textureOf(map) });
36
+ entries.set(id, entry);
37
+ const files = [...entries.values()].filter((row) => row.texture && isHdrAssetId(row.id));
38
+ for (const stale of files.slice(0, Math.max(0, files.length - KEPT_FILES))) {
39
+ entries.delete(stale.id);
40
+ stale.texture.dispose();
41
+ }
42
+ return entry;
43
+ }
44
+
45
+ /** What the cache holds for an id: a ready map, a pending or failed load, or null. Draws a built-in on first ask. */
46
+ function snapshot(id) {
47
+ if (!id) return null;
48
+ if (!entries.has(id) && isBuiltinHdrId(id)) store(id, analyzedMap(halfMapFromFloats(BUILTIN_WIDTH, BUILTIN_HEIGHT, builtinEnvironmentPixels(id))));
49
+ return entries.get(id) ?? null;
50
+ }
51
+
52
+ /** Start reading and decoding a stored file once. A failed load is forgotten after a while so a later ask retries. */
53
+ function request(id) {
54
+ if (!isHdrAssetId(id) || entries.has(id)) return;
55
+ const pending = Object.freeze({ id, pending: true });
56
+ entries.set(id, pending);
57
+ (async () => {
58
+ const db = await openAssetDb();
59
+ try {
60
+ const record = await getAsset(db, id);
61
+ if (!record) throw new Error("it is not in this browser's asset store");
62
+ return await decodeEnvironment(record);
63
+ } finally { db.close(); }
64
+ })().then((map) => {
65
+ if (entries.get(id) === pending) store(id, map);
66
+ }, (error) => {
67
+ console.warn(`[cozyclay] could not load HDR environment ${id}: ${error.message}`);
68
+ if (entries.get(id) !== pending) return;
69
+ const failed = Object.freeze({ id, failed: error.message }), count = (failures.get(id) ?? 0) + 1;
70
+ entries.set(id, failed);
71
+ failures.set(id, count);
72
+ if (count < RETRIES) setTimeout(() => { if (entries.get(id) === failed) { entries.delete(id); notify(); } }, 5000);
73
+ }).finally(notify);
74
+ }
75
+
76
+ /** Warm the cache with a map the importer already decoded. */
77
+ export function rememberHdrMap(id, map) {
78
+ failures.delete(id);
79
+ store(id, map);
80
+ notify();
81
+ }
82
+
83
+ /** The ready map for an id ({ texture, horizon, brightest, width, height }), or null while it loads or when it cannot. */
84
+ export function readyHdrMap(id) {
85
+ const entry = snapshot(id);
86
+ return entry?.texture ? entry : null;
87
+ }
88
+
89
+ /** React: the ready map for `id`, loading it when needed; re-renders when it lands. */
90
+ export function useHdrMap(id) {
91
+ const entry = useSyncExternalStore(subscribe, () => snapshot(id));
92
+ useEffect(() => { request(id); }, [id, entry]);
93
+ return entry?.texture ? entry : null;
94
+ }
@@ -0,0 +1,130 @@
1
+ // HDR environments for the stage look: the stage.lighting.hdr* fields, the
2
+ // built-in maps the Studio generates itself, and the records of the .hdr / .exr
3
+ // / equirectangular picture files a user loads. A loaded file lives in the
4
+ // shared asset store (scene-assets.js) under an `hdr-` id derived from its bytes,
5
+ // exactly like a picture or a model, so it travels in a saved project and
6
+ // survives a reload. Pure data and arithmetic, no three.js: the scene document,
7
+ // the tests and the agent sidecar load it.
8
+
9
+ /** Content-addressed prefix of a stored environment file. */
10
+ const HDR_ID_PREFIX = "hdr-";
11
+ const HDR_ID = /^hdr-[0-9a-f]{32}$/;
12
+ /** The file ceiling: the same 32 MB a project file decodes an embedded asset against (scene-assets.js ASSET_MAX_SOURCE_BYTES). */
13
+ export const HDR_MAX_BYTES = 32 * 1024 * 1024;
14
+ /** Decoded size ceiling for float maps (half floats: 8 bytes a pixel while decoding). */
15
+ export const HDR_MAX_PIXELS = 4096 * 4096;
16
+ /** Stored MIME types: Radiance RGBE, OpenEXR, and an LDR equirectangular picture. */
17
+ const HDR_TYPES = Object.freeze(["image/vnd.radiance", "image/x-exr", "image/png", "image/jpeg"]);
18
+
19
+ /**
20
+ * The maps the Studio draws itself (stage-hdr-builtins.js), so the agent has
21
+ * real environments without a file. `brightest` is the direction of the map's
22
+ * brightest light at hdrRotation 0, as a world vector (y up, +z toward the
23
+ * default camera); hdrRotation turns it about +y.
24
+ */
25
+ export const HDR_BUILTINS = Object.freeze([
26
+ { id: "builtin:night-street", name: "Night street", ko: "밤거리", brightest: [0.72, 0.2, 0.66],
27
+ note: "dark blue night sky, a building skyline with lit windows, two warm sodium street lamps (front right and back left) pooling light on wet asphalt, a dim moon" },
28
+ { id: "builtin:overcast-field", name: "Overcast field", ko: "흐린 들판", brightest: [-0.35, 0.66, -0.67],
29
+ note: "bright soft grey overcast sky (brighter overhead), a faint sun glow behind the clouds back left, a distant tree line and a green field" },
30
+ { id: "builtin:studio-softbox", name: "Studio softbox", ko: "스튜디오 소프트박스", brightest: [-0.6, 0.4, 0.7],
31
+ note: "dark grey photo studio: a large white softbox front left, a tall strip light right, a soft overhead panel and a rim panel behind" },
32
+ ].map((entry) => Object.freeze({ ...entry, brightest: Object.freeze(entry.brightest) })));
33
+ const BUILTIN_IDS = new Set(HDR_BUILTINS.map((entry) => entry.id));
34
+
35
+ export const isHdrAssetId = (value) => typeof value === "string" && HDR_ID.test(value);
36
+ export const isBuiltinHdrId = (value) => BUILTIN_IDS.has(value);
37
+ /** What stage.lighting.hdr may hold: a built-in name or a stored file's id. */
38
+ export const isHdrEnvironmentId = (value) => isBuiltinHdrId(value) || isHdrAssetId(value);
39
+
40
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
41
+ /** Degrees into [-180, 180): 180 wraps to -180, like the other yaw fields. */
42
+ export const wrapDegrees = (value) => ((((value + 180) % 360) + 360) % 360) - 180;
43
+
44
+ /** The four HDR fields of stage.lighting, present and in range: no map is the default. */
45
+ export function normalizeHdrFields(source = {}) {
46
+ return {
47
+ hdr: isHdrEnvironmentId(source.hdr) ? source.hdr : null,
48
+ hdrRotation: finite(source.hdrRotation) ? Number(wrapDegrees(source.hdrRotation).toFixed(3)) : 0,
49
+ hdrIntensity: finite(source.hdrIntensity) ? Math.max(0, Math.min(4, source.hdrIntensity)) : 1,
50
+ hdrBackground: typeof source.hdrBackground === "boolean" ? source.hdrBackground : true,
51
+ };
52
+ }
53
+ export const HDR_FIELDS = Object.freeze(Object.keys(normalizeHdrFields()));
54
+
55
+ /* ------------------------------------------------------------ files ---- */
56
+
57
+ const ascii = (bytes, start, length) => String.fromCharCode(...bytes.subarray(start, Math.min(bytes.length, start + length)));
58
+
59
+ /** The stored MIME type of an environment file from its first bytes, or null. */
60
+ export function sniffHdrType(bytes) {
61
+ if (ascii(bytes, 0, 10) === "#?RADIANCE" || ascii(bytes, 0, 6) === "#?RGBE") return "image/vnd.radiance";
62
+ if (bytes[0] === 0x76 && bytes[1] === 0x2f && bytes[2] === 0x31 && bytes[3] === 0x01) return "image/x-exr";
63
+ if (bytes[0] === 0x89 && ascii(bytes, 1, 3) === "PNG") return "image/png";
64
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
65
+ return null;
66
+ }
67
+
68
+ /** Width and height from a Radiance header ("-Y 512 +X 1024"), or null. */
69
+ function radianceSize(bytes) {
70
+ const match = /\n\s*-Y\s+(\d+)\s+\+X\s+(\d+)\s*\n/.exec(ascii(bytes, 0, 8192));
71
+ return match ? { width: Number(match[2]), height: Number(match[1]) } : null;
72
+ }
73
+
74
+ /** Width and height from an OpenEXR header's dataWindow attribute, or null. */
75
+ function exrSize(bytes) {
76
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
77
+ const text = (at) => { let end = at; while (end < bytes.length && bytes[end] !== 0) end++; return [ascii(bytes, at, end - at), end + 1]; };
78
+ let at = 8;
79
+ while (at < Math.min(bytes.length, 65536)) {
80
+ const [name, afterName] = text(at);
81
+ if (!name) return null;
82
+ const [, afterType] = text(afterName);
83
+ const size = view.getInt32(afterType, true), value = afterType + 4;
84
+ if (name === "dataWindow" && size === 16) {
85
+ const [xMin, yMin, xMax, yMax] = [0, 4, 8, 12].map((offset) => view.getInt32(value + offset, true));
86
+ return { width: xMax - xMin + 1, height: yMax - yMin + 1 };
87
+ }
88
+ at = value + size;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /** The pixel size a float map declares before it is decoded (null for a picture). */
94
+ export function declaredHdrSize(type, bytes) {
95
+ try { return type === "image/vnd.radiance" ? radianceSize(bytes) : type === "image/x-exr" ? exrSize(bytes) : null; }
96
+ catch { return null; }
97
+ }
98
+
99
+ /** Why an environment file cannot be loaded, as a sentence for the user, or null when it can. */
100
+ export function hdrFileProblem(byteLength, type, size) {
101
+ if (byteLength > HDR_MAX_BYTES) return `that file is ${(byteLength / 1048576).toFixed(1)} MB; an HDR environment can be at most ${HDR_MAX_BYTES / 1048576} MB`;
102
+ if (!type) return "that file is not an HDR environment CozyClay can load (Radiance .hdr, OpenEXR .exr, or an equirectangular .png / .jpg)";
103
+ if ((type === "image/vnd.radiance" || type === "image/x-exr") && !size) return "that HDR file has no readable image size";
104
+ if (size && size.width * size.height > HDR_MAX_PIXELS) return `that HDR is ${size.width} x ${size.height}; the limit is ${HDR_MAX_PIXELS / 1048576} megapixels (4096 x 4096)`;
105
+ return null;
106
+ }
107
+
108
+ async function sha256Hex(bytes, subtle) {
109
+ if (!subtle?.digest) throw new Error("SubtleCrypto is unavailable — a secure context is required to load an HDR");
110
+ const digest = await subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
111
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
112
+ }
113
+
114
+ /** The id these bytes are stored under: `hdr-` and the first 128 bits of their SHA-256. */
115
+ export async function hdrIdForBytes(bytes, subtle = globalThis.crypto?.subtle) {
116
+ const view = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
117
+ return `${HDR_ID_PREFIX}${(await sha256Hex(view, subtle)).slice(0, 32)}`;
118
+ }
119
+
120
+ const direction = (value) => Array.isArray(value) && value.length === 3 && value.every(finite) ? value.map((v) => Number(v.toFixed(2))) : null;
121
+ /** A stored environment record repaired, or null to drop it (scene-assets.js normalizeAsset). */
122
+ export function normalizeHdrAsset(record, bytes) {
123
+ if (!isHdrAssetId(record?.id) || typeof record.type !== "string" || !HDR_TYPES.includes(record.type.toLowerCase())) return null;
124
+ const width = Math.round(Number(record.width)), height = Math.round(Number(record.height)), brightest = direction(record.brightest);
125
+ return {
126
+ id: record.id, type: record.type.toLowerCase(), bytes, name: typeof record.name === "string" ? record.name : "",
127
+ ...(width > 0 && height > 0 ? { width, height } : {}),
128
+ ...(brightest ? { brightest } : {}),
129
+ };
130
+ }
@@ -0,0 +1,149 @@
1
+ // The stage look: the key light the user grabs plus stage.lighting (sky, fog,
2
+ // ambient, fill, exposure, shadow softness, environment light, ambient
3
+ // occlusion) and the recipes that set them in one action. Pure data and arithmetic — no three.js or React — so the scene
4
+ // document, the command layer, the tests and the agent sidecar can all load it.
5
+ // The renderer reads it through stageLook() (room.jsx, stage-sky.jsx).
6
+ import { elementByPath } from "./studio-elements.js";
7
+ import { GRID_BACKGROUND, GRID_FOG, stageBackground } from "./grid-view.js";
8
+ import { environmentSpec } from "./stage-environment.js";
9
+ import { HDR_FIELDS, normalizeHdrFields } from "./stage-hdr.js";
10
+
11
+ export const LIGHTING_PRESETS = Object.freeze(["studio", "day", "overcast", "golden", "dusk", "night", "interior", "stage"]);
12
+
13
+ const HEX = /^#[0-9a-fA-F]{6}$/;
14
+ const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
15
+ const color = (value, fallback) => (typeof value === "string" && HEX.test(value) ? value : fallback);
16
+ const limits = (path) => elementByPath(path);
17
+ const finite = (value, fallback, { min, max }) => (typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback);
18
+
19
+ // The tuned rig: these values render the stage exactly as it looked before the
20
+ // look was authorable, so a stage saved earlier (no lighting, no key colour)
21
+ // renders unchanged.
22
+ const DEFAULT_KEY_LIGHT = Object.freeze({ x: 6, y: 9, z: 4, intensity: 1.12, warmth: 0.5, color: null });
23
+ // envLight and ao are 0 in the studio look: no environment map, no occlusion pass.
24
+ // No look names an HDR environment (stage-hdr.js); the user or the agent picks one.
25
+ const DEFAULT_LIGHTING = Object.freeze({ preset: "studio", sky: null, skyTop: null, ambient: 1, hemiSky: "#ffffff", hemiGround: "#5a5d64", fill: 1, fog: 0, exposure: 1, shadowSoftness: 0, envLight: 0, ao: 0, ...normalizeHdrFields() });
26
+ const LIGHTING_NUMBERS = ["fill", "fog", "exposure", "shadowSoftness", "envLight", "ao"];
27
+
28
+ /** Clamp a stored key light back into the stage's usable envelope. `color`
29
+ * (a #rrggbb or null) overrides the warmth slider when set. */
30
+ export function createKeyLight(value) {
31
+ const source = plain(value) ? value : {};
32
+ const key = Object.fromEntries(["x", "y", "z", "intensity", "warmth"].map((axis) => [axis, finite(source[axis], DEFAULT_KEY_LIGHT[axis], limits(`stage.keyLight.${axis}`))]));
33
+ return { ...key, color: color(source.color, null) };
34
+ }
35
+
36
+ /** stage.lighting with every field present and in range; anything missing or
37
+ * malformed falls back to the studio look. */
38
+ export function normalizeLighting(value) {
39
+ const source = plain(value) ? value : {};
40
+ const number = (key) => finite(source[key], DEFAULT_LIGHTING[key], limits(`stage.lighting.${key}`));
41
+ return {
42
+ preset: LIGHTING_PRESETS.includes(source.preset) ? source.preset : DEFAULT_LIGHTING.preset,
43
+ sky: color(source.sky, null),
44
+ skyTop: color(source.skyTop, null),
45
+ ambient: number("ambient"),
46
+ hemiSky: color(source.hemiSky, DEFAULT_LIGHTING.hemiSky),
47
+ hemiGround: color(source.hemiGround, DEFAULT_LIGHTING.hemiGround),
48
+ ...Object.fromEntries(LIGHTING_NUMBERS.map((key) => [key, number(key)])),
49
+ ...normalizeHdrFields(source),
50
+ };
51
+ }
52
+
53
+ // Each recipe is a complete look: every lighting field plus the key's colour,
54
+ // brightness and place (the sun, the moon, the lamp). Key positions are metres
55
+ // around the stage centre; a low y is a low sun. Tuned by eye on the clay
56
+ // figure through the shot camera, so treat them as starting points to tweak.
57
+ const recipe = (lighting, keyLight) => Object.freeze({ lighting: Object.freeze({ ...DEFAULT_LIGHTING, ...lighting }), keyLight: Object.freeze(keyLight) });
58
+ export const LIGHTING_RECIPES = Object.freeze({
59
+ studio: recipe({}, { ...DEFAULT_KEY_LIGHT }),
60
+ day: recipe({ sky: "#bcd2e8", skyTop: "#4f86c6", ambient: 1.15, hemiSky: "#c4dcff", hemiGround: "#6b6253", fill: 0.7, fog: 0.12, shadowSoftness: 0.15, envLight: 1, ao: 0.8 },
61
+ { x: 5, y: 14, z: 7, intensity: 2.1, color: "#fff3df" }),
62
+ overcast: recipe({ sky: "#c2c7ce", skyTop: "#8f97a3", ambient: 1.9, hemiSky: "#e3e8ef", hemiGround: "#5c5f64", fill: 1.2, fog: 0.3, exposure: 1.05, shadowSoftness: 1, envLight: 1, ao: 1 },
63
+ { x: 2, y: 14, z: 3, intensity: 0.55, color: "#e8ecf2" }),
64
+ golden: recipe({ sky: "#f0a868", skyTop: "#6a8cc0", ambient: 0.75, hemiSky: "#9fb3d6", hemiGround: "#6e5136", fill: 0.45, fog: 0.3, shadowSoftness: 0.35, envLight: 1, ao: 0.8 },
65
+ { x: 9, y: 3.2, z: 4, intensity: 1.9, color: "#ffad5c" }),
66
+ dusk: recipe({ sky: "#8a6f8c", skyTop: "#18223f", ambient: 0.8, hemiSky: "#5b6c9c", hemiGround: "#29242e", fill: 0.35, fog: 0.4, shadowSoftness: 0.6, envLight: 1, ao: 0.8 },
67
+ { x: -9, y: 1.8, z: -4, intensity: 0.7, color: "#ff9466" }),
68
+ night: recipe({ sky: "#0b1426", skyTop: "#020408", ambient: 0.6, hemiSky: "#34507f", hemiGround: "#0a0c12", fill: 0.25, fog: 0.45, shadowSoftness: 0.75, envLight: 1, ao: 0.8 },
69
+ { x: -6, y: 6.5, z: 0.5, intensity: 0.6, color: "#9db6e6" }),
70
+ interior: recipe({ sky: "#2b2621", ambient: 0.7, hemiSky: "#ffe2bf", hemiGround: "#3d3128", fill: 0.55, fog: 0.35, shadowSoftness: 0.55, envLight: 1, ao: 0.9 },
71
+ { x: 2, y: 4.5, z: 2.5, intensity: 1.35, color: "#ffd29c" }),
72
+ stage: recipe({ sky: "#000000", ambient: 0, hemiSky: "#000000", hemiGround: "#000000", fill: 0, fog: 0.7, shadowSoftness: 0.2, ao: 0.6 },
73
+ { x: 1.5, y: 9, z: 3, intensity: 2.6, color: "#fff0d8" }),
74
+ });
75
+
76
+ /** What stage.applyLighting { keep } may hold back from the recipe. "lighting.hdr"
77
+ * keeps the HDR environment with its rotation, intensity and backdrop switch. */
78
+ export const LIGHTING_KEEP_PATHS = Object.freeze(["keyLight.position", "keyLight.intensity", "keyLight.color",
79
+ ...["sky", "skyTop", "ambient", "hemiSky", "hemiGround", "fill", "fog", "exposure", "shadowSoftness", "envLight", "ao", "hdr"].map((key) => `lighting.${key}`)]);
80
+
81
+ /** The stage's { lighting, keyLight } after one recipe: every field written
82
+ * from it except the `keep` paths, which stay as they are. Studio also resets
83
+ * warmth, so it renders the tuned rig exactly; other recipes set a key colour,
84
+ * which overrides warmth. */
85
+ export function applyLightingRecipe(stage, preset, keep = []) {
86
+ const chosen = LIGHTING_RECIPES[preset];
87
+ if (!chosen) throw new Error(`Unknown lighting preset: ${preset}`);
88
+ const before = { lighting: normalizeLighting(stage?.lighting), keyLight: createKeyLight(stage?.keyLight) };
89
+ const lighting = { ...chosen.lighting, preset };
90
+ for (const path of keep) if (path.startsWith("lighting.")) lighting[path.slice(9)] = before.lighting[path.slice(9)];
91
+ if (keep.includes("lighting.hdr")) for (const key of HDR_FIELDS) lighting[key] = before.lighting[key];
92
+ const keyLight = { ...before.keyLight, ...chosen.keyLight };
93
+ if (keep.includes("keyLight.position")) Object.assign(keyLight, { x: before.keyLight.x, y: before.keyLight.y, z: before.keyLight.z });
94
+ if (keep.includes("keyLight.intensity")) keyLight.intensity = before.keyLight.intensity;
95
+ if (keep.includes("keyLight.color")) Object.assign(keyLight, { color: before.keyLight.color, warmth: before.keyLight.warmth });
96
+ return { lighting: normalizeLighting(lighting), keyLight: createKeyLight(keyLight) };
97
+ }
98
+
99
+ // Linear fog, in metres from the camera. With fog 0 the viewport keeps its
100
+ // tuned 18..54 m horizon fade and exports their 55..95 m one (stage-capture.jsx);
101
+ // haze pulls both in together, reaching 2..14 m at fog 1 (a black-void pool of
102
+ // light under the stage recipe). The squared falloff makes small values a
103
+ // distant haze rather than a jump.
104
+ export function hazeRange(fog) {
105
+ const far = (1 - fog) ** 2;
106
+ return { near: 2 + 53 * far, far: 14 + 81 * far };
107
+ }
108
+
109
+ /**
110
+ * Everything the renderer needs for one look, as plain colours and numbers.
111
+ * A null sky keeps the UI theme's studio stage — its grey, its deck and its
112
+ * ground bounce — so the default renders exactly as before under both themes;
113
+ * the grid view's neutral rig applies only then. An explicit sky makes the look
114
+ * theme-independent: what the shot camera exports is the same for every user,
115
+ * and so does an HDR backdrop (stage.lighting.hdr with hdrBackground).
116
+ */
117
+ export function stageLook({ lighting, keyLight, theme = "dark", grid = false }) {
118
+ const normalized = normalizeLighting(lighting), key = createKeyLight(keyLight);
119
+ // An HDR backdrop is no UI theme's stage either: a null sky under it is the dark stage's grey.
120
+ const l = normalized.sky === null && normalized.hdr && normalized.hdrBackground ? { ...normalized, sky: stageBackground("dark") } : normalized;
121
+ const themed = l.sky === null, neutral = grid && themed, lightTheme = themed && theme === "light";
122
+ const sky = themed ? (grid ? GRID_BACKGROUND : stageBackground(theme)) : l.sky;
123
+ const haze = hazeRange(l.fog), base = neutral ? GRID_FOG : { near: 18, far: 54 };
124
+ const dim = key.intensity / 1.12, lift = Math.min(1, 0.35 + 0.65 * dim);
125
+ // The environment map takes over the hemisphere's share as envLight rises (stage-environment.js).
126
+ const skyLight = 0.9 * lift * l.ambient, hemiShare = Math.max(0, 1 - l.envLight);
127
+ return {
128
+ sky, skyTop: l.skyTop,
129
+ fog: { color: sky, near: Math.min(base.near, haze.near), far: Math.min(base.far, haze.far) },
130
+ // Exports keep their own far horizon until haze asks for a nearer one.
131
+ captureFog: l.fog > 0 ? haze : null,
132
+ hemi: { sky: neutral ? "#ffffff" : l.hemiSky, ground: neutral ? "#3a3d42" : lightTheme ? "#d6d8de" : l.hemiGround, intensity: skyLight * hemiShare },
133
+ // three's IBL irradiance is pi x the map, a hemisphere's is its colour x intensity: the 1/pi matches them.
134
+ env: environmentSpec(l, key, sky), envIntensity: (skyLight * l.envLight) / Math.PI,
135
+ // An HDR environment replaces that map once it has loaded (stage-sky.jsx): its light is
136
+ // the file's radiance x hdrIntensity x envLight, its backdrop the file x hdrIntensity.
137
+ hdr: l.hdr ? { id: l.hdr, rotation: l.hdrRotation, intensity: l.hdrIntensity, background: l.hdrBackground, envIntensity: l.envLight * l.hdrIntensity } : null,
138
+ ao: l.ao,
139
+ ambient: (neutral ? 0.34 : lightTheme ? 0.32 : 0.18) * lift * l.ambient,
140
+ fill: 0.36 * dim * l.fill, rim: 0.22 * dim * l.fill,
141
+ exposure: l.exposure,
142
+ // shadow.radius in shadow-map texels (2048 over 28 m: ~1.4 cm each); 1 is the tuned hard
143
+ // edge. A wider kernel reaches the surface's own depth on curved clay and
144
+ // cross-hatches it, so the normal offset grows with it (metres).
145
+ shadowRadius: 1 + l.shadowSoftness * 7,
146
+ shadowNormalBias: 0.02 + l.shadowSoftness * 0.08,
147
+ lightDeck: lightTheme,
148
+ };
149
+ }