cozyclay 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +40 -113
- package/bin/agent/agent-runner.mjs +9 -10
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +22 -6
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-C9sUnndd.js +1 -0
- package/dist/assets/app-CCh--1Hc.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-BlT33CGf.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-BSesrCpL.js → landing-DjHD8u7k.js} +1 -1
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-C4yKH1p4.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/llms.txt +14 -0
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +174 -2639
- package/src/agent-progress.js +79 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +5 -0
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +8 -6
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +207 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +49 -11
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +51 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +46 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +59 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +48 -419
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +75 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +105 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +170 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +162 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +143 -0
- package/src/motion3/compiler/validate-parts.js +116 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +73 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +174 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +173 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +121 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +179 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +42 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +218 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +120 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +111 -0
- package/src/motion3/schema/types.js +147 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/skills/basic.js +82 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/index.js +83 -0
- package/src/motion3/skills/info.js +65 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PoseImagePanel.jsx +1 -1
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +11 -53
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +5 -83
- package/src/shell/ViewportToolbar.jsx +23 -93
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +12 -14
- package/src/studio-agent-commands.js +46 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +46 -73
- package/src/studio-app-binding.js +29 -45
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +8 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +107 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/headless-studio.mjs +85 -0
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +33 -83
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +9 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-B41FmPYI.js +0 -1
- package/dist/assets/app-Do-Og5Pu.css +0 -1
- package/dist/assets/app-DwP-tl2x.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-Dt0qZfS7.js +0 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-jqMhqL88.js +0 -1
- package/dist/assets/workflow-Br-K7Srs.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -159
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /package/src/{ardy → motion}/range-pin.js +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Environment files to the half-float equirectangular maps the stage renders
|
|
2
|
+
// (stage-hdr-map.js): a Radiance .hdr through three's HDRLoader (the r180+ name
|
|
3
|
+
// of RGBELoader), an OpenEXR through EXRLoader (loaded on first use), and an LDR
|
|
4
|
+
// .png / .jpg through the browser's own decoder. Every map comes out the same
|
|
5
|
+
// way: RGBA half floats in linear light, rows from straight down (row 0) to
|
|
6
|
+
// straight up, at most HDR_MAX_WIDTH wide (a box filter by a whole factor), plus
|
|
7
|
+
// its horizon colour (the deck fades into it) and its brightest direction (the
|
|
8
|
+
// agent aims hdrRotation with it). No DOM at import, so Node tests decode too.
|
|
9
|
+
import { DataUtils } from "three";
|
|
10
|
+
import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
|
|
11
|
+
|
|
12
|
+
/** Widest map kept: the background's sharpness against the GPU memory of its cube and PMREM. */
|
|
13
|
+
export const HDR_MAX_WIDTH = 2048;
|
|
14
|
+
const fromHalf = DataUtils.fromHalfFloat;
|
|
15
|
+
const toHalf = DataUtils.toHalfFloat;
|
|
16
|
+
|
|
17
|
+
/** RGBA floats (rows bottom-up) as a half-float map. */
|
|
18
|
+
export function halfMapFromFloats(width, height, floats) {
|
|
19
|
+
const data = new Uint16Array(floats.length);
|
|
20
|
+
for (let i = 0; i < floats.length; i++) data[i] = toHalf(floats[i]);
|
|
21
|
+
return { width, height, data };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A decoded image as a map: rows flipped bottom-up when the source runs top-down,
|
|
26
|
+
* and box-filtered by the smallest whole factor that fits maxWidth.
|
|
27
|
+
*/
|
|
28
|
+
export function fitMap({ width, height, data, topDown = false }, maxWidth = HDR_MAX_WIDTH) {
|
|
29
|
+
const factor = Math.max(1, Math.ceil(width / maxWidth));
|
|
30
|
+
if (factor === 1 && !topDown) return { width, height, data };
|
|
31
|
+
const w = Math.floor(width / factor), h = Math.max(1, Math.floor(height / factor)), out = new Uint16Array(w * h * 4);
|
|
32
|
+
const sourceRow = (row) => (topDown ? height - 1 - row : row);
|
|
33
|
+
if (factor === 1) {
|
|
34
|
+
for (let row = 0; row < h; row++) out.set(data.subarray(sourceRow(row) * width * 4, (sourceRow(row) + 1) * width * 4), row * width * 4);
|
|
35
|
+
return { width: w, height: h, data: out };
|
|
36
|
+
}
|
|
37
|
+
const sum = new Float64Array(4), area = factor * factor;
|
|
38
|
+
for (let row = 0; row < h; row++) {
|
|
39
|
+
for (let column = 0; column < w; column++) {
|
|
40
|
+
sum.fill(0);
|
|
41
|
+
for (let dy = 0; dy < factor; dy++) {
|
|
42
|
+
const base = (sourceRow(row * factor + dy) * width + column * factor) * 4;
|
|
43
|
+
for (let i = 0; i < factor * 4; i++) sum[i & 3] += fromHalf(data[base + i]);
|
|
44
|
+
}
|
|
45
|
+
for (let channel = 0; channel < 4; channel++) out[(row * w + column) * 4 + channel] = toHalf(sum[channel] / area);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { width: w, height: h, data: out };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mean linear RGB of the band within 3 degrees of the horizon. */
|
|
52
|
+
export function mapHorizon({ width, height, data }) {
|
|
53
|
+
const band = Math.max(1, Math.round((3 / 180) * height)), first = Math.max(0, Math.floor(height / 2) - band), last = Math.min(height, Math.ceil(height / 2) + band);
|
|
54
|
+
const sum = [0, 0, 0];
|
|
55
|
+
for (let row = first; row < last; row++) for (let i = row * width * 4; i < (row + 1) * width * 4; i += 4) for (let c = 0; c < 3; c++) sum[c] += fromHalf(data[i + c]);
|
|
56
|
+
return sum.map((value) => Number((value / ((last - first) * width)).toFixed(5)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* World direction [x, y, z] of the map's brightest light (y up, azimuth 0 = +x,
|
|
61
|
+
* 90 = +z), rounded: the solid-angle-weighted mean direction of every texel
|
|
62
|
+
* within 10% of the peak, so a flat sun disc or a softbox reads at its centre.
|
|
63
|
+
*/
|
|
64
|
+
export function mapBrightest({ width, height, data }) {
|
|
65
|
+
const luma = new Float32Array(width * height);
|
|
66
|
+
let peak = 0;
|
|
67
|
+
for (let i = 0; i < luma.length; i++) peak = Math.max(peak, (luma[i] = 0.2126 * fromHalf(data[i * 4]) + 0.7152 * fromHalf(data[i * 4 + 1]) + 0.0722 * fromHalf(data[i * 4 + 2])));
|
|
68
|
+
const sum = [0, 0, 0];
|
|
69
|
+
for (let i = 0; i < luma.length; i++) {
|
|
70
|
+
if (luma[i] < 0.9 * peak) continue;
|
|
71
|
+
const az = (((i % width) + 0.5) / width - 0.5) * 2 * Math.PI, el = ((Math.floor(i / width) + 0.5) / height - 0.5) * Math.PI, cos = Math.cos(el);
|
|
72
|
+
sum[0] += cos * cos * Math.cos(az);
|
|
73
|
+
sum[1] += cos * Math.sin(el);
|
|
74
|
+
sum[2] += cos * cos * Math.sin(az);
|
|
75
|
+
}
|
|
76
|
+
const length = Math.hypot(...sum) || 1;
|
|
77
|
+
return sum.map((value) => Number((value / length).toFixed(2)) || 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A map with its analysis, as the map cache stores it. */
|
|
81
|
+
export const analyzedMap = (map) => ({ ...map, horizon: mapHorizon(map), brightest: mapBrightest(map) });
|
|
82
|
+
|
|
83
|
+
/** A Radiance RGBE file (ArrayBuffer) as a fitted map. Throws on a malformed file. */
|
|
84
|
+
export function decodeRadiance(buffer) {
|
|
85
|
+
const { width, height, data } = new HDRLoader().parse(buffer);
|
|
86
|
+
return analyzedMap(fitMap({ width, height, data, topDown: true }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function decodeExr(buffer) {
|
|
90
|
+
const { EXRLoader } = await import("three/examples/jsm/loaders/EXRLoader.js");
|
|
91
|
+
const { width, height, data } = new EXRLoader().parse(buffer);
|
|
92
|
+
return analyzedMap(fitMap({ width, height, data }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SRGB_TO_LINEAR = Array.from({ length: 256 }, (_, i) => { const c = i / 255; return toHalf(c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); });
|
|
96
|
+
/** An LDR equirectangular picture through the browser's decoder, scaled to fit, in linear light. */
|
|
97
|
+
async function decodePicture(buffer, type) {
|
|
98
|
+
const bitmap = await createImageBitmap(new Blob([buffer], { type }));
|
|
99
|
+
try {
|
|
100
|
+
const width = Math.min(HDR_MAX_WIDTH, bitmap.width), height = Math.max(1, Math.round((bitmap.height * width) / bitmap.width));
|
|
101
|
+
const canvas = new OffscreenCanvas(width, height), context = canvas.getContext("2d");
|
|
102
|
+
context.drawImage(bitmap, 0, 0, width, height);
|
|
103
|
+
const pixels = context.getImageData(0, 0, width, height).data, data = new Uint16Array(pixels.length);
|
|
104
|
+
for (let i = 0; i < pixels.length; i++) data[i] = (i & 3) === 3 ? toHalf(1) : SRGB_TO_LINEAR[pixels[i]];
|
|
105
|
+
return analyzedMap(fitMap({ width, height, data, topDown: true }));
|
|
106
|
+
} finally {
|
|
107
|
+
bitmap.close?.();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A stored environment record (stage-hdr.js normalizeHdrAsset) decoded to a map with its analysis. */
|
|
112
|
+
export function decodeEnvironment({ type, bytes }) {
|
|
113
|
+
if (type === "image/vnd.radiance") return Promise.resolve().then(() => decodeRadiance(bytes));
|
|
114
|
+
if (type === "image/x-exr") return decodeExr(bytes);
|
|
115
|
+
return decodePicture(bytes, type);
|
|
116
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The HDR environments the stage can use: the built-ins and every environment
|
|
2
|
+
// file loaded into this browser's asset store. The Light panel lists them and
|
|
3
|
+
// inspect_studio { scope: "catalogue" } answers them as `environments`, so the
|
|
4
|
+
// agent, which cannot upload a file, can pick a map the user already loaded and
|
|
5
|
+
// aim it. Loading a file goes through here too: checked, decoded once, stored
|
|
6
|
+
// under its content address and remembered decoded.
|
|
7
|
+
import { declaredHdrSize, HDR_BUILTINS, HDR_MAX_BYTES, hdrFileProblem, hdrIdForBytes, isHdrAssetId, sniffHdrType } from "./stage-hdr.js";
|
|
8
|
+
import { getAsset, listAssetIds, openAssetDb, putAsset } from "./scene-assets.js";
|
|
9
|
+
import { decodeEnvironment } from "./stage-hdr-image.js";
|
|
10
|
+
import { rememberHdrMap } from "./stage-hdr-map.js";
|
|
11
|
+
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const builtins = HDR_BUILTINS.map(({ id, name, brightest, note }) => ({ id, name, source: "builtin", brightest, note }));
|
|
14
|
+
let files = [], catalogue = Object.freeze([...builtins]);
|
|
15
|
+
|
|
16
|
+
function publish(next) {
|
|
17
|
+
files = next;
|
|
18
|
+
catalogue = Object.freeze([...builtins, ...files.map(({ id, name, width, height, megabytes, brightest }) => ({
|
|
19
|
+
id, name, source: "file", ...(width ? { size: `${width}x${height}` } : {}), megabytes, ...(brightest ? { brightest } : {}),
|
|
20
|
+
}))]);
|
|
21
|
+
for (const listener of listeners) listener();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every environment, built-ins first: { id, name, source: "builtin" | "file", brightest?, note?, size?, megabytes? }. */
|
|
25
|
+
export const hdrEnvironmentCatalogue = () => catalogue;
|
|
26
|
+
export function subscribeHdrLibrary(listener) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => listeners.delete(listener);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Re-read the stored environment files' names and sizes. Without IndexedDB the list is the built-ins. */
|
|
32
|
+
export async function refreshHdrLibrary() {
|
|
33
|
+
let db = null;
|
|
34
|
+
try {
|
|
35
|
+
db = await openAssetDb();
|
|
36
|
+
const ids = (await listAssetIds(db)).filter(isHdrAssetId);
|
|
37
|
+
const records = (await Promise.all(ids.map((id) => getAsset(db, id)))).filter(Boolean);
|
|
38
|
+
publish(records.map(({ id, name, width, height, brightest, bytes }) => ({ id, name: name || id, width, height, brightest, megabytes: Number((bytes.byteLength / 1048576).toFixed(2)) }))
|
|
39
|
+
.sort((a, b) => a.name.localeCompare(b.name)));
|
|
40
|
+
} catch {
|
|
41
|
+
publish([]);
|
|
42
|
+
} finally {
|
|
43
|
+
db?.close?.();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load one environment file the user picked: at most HDR_MAX_BYTES (32 MB) and
|
|
49
|
+
* HDR_MAX_PIXELS, decoded before it is stored so a broken file never reaches
|
|
50
|
+
* the store. Answers { id, name }; throws with a sentence for the user.
|
|
51
|
+
*/
|
|
52
|
+
export async function importHdrFile(file) {
|
|
53
|
+
const early = file.size > HDR_MAX_BYTES ? hdrFileProblem(file.size, null, null) : null;
|
|
54
|
+
if (early) throw new Error(early);
|
|
55
|
+
const buffer = await file.arrayBuffer(), bytes = new Uint8Array(buffer), type = sniffHdrType(bytes), size = declaredHdrSize(type, bytes);
|
|
56
|
+
const problem = hdrFileProblem(bytes.length, type, size);
|
|
57
|
+
if (problem) throw new Error(problem);
|
|
58
|
+
let map;
|
|
59
|
+
try { map = await decodeEnvironment({ type, bytes: buffer }); }
|
|
60
|
+
catch (error) { throw new Error(`that file could not be decoded (${error.message})`, { cause: error }); }
|
|
61
|
+
const id = await hdrIdForBytes(bytes), db = await openAssetDb();
|
|
62
|
+
try {
|
|
63
|
+
await putAsset(db, { id, type, bytes: buffer, name: file.name || id, ...(size ?? { width: map.width, height: map.height }), brightest: map.brightest });
|
|
64
|
+
} finally {
|
|
65
|
+
db.close();
|
|
66
|
+
}
|
|
67
|
+
rememberHdrMap(id, map);
|
|
68
|
+
await refreshHdrLibrary();
|
|
69
|
+
return { id, name: file.name || id };
|
|
70
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The stage's HDR environment maps as textures, decoded once per session and
|
|
2
|
+
// shared by every scene that names them. A built-in map is drawn on first use,
|
|
3
|
+
// synchronously, so a look that names one renders it in the same frame; a
|
|
4
|
+
// loaded file is read from the asset store and decoded off the render path,
|
|
5
|
+
// and the stage keeps its own sky light until it lands (or if it never does).
|
|
6
|
+
// stage-sky.jsx prefilters the texture with PMREMGenerator for the light and
|
|
7
|
+
// draws it as the background.
|
|
8
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
9
|
+
import * as THREE from "three";
|
|
10
|
+
import { isBuiltinHdrId, isHdrAssetId } from "./stage-hdr.js";
|
|
11
|
+
import { BUILTIN_HEIGHT, BUILTIN_WIDTH, builtinEnvironmentPixels } from "./stage-hdr-builtins.js";
|
|
12
|
+
import { analyzedMap, decodeEnvironment, halfMapFromFloats } from "./stage-hdr-image.js";
|
|
13
|
+
import { getAsset, openAssetDb } from "./scene-assets.js";
|
|
14
|
+
|
|
15
|
+
// Decoded files kept besides the built-ins; an older one is disposed (three
|
|
16
|
+
// uploads it again if a scene still draws it).
|
|
17
|
+
const KEPT_FILES = 3;
|
|
18
|
+
// A stored file that fails to load is tried again a few times (5 s apart), then left failed until a reload.
|
|
19
|
+
const RETRIES = 3;
|
|
20
|
+
const entries = new Map(), listeners = new Set(), failures = new Map();
|
|
21
|
+
const notify = () => { for (const listener of listeners) listener(); };
|
|
22
|
+
const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); };
|
|
23
|
+
|
|
24
|
+
function textureOf({ width, height, data }) {
|
|
25
|
+
const texture = new THREE.DataTexture(data, width, height, THREE.RGBAFormat, THREE.HalfFloatType);
|
|
26
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
27
|
+
texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
28
|
+
texture.magFilter = THREE.LinearFilter;
|
|
29
|
+
texture.minFilter = THREE.LinearFilter;
|
|
30
|
+
texture.needsUpdate = true;
|
|
31
|
+
return texture;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function store(id, map) {
|
|
35
|
+
const entry = Object.freeze({ id, width: map.width, height: map.height, horizon: map.horizon, brightest: map.brightest, texture: textureOf(map) });
|
|
36
|
+
entries.set(id, entry);
|
|
37
|
+
const files = [...entries.values()].filter((row) => row.texture && isHdrAssetId(row.id));
|
|
38
|
+
for (const stale of files.slice(0, Math.max(0, files.length - KEPT_FILES))) {
|
|
39
|
+
entries.delete(stale.id);
|
|
40
|
+
stale.texture.dispose();
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What the cache holds for an id: a ready map, a pending or failed load, or null. Draws a built-in on first ask. */
|
|
46
|
+
function snapshot(id) {
|
|
47
|
+
if (!id) return null;
|
|
48
|
+
if (!entries.has(id) && isBuiltinHdrId(id)) store(id, analyzedMap(halfMapFromFloats(BUILTIN_WIDTH, BUILTIN_HEIGHT, builtinEnvironmentPixels(id))));
|
|
49
|
+
return entries.get(id) ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Start reading and decoding a stored file once. A failed load is forgotten after a while so a later ask retries. */
|
|
53
|
+
function request(id) {
|
|
54
|
+
if (!isHdrAssetId(id) || entries.has(id)) return;
|
|
55
|
+
const pending = Object.freeze({ id, pending: true });
|
|
56
|
+
entries.set(id, pending);
|
|
57
|
+
(async () => {
|
|
58
|
+
const db = await openAssetDb();
|
|
59
|
+
try {
|
|
60
|
+
const record = await getAsset(db, id);
|
|
61
|
+
if (!record) throw new Error("it is not in this browser's asset store");
|
|
62
|
+
return await decodeEnvironment(record);
|
|
63
|
+
} finally { db.close(); }
|
|
64
|
+
})().then((map) => {
|
|
65
|
+
if (entries.get(id) === pending) store(id, map);
|
|
66
|
+
}, (error) => {
|
|
67
|
+
console.warn(`[cozyclay] could not load HDR environment ${id}: ${error.message}`);
|
|
68
|
+
if (entries.get(id) !== pending) return;
|
|
69
|
+
const failed = Object.freeze({ id, failed: error.message }), count = (failures.get(id) ?? 0) + 1;
|
|
70
|
+
entries.set(id, failed);
|
|
71
|
+
failures.set(id, count);
|
|
72
|
+
if (count < RETRIES) setTimeout(() => { if (entries.get(id) === failed) { entries.delete(id); notify(); } }, 5000);
|
|
73
|
+
}).finally(notify);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Warm the cache with a map the importer already decoded. */
|
|
77
|
+
export function rememberHdrMap(id, map) {
|
|
78
|
+
failures.delete(id);
|
|
79
|
+
store(id, map);
|
|
80
|
+
notify();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The ready map for an id ({ texture, horizon, brightest, width, height }), or null while it loads or when it cannot. */
|
|
84
|
+
export function readyHdrMap(id) {
|
|
85
|
+
const entry = snapshot(id);
|
|
86
|
+
return entry?.texture ? entry : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** React: the ready map for `id`, loading it when needed; re-renders when it lands. */
|
|
90
|
+
export function useHdrMap(id) {
|
|
91
|
+
const entry = useSyncExternalStore(subscribe, () => snapshot(id));
|
|
92
|
+
useEffect(() => { request(id); }, [id, entry]);
|
|
93
|
+
return entry?.texture ? entry : null;
|
|
94
|
+
}
|
package/src/stage-hdr.js
ADDED
|
@@ -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
|
+
}
|