cozyclay 2.1.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -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
+ }
@@ -0,0 +1,210 @@
1
+ // Ambient occlusion for the stage look (stage.lighting.ao): contact darkening
2
+ // where a body meets the floor or a prop, and in creases (an armpit, the back of
3
+ // a knee in a squat). Screen space, because the bodies are skinned and move: a
4
+ // baked aoMap cannot follow a skinned mesh, and contact blobs under the feet
5
+ // miss every crease.
6
+ //
7
+ // One hook on the scene (scene.onAfterRender) runs it after each draw of the
8
+ // stage through a perspective camera, so every path gets it with no code of its
9
+ // own: the viewport panes, the shot preview card, capture_frame, contact sheets
10
+ // and video export (stage-capture.jsx renders the same scene into its target).
11
+ // A draw with an override material (the outline prepass, the depth and normal
12
+ // plates) is left alone, and so is the orthographic plan.
13
+ //
14
+ // Per draw, at half resolution past 960 px wide: a depth-only prepass of the
15
+ // opaque set, the occlusion within AO_RADIUS of each pixel from SAMPLES samples
16
+ // (how far each sample's direction rises above the surface the depth holds,
17
+ // weighted toward the near ones and faded with the fog), a depth-aware 5x5
18
+ // blur, then a multiply over the frame just drawn. The prepass reuses the
19
+ // shadow maps the draw already made. On an Apple M4 Pro it adds about 0.4 ms of
20
+ // GPU time to a 1920x1080 export frame.
21
+ import * as THREE from "three";
22
+ import { FullScreenQuad } from "three/examples/jsm/postprocessing/Pass.js";
23
+ import { GIZMO_LAYER, POSER_LAYER } from "./dualview.jsx";
24
+
25
+ const AO_RADIUS = 0.4;
26
+ const SAMPLES = 16;
27
+ // A sample whose direction rises less than this cosine above the normal is the
28
+ // same surface (floor noise at grazing angles), not an occluder.
29
+ const BIAS = 0.15;
30
+ // Occlusion to darkening, tuned by eye on the clay figure (a squat's creases, feet on the deck).
31
+ const GAIN = 3;
32
+ // Size buckets kept alive at once (the main pane, the preview card, the exports).
33
+ const CACHED_SIZES = 4;
34
+
35
+ const VERTEX = "varying vec2 vUv;\nvoid main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }";
36
+ const OCCLUSION = `
37
+ uniform sampler2D tDepth;
38
+ uniform mat4 projection;
39
+ uniform mat4 inverseProjection;
40
+ uniform vec2 size;
41
+ uniform vec2 fog;
42
+ varying vec2 vUv;
43
+ vec3 viewAt(vec2 uv, float depth) {
44
+ vec4 p = inverseProjection * vec4(vec3(uv, depth) * 2.0 - 1.0, 1.0);
45
+ return p.xyz / p.w;
46
+ }
47
+ vec3 viewAt(vec2 uv) { return viewAt(uv, texture2D(tDepth, uv).x); }
48
+ void main() {
49
+ float depth = texture2D(tDepth, vUv).x;
50
+ if (depth >= 1.0) { gl_FragColor = vec4(1.0); return; }
51
+ vec3 p = viewAt(vUv, depth);
52
+ // The normal of the surface the depth buffer holds (each axis from the nearer
53
+ // neighbour, so an edge does not bend it): flat facets of the low-poly clay
54
+ // then occlude nothing of their own, whatever their smoothed shading normal says.
55
+ vec2 texel = 1.0 / size;
56
+ vec3 right = viewAt(vUv + vec2(texel.x, 0.0)) - p, left = p - viewAt(vUv - vec2(texel.x, 0.0));
57
+ vec3 up = viewAt(vUv + vec2(0.0, texel.y)) - p, down = p - viewAt(vUv - vec2(0.0, texel.y));
58
+ vec3 n = normalize(cross(abs(right.z) < abs(left.z) ? right : left, abs(up.z) < abs(down.z) ? up : down));
59
+ float reach = min(${AO_RADIUS.toFixed(2)} * projection[1][1] * 0.5 * size.y / -p.z, size.y * 0.2);
60
+ float fade = 1.0 - smoothstep(fog.x, fog.y, -p.z);
61
+ if (reach < 1.0 || fade <= 0.0) { gl_FragColor = vec4(1.0); return; }
62
+ float spin = 6.2831853 * fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
63
+ // A weighted mean over the samples near the surface (weight falls to 0 at RADIUS),
64
+ // so an occluder close by counts fully however small it is on screen.
65
+ float occlusion = 0.0, weights = 0.25;
66
+ for (int i = 0; i < ${SAMPLES}; i++) {
67
+ float angle = float(i) * 2.39996323 + spin;
68
+ // snapped to a texel centre, so the depth read and the position rebuilt from it agree
69
+ vec2 uv = (floor((vUv + vec2(cos(angle), sin(angle)) * ((float(i) + 0.5) / ${SAMPLES}.0) * reach / size) * size) + 0.5) / size;
70
+ vec3 v = viewAt(uv) - p;
71
+ float distance = length(v), weight = pow(max(0.0, 1.0 - distance / ${AO_RADIUS.toFixed(2)}), 2.0);
72
+ occlusion += max(dot(v, n) / max(distance, 1e-4) - ${BIAS.toFixed(2)}, 0.0) * weight;
73
+ weights += weight;
74
+ }
75
+ float ao = clamp(1.0 - ${GAIN.toFixed(2)} * occlusion / (weights * ${(1 - BIAS).toFixed(2)}), 0.0, 1.0);
76
+ gl_FragColor = vec4(vec3(mix(1.0, ao, fade)), 1.0);
77
+ }`;
78
+ const BLUR = `
79
+ #include <packing>
80
+ uniform sampler2D tOcclusion;
81
+ uniform sampler2D tDepth;
82
+ uniform vec2 size;
83
+ uniform vec2 clip;
84
+ varying vec2 vUv;
85
+ float viewZ(vec2 uv) { return perspectiveDepthToViewZ(texture2D(tDepth, uv).x, clip.x, clip.y); }
86
+ void main() {
87
+ float z = viewZ(vUv), total = 0.0, weights = 0.0;
88
+ for (int x = -2; x <= 2; x++) for (int y = -2; y <= 2; y++) {
89
+ vec2 uv = vUv + vec2(float(x), float(y)) / size;
90
+ float weight = 1.0 - smoothstep(0.02, 0.06, abs(viewZ(uv) - z) / -z);
91
+ total += texture2D(tOcclusion, uv).r * weight;
92
+ weights += weight;
93
+ }
94
+ gl_FragColor = vec4(vec3(total / max(weights, 1e-4)), 1.0);
95
+ }`;
96
+ const COMPOSITE = `
97
+ uniform sampler2D tOcclusion;
98
+ uniform float strength;
99
+ varying vec2 vUv;
100
+ void main() { gl_FragColor = vec4(vec3(mix(1.0, texture2D(tOcclusion, vUv).r, strength)), 1.0); }`;
101
+
102
+ const pass = (fragmentShader, uniforms, extra = {}) => new THREE.ShaderMaterial({ vertexShader: VERTEX, fragmentShader, uniforms, depthTest: false, depthWrite: false, ...extra });
103
+ const target = (width, height, filter, depth = false) => new THREE.WebGLRenderTarget(width, height, {
104
+ minFilter: filter, magFilter: filter, depthBuffer: depth, generateMipmaps: false,
105
+ ...(depth ? { depthTexture: Object.assign(new THREE.DepthTexture(width, height, THREE.UnsignedIntType), { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }) } : {}),
106
+ });
107
+ // What the prepass leaves out: light rather than surface (glow halos, smoke,
108
+ // see-through and faded parts, ghosts), picture cards whose alpha a solid
109
+ // depth pass would ignore, lines and points.
110
+ const seeThrough = (material) => [material].flat().some((m) => m && (m.transparent || m.depthWrite === false));
111
+ const leftOut = (node) => node.userData?.beautyOnly || node.userData?.cutoutTexture !== undefined || node.isSprite || node.isPoints || node.isLine || (node.isMesh && seeThrough(node.material));
112
+
113
+ /**
114
+ * Put the occlusion pass on `scene`. `settings.strength` (0..1) is read at each
115
+ * draw, so a slider changes it without rebuilding anything. Answers a function
116
+ * that takes the hook off and frees the targets.
117
+ */
118
+ export function installOcclusion(scene, settings) {
119
+ // Depth is all the pass reads: the prepass writes no colour at all.
120
+ const depthOnly = new THREE.MeshBasicMaterial({ side: THREE.DoubleSide, colorWrite: false });
121
+ const occlusion = pass(OCCLUSION, { tDepth: { value: null }, projection: { value: new THREE.Matrix4() }, inverseProjection: { value: new THREE.Matrix4() }, size: { value: new THREE.Vector2() }, fog: { value: new THREE.Vector2() } });
122
+ const blur = pass(BLUR, { tOcclusion: { value: null }, tDepth: { value: null }, size: { value: new THREE.Vector2() }, clip: { value: new THREE.Vector2() } });
123
+ const composite = pass(COMPOSITE, { tOcclusion: { value: null }, strength: { value: 1 } }, {
124
+ blending: THREE.CustomBlending, blendEquation: THREE.AddEquation, blendSrc: THREE.ZeroFactor, blendDst: THREE.SrcColorFactor,
125
+ blendSrcAlpha: THREE.ZeroFactor, blendDstAlpha: THREE.OneFactor, toneMapped: false,
126
+ });
127
+ const quad = new FullScreenQuad(), sizes = new Map(), viewport = new THREE.Vector4(), scissor = new THREE.Vector4();
128
+ let running = false;
129
+ const targetsFor = (width, height) => {
130
+ const key = `${width}x${height}`;
131
+ let set = sizes.get(key);
132
+ if (set) sizes.delete(key);
133
+ else set = { prepass: target(width, height, THREE.NearestFilter, true), raw: target(width, height, THREE.NearestFilter), smooth: target(width, height, THREE.LinearFilter) };
134
+ sizes.set(key, set);
135
+ if (sizes.size > CACHED_SIZES) {
136
+ const [oldest, stale] = sizes.entries().next().value;
137
+ sizes.delete(oldest);
138
+ for (const t of Object.values(stale)) { t.depthTexture?.dispose(); t.dispose(); }
139
+ }
140
+ return set;
141
+ };
142
+ scene.onAfterRender = (renderer, _scene, camera) => {
143
+ if (running || scene.overrideMaterial || !camera?.isPerspectiveCamera || !(settings.strength > 0)) return;
144
+ running = true;
145
+ const output = renderer.getRenderTarget(), autoClear = renderer.autoClear, background = scene.background, mask = camera.layers.mask;
146
+ const shadows = renderer.shadowMap, shadowAuto = shadows.autoUpdate, shadowNeeds = shadows.needsUpdate;
147
+ renderer.getViewport(viewport);
148
+ renderer.getScissor(scissor);
149
+ const pixels = renderer.getCurrentViewport(new THREE.Vector4()), half = pixels.z > 960 ? 0.5 : 1;
150
+ const width = Math.max(1, Math.round(pixels.z * half)), height = Math.max(1, Math.round(pixels.w * half));
151
+ const { prepass, raw, smooth } = targetsFor(width, height), hidden = [];
152
+ try {
153
+ scene.traverse((node) => { if (node.visible && leftOut(node)) { hidden.push(node); node.visible = false; } });
154
+ scene.background = null;
155
+ scene.overrideMaterial = depthOnly;
156
+ camera.layers.disable(GIZMO_LAYER);
157
+ camera.layers.disable(POSER_LAYER);
158
+ shadows.autoUpdate = false;
159
+ renderer.autoClear = true;
160
+ renderer.setRenderTarget(prepass);
161
+ renderer.render(scene, camera);
162
+ for (const node of hidden.splice(0)) node.visible = true;
163
+ scene.background = background;
164
+ scene.overrideMaterial = null;
165
+ camera.layers.mask = mask;
166
+ const u = occlusion.uniforms, b = blur.uniforms, fog = scene.fog;
167
+ u.tDepth.value = b.tDepth.value = prepass.depthTexture;
168
+ u.projection.value.copy(camera.projectionMatrix);
169
+ u.inverseProjection.value.copy(camera.projectionMatrixInverse);
170
+ u.size.value.set(width, height);
171
+ u.fog.value.set(fog?.near ?? 1e5, fog?.far ?? 2e5);
172
+ renderer.setRenderTarget(raw);
173
+ quad.material = occlusion;
174
+ quad.render(renderer);
175
+ b.tOcclusion.value = raw.texture;
176
+ b.size.value.set(width, height);
177
+ b.clip.value.set(camera.near, camera.far);
178
+ renderer.setRenderTarget(smooth);
179
+ quad.material = blur;
180
+ quad.render(renderer);
181
+ composite.uniforms.tOcclusion.value = smooth.texture;
182
+ composite.uniforms.strength.value = Math.min(1, settings.strength);
183
+ renderer.setRenderTarget(output);
184
+ // The canvas takes back the pane's own viewport (setRenderTarget(null) floors it, the draw rounded it).
185
+ if (output === null) {
186
+ renderer.setViewport(viewport);
187
+ renderer.setScissor(scissor);
188
+ }
189
+ renderer.autoClear = false;
190
+ quad.material = composite;
191
+ quad.render(renderer);
192
+ } finally {
193
+ for (const node of hidden) node.visible = true;
194
+ if (renderer.getRenderTarget() !== output) renderer.setRenderTarget(output);
195
+ scene.background = background;
196
+ scene.overrideMaterial = null;
197
+ camera.layers.mask = mask;
198
+ shadows.autoUpdate = shadowAuto;
199
+ shadows.needsUpdate = shadowNeeds;
200
+ renderer.autoClear = autoClear;
201
+ running = false;
202
+ }
203
+ };
204
+ return () => {
205
+ delete scene.onAfterRender;
206
+ for (const set of sizes.values()) for (const t of Object.values(set)) { t.depthTexture?.dispose(); t.dispose(); }
207
+ for (const material of [depthOnly, occlusion, blur, composite]) material.dispose();
208
+ quad.dispose();
209
+ };
210
+ }