cozyclay 2.1.1 → 3.0.0

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