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.
- package/CHANGELOG.md +60 -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 +3 -3
- 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-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.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/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 +150 -2624
- 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 +42 -413
- 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/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 +9 -51
- 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/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- 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/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 +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +41 -71
- package/src/studio-app-binding.js +25 -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/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- 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 +8 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.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 -182
- 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
package/src/stage-characters.jsx
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { memo, useEffect, useMemo, useRef
|
|
1
|
+
import { memo, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useFBX } from "@react-three/drei";
|
|
2
3
|
import { useThree } from "@react-three/fiber";
|
|
3
|
-
import { Line, useFBX } from "@react-three/drei";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { SkeletonUtils } from "three/examples/jsm/Addons.js";
|
|
6
6
|
import { ProxyFigure } from "./proxy-figure.jsx";
|
|
7
7
|
import { isProxyFigure } from "./scenes.js";
|
|
8
8
|
import { applyPartColours } from "./part-colours.js";
|
|
9
9
|
import { applyHipsOffset, applyPose, primeBindPose } from "./poses.js";
|
|
10
|
-
import { TRAIL_TRACKS, falloffWeight, jointTrailPoints, trailEditRange } from "./motion-trail.js";
|
|
11
|
-
import { pickTrailPoint } from "./trail-pick.js";
|
|
12
10
|
import { REST_BONES } from "./stage-startup-defaults.js";
|
|
13
11
|
|
|
14
12
|
// Memoized: unchanged cast members skip re-rendering on every playhead tick.
|
|
@@ -28,19 +26,11 @@ const RiggedCharacter = memo(function RiggedCharacter({ url, position, rot, tint
|
|
|
28
26
|
// inside rig space: playback.js derives prep.scale from the bind pose,
|
|
29
27
|
// so a scaled bone hierarchy would be measured as a different rig.
|
|
30
28
|
clone.scale.setScalar(0.01 * scale);
|
|
31
|
-
// The joint shells (Alpha_Joints / Beta_Joints — elbows, knees, the
|
|
32
|
-
// exoskeleton bands) render in a darkened shade of the body tint so
|
|
33
|
-
// the segments read as separate pieces.
|
|
34
|
-
// The shipped clay is intentionally pale, but the stage deck is pale too.
|
|
35
|
-
// Lift neither with emissive nor a second light; a small value drop keeps
|
|
36
|
-
// the same authored hue while restoring silhouette contrast in the editor.
|
|
37
|
-
const bodyTint = new THREE.Color(tint).multiplyScalar(0.9);
|
|
38
|
-
const jointTint = bodyTint.clone().multiplyScalar(0.62);
|
|
39
29
|
clone.traverse((child) => {
|
|
40
30
|
if (child.isMesh) {
|
|
41
|
-
// warm clay reads as a maquette; cold grey reads as a broken render
|
|
31
|
+
// warm clay reads as a maquette; cold grey reads as a broken render.
|
|
32
|
+
// Its colour is the tint, set below in place.
|
|
42
33
|
child.material = new THREE.MeshStandardMaterial({
|
|
43
|
-
color: /_Joints$/i.test(child.name) ? jointTint : bodyTint,
|
|
44
34
|
// A slightly tighter highlight gives the clay enough form to read
|
|
45
35
|
// at blocking distance without turning it into shiny plastic.
|
|
46
36
|
roughness: 0.66,
|
|
@@ -59,7 +49,24 @@ const RiggedCharacter = memo(function RiggedCharacter({ url, position, rot, tint
|
|
|
59
49
|
// below runs immediately after and would otherwise be baked into "rest".
|
|
60
50
|
primeBindPose(clone);
|
|
61
51
|
return clone;
|
|
62
|
-
}, [fbx,
|
|
52
|
+
}, [fbx, partColoursEnabled, partColoursMode]);
|
|
53
|
+
|
|
54
|
+
// The tint recolours the clay in place, before the next frame. It must not rebuild the clone: a colour
|
|
55
|
+
// picker streams a value every drag frame, and a new clone re-binds the rig (playback, IK, contact radii).
|
|
56
|
+
// The joint shells (Alpha_Joints / Beta_Joints — elbows, knees, the exoskeleton bands) take a darkened
|
|
57
|
+
// shade of the body tint so the segments read as separate pieces. The shipped clay is intentionally pale,
|
|
58
|
+
// but the stage deck is pale too: a small value drop keeps the authored hue and restores silhouette
|
|
59
|
+
// contrast in the editor. Part colours paint the vertices themselves and keep their white material.
|
|
60
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
if (partColoursEnabled) return;
|
|
63
|
+
const bodyTint = new THREE.Color(tint).multiplyScalar(0.9);
|
|
64
|
+
const jointTint = bodyTint.clone().multiplyScalar(0.62);
|
|
65
|
+
model.traverse((child) => {
|
|
66
|
+
if (child.isMesh) child.material.color.copy(/_Joints$/i.test(child.name) ? jointTint : bodyTint);
|
|
67
|
+
});
|
|
68
|
+
invalidate();
|
|
69
|
+
}, [model, tint, partColoursEnabled, invalidate]);
|
|
63
70
|
|
|
64
71
|
// A new stature (a fresh extraction on this character) must not rebuild the
|
|
65
72
|
// clone — that would drop the rig the playback effects hold. Only the world
|
|
@@ -99,204 +106,3 @@ const RiggedCharacter = memo(function RiggedCharacter({ url, position, rot, tint
|
|
|
99
106
|
</group>
|
|
100
107
|
);
|
|
101
108
|
});
|
|
102
|
-
|
|
103
|
-
/* ------------------------- IK-mode motion trails --------------------------
|
|
104
|
-
* World-space trajectory polylines of the loaded take: the selected track,
|
|
105
|
-
* or all tracks when the user asks for an overview. The whole clip is
|
|
106
|
-
* drawn as a faint line; while a grab is active the falloff window is
|
|
107
|
-
* re-drawn on top as a bright highlight. Grabbing any point of a line starts
|
|
108
|
-
* a drag on a camera-facing plane through the grab point; the caller deforms
|
|
109
|
-
* the take (motion-trail.js falloff math) so the preview updates live. */
|
|
110
|
-
export const MotionTrails = memo(function MotionTrails({ motion, rig = null, baseY, charScale, ikFocus, activeTrackId = null, falloffFrames, playheadFrame, pendingEdit, enabled, visible = true, onDragStart, onDragPreview, onDragEnd }) {
|
|
111
|
-
const { camera, gl, invalidate } = useThree();
|
|
112
|
-
const [drag, setDrag] = useState(null);
|
|
113
|
-
const callbacksRef = useRef({ onDragStart, onDragPreview, onDragEnd });
|
|
114
|
-
callbacksRef.current = { onDragStart, onDragPreview, onDragEnd };
|
|
115
|
-
const toTriples = (flat) => {
|
|
116
|
-
const out = [];
|
|
117
|
-
for (let index = 0; index + 2 < flat.length; index += 3) out.push([flat[index], flat[index + 1], flat[index + 2]]);
|
|
118
|
-
return out;
|
|
119
|
-
};
|
|
120
|
-
// Every trail track (root + IK endpoints + head) in its handle colour,
|
|
121
|
-
// sampled where playback renders each bone on this rig (not the clip's
|
|
122
|
-
// own joints, which sit 4-15 cm off the body), so draw and pick agree.
|
|
123
|
-
const tracks = useMemo(
|
|
124
|
-
() => TRAIL_TRACKS.map((track) => {
|
|
125
|
-
const flat = jointTrailPoints(motion, track.joint, { baseY, scale: charScale, rig });
|
|
126
|
-
return flat ? { ...track, flat, points: toTriples(flat) } : null;
|
|
127
|
-
}).filter(Boolean),
|
|
128
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
-
[motion, rig, baseY, charScale],
|
|
130
|
-
);
|
|
131
|
-
const trackById = (id) => tracks.find((track) => track.id === id) ?? null;
|
|
132
|
-
const focusTrack = activeTrackId ? tracks.find((track) => track.id === activeTrackId)?.id ?? null : null;
|
|
133
|
-
const visibleTracks = focusTrack ? tracks.filter((track) => track.id === focusTrack) : tracks;
|
|
134
|
-
// The falloff window rides whichever line is being (or was last) grabbed.
|
|
135
|
-
const highlightCandidate = drag ?? pendingEdit;
|
|
136
|
-
const highlight = highlightCandidate && (!focusTrack || highlightCandidate.track === focusTrack) ? highlightCandidate : null;
|
|
137
|
-
const highlightPoints = useMemo(() => {
|
|
138
|
-
if (!highlight || !motion?.frames) return null;
|
|
139
|
-
const flat = trackById(highlight.track)?.flat ?? trackById("hips")?.flat;
|
|
140
|
-
if (!flat) return null;
|
|
141
|
-
const { startFrame, endFrame } = trailEditRange(motion.frames, highlight.grabFrame, falloffFrames);
|
|
142
|
-
const out = [];
|
|
143
|
-
for (let frame = startFrame; frame < endFrame; frame += 1) {
|
|
144
|
-
out.push([flat[frame * 3], flat[frame * 3 + 1], flat[frame * 3 + 2]]);
|
|
145
|
-
}
|
|
146
|
-
return out.length > 1 ? out : null;
|
|
147
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
-
}, [highlight, motion, tracks, falloffFrames]);
|
|
149
|
-
|
|
150
|
-
// Manual capture-phase picking instead of r3f pointer handlers: handlers on
|
|
151
|
-
// a drei Line make react-three-fiber raycast the fat-line shader geometry on
|
|
152
|
-
// EVERY pointermove for hover bookkeeping, which is exactly the IK-mode lag.
|
|
153
|
-
// A single pointerdown listener that measures point-to-ray distance against
|
|
154
|
-
// the cached trail arrays costs nothing while the mouse merely moves.
|
|
155
|
-
const pickRef = useRef(null);
|
|
156
|
-
pickRef.current = { tracks: visibleTracks, enabled, falloffFrames, playheadFrame };
|
|
157
|
-
// Line2 instances for in-place geometry rewrites during a drag.
|
|
158
|
-
const lineRefs = useRef({});
|
|
159
|
-
const highlightRef = useRef(null);
|
|
160
|
-
useEffect(() => {
|
|
161
|
-
const dom = gl.domElement;
|
|
162
|
-
const raycaster = new THREE.Raycaster();
|
|
163
|
-
const hit = new THREE.Vector3();
|
|
164
|
-
const down = (event) => {
|
|
165
|
-
const pick = pickRef.current;
|
|
166
|
-
if (!pick?.enabled || event.button !== 0) return;
|
|
167
|
-
const rect = dom.getBoundingClientRect();
|
|
168
|
-
const ndc = new THREE.Vector2(
|
|
169
|
-
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
|
170
|
-
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
|
171
|
-
);
|
|
172
|
-
raycaster.setFromCamera(ndc, camera);
|
|
173
|
-
const { origin, direction } = raycaster.ray;
|
|
174
|
-
const picked = pickTrailPoint({
|
|
175
|
-
tracks: pick.tracks,
|
|
176
|
-
playheadFrame: pick.playheadFrame,
|
|
177
|
-
falloffFrames: pick.falloffFrames,
|
|
178
|
-
rayOrigin: origin,
|
|
179
|
-
rayDirection: direction,
|
|
180
|
-
maxDistance: 0.2,
|
|
181
|
-
});
|
|
182
|
-
if (!picked) return;
|
|
183
|
-
const { track, grabFrame } = picked;
|
|
184
|
-
// The grab wins over the camera controls listening in the bubble phase.
|
|
185
|
-
event.stopPropagation();
|
|
186
|
-
event.preventDefault();
|
|
187
|
-
const flat = pick.tracks.find((candidate) => candidate.id === track)?.flat;
|
|
188
|
-
if (!flat) return;
|
|
189
|
-
const start = new THREE.Vector3(flat[grabFrame * 3], flat[grabFrame * 3 + 1], flat[grabFrame * 3 + 2]);
|
|
190
|
-
const normal = camera.getWorldDirection(new THREE.Vector3()).negate();
|
|
191
|
-
const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, start);
|
|
192
|
-
setDrag({ track, grabFrame });
|
|
193
|
-
callbacksRef.current.onDragStart?.({ track, grabFrame });
|
|
194
|
-
// The drag never touches React state: per rAF the grabbed line and the
|
|
195
|
-
// falloff highlight are rewritten in place on their Line2 geometries,
|
|
196
|
-
// and onDragPreview poses the rig imperatively. Re-rendering the whole
|
|
197
|
-
// app per pointermove is exactly the drag lag this avoids; the real
|
|
198
|
-
// commit (setMotion + pending edit) happens once, on pointerup.
|
|
199
|
-
const frames = flat.length / 3;
|
|
200
|
-
const { startFrame, endFrame } = trailEditRange(frames, grabFrame, pickRef.current.falloffFrames);
|
|
201
|
-
const deformedFlat = new Float32Array(flat);
|
|
202
|
-
const windowFlat = new Float32Array((endFrame - startFrame) * 3);
|
|
203
|
-
let lastDelta = null;
|
|
204
|
-
let queued = null;
|
|
205
|
-
let rafId = 0;
|
|
206
|
-
const applyPreview = (delta) => {
|
|
207
|
-
for (let frame = startFrame; frame < endFrame; frame += 1) {
|
|
208
|
-
const weight = falloffWeight(frame - grabFrame, pickRef.current.falloffFrames);
|
|
209
|
-
deformedFlat[frame * 3] = flat[frame * 3] + delta.x * weight;
|
|
210
|
-
deformedFlat[frame * 3 + 1] = flat[frame * 3 + 1] + delta.y * weight;
|
|
211
|
-
deformedFlat[frame * 3 + 2] = flat[frame * 3 + 2] + delta.z * weight;
|
|
212
|
-
}
|
|
213
|
-
windowFlat.set(deformedFlat.subarray(startFrame * 3, endFrame * 3));
|
|
214
|
-
lineRefs.current[track]?.geometry?.setPositions(deformedFlat);
|
|
215
|
-
highlightRef.current?.geometry?.setPositions(windowFlat);
|
|
216
|
-
callbacksRef.current.onDragPreview?.({ track, grabFrame, delta });
|
|
217
|
-
invalidate();
|
|
218
|
-
};
|
|
219
|
-
const flush = () => {
|
|
220
|
-
rafId = 0;
|
|
221
|
-
if (queued) applyPreview(queued);
|
|
222
|
-
queued = null;
|
|
223
|
-
};
|
|
224
|
-
const move = (pointerEvent) => {
|
|
225
|
-
const moveRect = dom.getBoundingClientRect();
|
|
226
|
-
const moveNdc = new THREE.Vector2(
|
|
227
|
-
((pointerEvent.clientX - moveRect.left) / moveRect.width) * 2 - 1,
|
|
228
|
-
-((pointerEvent.clientY - moveRect.top) / moveRect.height) * 2 + 1,
|
|
229
|
-
);
|
|
230
|
-
raycaster.setFromCamera(moveNdc, camera);
|
|
231
|
-
if (!raycaster.ray.intersectPlane(plane, hit)) return;
|
|
232
|
-
lastDelta = { x: hit.x - start.x, y: hit.y - start.y, z: hit.z - start.z };
|
|
233
|
-
queued = lastDelta;
|
|
234
|
-
if (!rafId) rafId = requestAnimationFrame(flush);
|
|
235
|
-
};
|
|
236
|
-
const up = () => {
|
|
237
|
-
window.removeEventListener("pointermove", move);
|
|
238
|
-
window.removeEventListener("pointerup", up);
|
|
239
|
-
if (rafId) cancelAnimationFrame(rafId);
|
|
240
|
-
if (queued) applyPreview(queued);
|
|
241
|
-
setDrag(null);
|
|
242
|
-
callbacksRef.current.onDragEnd?.({ track, grabFrame, delta: lastDelta });
|
|
243
|
-
};
|
|
244
|
-
window.addEventListener("pointermove", move);
|
|
245
|
-
window.addEventListener("pointerup", up);
|
|
246
|
-
};
|
|
247
|
-
dom.addEventListener("pointerdown", down, true);
|
|
248
|
-
return () => dom.removeEventListener("pointerdown", down, true);
|
|
249
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
250
|
-
}, [gl, camera]);
|
|
251
|
-
|
|
252
|
-
if (!tracks.length) return null;
|
|
253
|
-
// depthTest off + high renderOrder: the trails read through the character
|
|
254
|
-
// and the floor instead of vanishing into them. Every part rides its own
|
|
255
|
-
// IK-handle colour; the focused part draws thicker.
|
|
256
|
-
return (
|
|
257
|
-
<group
|
|
258
|
-
visible={visible}
|
|
259
|
-
renderOrder={900}
|
|
260
|
-
userData={{ editorOverlay: true }}
|
|
261
|
-
ref={(group) => {
|
|
262
|
-
// QA-only escape hatch (same spirit as window.__cozyclay): lets
|
|
263
|
-
// headless perf probes toggle the trails without a rebuild.
|
|
264
|
-
if (typeof window !== "undefined") {
|
|
265
|
-
window.__cozyclayTrails = group;
|
|
266
|
-
window.__cozyclayVisibleTrailTracks = visibleTracks.map((track) => track.id);
|
|
267
|
-
}
|
|
268
|
-
}}
|
|
269
|
-
>
|
|
270
|
-
{visibleTracks.map((track) => track.points.length > 1 && (
|
|
271
|
-
<Line
|
|
272
|
-
key={track.id}
|
|
273
|
-
ref={(line) => {
|
|
274
|
-
if (line) lineRefs.current[track.id] = line;
|
|
275
|
-
else delete lineRefs.current[track.id];
|
|
276
|
-
}}
|
|
277
|
-
points={track.points}
|
|
278
|
-
color={track.color}
|
|
279
|
-
lineWidth={track.id === focusTrack ? 4 : 2.5}
|
|
280
|
-
depthTest={false}
|
|
281
|
-
transparent
|
|
282
|
-
opacity={track.id === focusTrack ? 1 : 0.85}
|
|
283
|
-
/>
|
|
284
|
-
))}
|
|
285
|
-
{highlightPoints && (
|
|
286
|
-
<Line ref={highlightRef} points={highlightPoints} color={drag ? "#3dff7a" : "#ff8c42"} lineWidth={5} depthTest={false} />
|
|
287
|
-
)}
|
|
288
|
-
</group>
|
|
289
|
-
);
|
|
290
|
-
}, (previous, next) => (
|
|
291
|
-
previous.motion === next.motion &&
|
|
292
|
-
previous.rig === next.rig &&
|
|
293
|
-
previous.baseY === next.baseY &&
|
|
294
|
-
previous.charScale === next.charScale &&
|
|
295
|
-
previous.ikFocus === next.ikFocus &&
|
|
296
|
-
previous.activeTrackId === next.activeTrackId &&
|
|
297
|
-
previous.falloffFrames === next.falloffFrames &&
|
|
298
|
-
previous.playheadFrame === next.playheadFrame &&
|
|
299
|
-
previous.pendingEdit === next.pendingEdit &&
|
|
300
|
-
previous.enabled === next.enabled &&
|
|
301
|
-
previous.visible === next.visible
|
|
302
|
-
));
|
|
@@ -4,6 +4,7 @@ import { Line, Text } from "@react-three/drei";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { GIZMO_LAYER } from "./dualview.jsx";
|
|
6
6
|
import { craneHeightAt, prepareRailBend, railPoint } from "./camera-follow.js";
|
|
7
|
+
import { floorDragStart } from "./rail-dot-drag.js";
|
|
7
8
|
import { insertPathMark, MARK_GAP, MAX_PATH_LEAN, MAX_PATH_MARKS, MAX_PATH_POINTS, nearestPathFraction, pathCurve, pathMarkFractions, pathMarks, pathPointAtFraction } from "./object-path.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -261,8 +262,7 @@ export function CameraRailScenePreview({ points, cumLen, length, crane }) {
|
|
|
261
262
|
* The crane's height marks as grabbable dots on the lifted curve. Click a
|
|
262
263
|
* dot to select it; the selected dot grows a 3-axis gizmo — Y sets the
|
|
263
264
|
* mark's height, X/Z bend the camera path itself by moving the drawn rail
|
|
264
|
-
* control point nearest the mark. A free drag
|
|
265
|
-
* camera-facing vertical plane (height, plus arc slide for interior marks),
|
|
265
|
+
* control point nearest the mark. A free drag moves the dot over the floor, bending the rail (rail-dot-drag.js);
|
|
266
266
|
* Shift-drag slides along the arc at a fixed height, double-click the curve
|
|
267
267
|
* adds a mark and Delete removes a non-endpoint. Dots live on GIZMO_LAYER, so the
|
|
268
268
|
* recording never sees them.
|
|
@@ -958,8 +958,8 @@ export function CraneHandles({ rail, crane, controlPoints, selectedIndex, enable
|
|
|
958
958
|
event.preventDefault();
|
|
959
959
|
const index = hit.object.userData.craneIndex;
|
|
960
960
|
s.onSelect(index);
|
|
961
|
-
dragRef.current = { index, slide: event.shiftKey, recorded: false };
|
|
962
|
-
setDragInfo({ kind: event.shiftKey ? "slide" : "free" });
|
|
961
|
+
dragRef.current = floorDragStart(s, index, raycaster.ray, event.shiftKey) ?? { index, slide: event.shiftKey, recorded: false };
|
|
962
|
+
setDragInfo(dragRef.current.axis ? { kind: "bend", t0: dragRef.current.t0, radius: dragRef.current.radius } : { kind: event.shiftKey ? "slide" : "free" });
|
|
963
963
|
invalidate();
|
|
964
964
|
};
|
|
965
965
|
const onMove = (event) => {
|
|
@@ -982,8 +982,8 @@ export function CraneHandles({ rail, crane, controlPoints, selectedIndex, enable
|
|
|
982
982
|
const height = Math.max(0.1, Math.min(12, drag.startHeight + travel));
|
|
983
983
|
s.onChangePoints(points.map((entry, i) => (i === drag.index ? { ...entry, height } : entry)), { dragging: true });
|
|
984
984
|
} else if (drag.startControls && drag.bendWeights && s.onChangeRail) {
|
|
985
|
-
const dx = drag.axis === "x" ? travel : 0;
|
|
986
|
-
const dz = drag.axis === "z" ? travel : 0;
|
|
985
|
+
const dx = drag.axis === "x" ? travel : drag.axis === "floor" ? world.x : 0;
|
|
986
|
+
const dz = drag.axis === "z" ? travel : drag.axis === "floor" ? world.z : 0;
|
|
987
987
|
const moved = drag.startControls.map((entry, i) => (drag.bendWeights[i] > 0
|
|
988
988
|
? { ...entry, x: entry.x + dx * drag.bendWeights[i], z: entry.z + dz * drag.bendWeights[i] }
|
|
989
989
|
: entry));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The stage look's environment light (stage.lighting.envLight): a sky-and-ground
|
|
2
|
+
// map built from the look's own colours, so clay and kit materials take the sky's
|
|
3
|
+
// tint and a soft fill from every direction instead of a flat two-colour
|
|
4
|
+
// hemisphere. The upper half grades from `sky` at the horizon to `skyTop`
|
|
5
|
+
// overhead (the same easing as the background, stage-sky.jsx), the lower half is
|
|
6
|
+
// the ground bounce (hemiGround), and a glow sits around the key's direction (the
|
|
7
|
+
// sun, the moon). Pure arithmetic in linear RGB, so the tests read the pixels;
|
|
8
|
+
// stage-sky.jsx prefilters the map with PMREMGenerator and sets it as
|
|
9
|
+
// scene.environment.
|
|
10
|
+
//
|
|
11
|
+
// Brightness: the sky half is scaled to carry the hemisphere's sky light (hemiSky)
|
|
12
|
+
// in total, and stageLook() hands the hemisphere's
|
|
13
|
+
// share to the map as envLight rises (0 = hemisphere only, the studio look;
|
|
14
|
+
// 1 = the map alone at the same level; 2 = twice as bright).
|
|
15
|
+
|
|
16
|
+
export const ENV_WIDTH = 128;
|
|
17
|
+
export const ENV_HEIGHT = 64;
|
|
18
|
+
// How much the key's glow adds under crisp shadows, as a share of the sky's own
|
|
19
|
+
// light on a surface facing it, and its spread (1 - cos of the angle where it falls to 1/e, ~24 deg).
|
|
20
|
+
const SUN_SHARE = 0.8;
|
|
21
|
+
const SUN_SPREAD = 0.085;
|
|
22
|
+
// Below the horizon the ground takes over across this band (degrees).
|
|
23
|
+
const HORIZON_BAND = 8;
|
|
24
|
+
|
|
25
|
+
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
26
|
+
/** "#rrggbb" to linear RGB 0..1. */
|
|
27
|
+
export const linearRgb = (hex) => [1, 3, 5].map((i) => toLinear(parseInt(hex.slice(i, i + 2), 16) / 255));
|
|
28
|
+
export const luminance = ([r, g, b]) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
29
|
+
const mix = (a, b, t) => a.map((value, i) => value + (b[i] - value) * t);
|
|
30
|
+
const scale = (a, k) => a.map((value) => value * k);
|
|
31
|
+
const round = (a) => a.map((value) => Number(value.toFixed(5)));
|
|
32
|
+
|
|
33
|
+
// The sky's easing by elevation (degrees): 0 at and below the horizon, 1 from 60 deg up.
|
|
34
|
+
const skyBlend = (elevation) => (elevation <= 0 ? 0 : 1 - (1 - Math.min(1, elevation / 60)) ** 2);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What the map is made of, in linear RGB, or null when envLight is 0. `lighting`
|
|
38
|
+
* and `keyLight` are normalized (stage-lighting.js); `sky` is the horizon colour
|
|
39
|
+
* the look resolved (the theme grey when lighting.sky is null).
|
|
40
|
+
*/
|
|
41
|
+
export function environmentSpec(lighting, keyLight, sky) {
|
|
42
|
+
if (!(lighting.envLight > 0)) return null;
|
|
43
|
+
const horizon = linearRgb(sky), zenith = linearRgb(lighting.skyTop ?? sky), skyLight = linearRgb(lighting.hemiSky);
|
|
44
|
+
// The sky half's mean luminance over its solid angle, so the sky can be scaled
|
|
45
|
+
// to carry hemiSky's light in total whatever its colours: a sky brighter at the
|
|
46
|
+
// horizon then lights the sides of a figure more and its top less.
|
|
47
|
+
let weighted = 0, weights = 0;
|
|
48
|
+
for (let step = 0; step < 90; step++) {
|
|
49
|
+
const elevation = step + 0.5, cos = Math.cos((elevation * Math.PI) / 180);
|
|
50
|
+
weighted += luminance(mix(horizon, zenith, skyBlend(elevation))) * cos;
|
|
51
|
+
weights += cos;
|
|
52
|
+
}
|
|
53
|
+
const mean = weighted / weights, target = luminance(skyLight);
|
|
54
|
+
// A black sky has no shape to keep: the sky half is then hemiSky itself.
|
|
55
|
+
const [low, high] = mean > 1e-6 ? [scale(horizon, target / mean), scale(zenith, target / mean)] : [skyLight, skyLight];
|
|
56
|
+
const length = Math.hypot(keyLight.x, keyLight.y, keyLight.z) || 1, key = linearRgb(keyLight.color ?? "#ffffff");
|
|
57
|
+
// A soft-shadowed look is a hazy or overcast sky: its sun is a fainter glow.
|
|
58
|
+
const keyLuma = luminance(key) || 1, share = SUN_SHARE * (1 - 0.6 * lighting.shadowSoftness);
|
|
59
|
+
return {
|
|
60
|
+
horizon: round(low), zenith: round(high), ground: round(linearRgb(lighting.hemiGround)),
|
|
61
|
+
sun: { direction: round([keyLight.x / length, keyLight.y / length, keyLight.z / length]),
|
|
62
|
+
// peak radiance: the glow integrates to ~2*pi*spread, so this adds `share` of the sky's light toward it
|
|
63
|
+
color: round(scale(key, (share * target) / (2 * SUN_SPREAD) / keyLuma)) },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Radiance (linear RGB) of the map in a unit direction [x, y, z] (y up). */
|
|
68
|
+
export function environmentRadiance(spec, [x, y, z]) {
|
|
69
|
+
const elevation = (Math.asin(Math.max(-1, Math.min(1, y))) * 180) / Math.PI;
|
|
70
|
+
const skyAt = mix(spec.horizon, spec.zenith, skyBlend(elevation));
|
|
71
|
+
const base = elevation >= 0 ? skyAt : mix(spec.horizon, spec.ground, Math.min(1, -elevation / HORIZON_BAND));
|
|
72
|
+
const [sx, sy, sz] = spec.sun.direction, cos = x * sx + y * sy + z * sz;
|
|
73
|
+
const glow = Math.exp((cos - 1) / SUN_SPREAD);
|
|
74
|
+
return base.map((value, i) => value + spec.sun.color[i] * glow);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The equirectangular map as RGBA floats, rows from straight down (row 0) to
|
|
79
|
+
* straight up, columns around the horizon the way three samples an
|
|
80
|
+
* equirectangular texture (u = atan2(z, x) / 2pi + 0.5).
|
|
81
|
+
*/
|
|
82
|
+
export function environmentPixels(spec, width = ENV_WIDTH, height = ENV_HEIGHT) {
|
|
83
|
+
const data = new Float32Array(width * height * 4);
|
|
84
|
+
for (let row = 0; row < height; row++) {
|
|
85
|
+
const elevation = ((row + 0.5) / height - 0.5) * Math.PI, cosEl = Math.cos(elevation), y = Math.sin(elevation);
|
|
86
|
+
for (let column = 0; column < width; column++) {
|
|
87
|
+
const azimuth = ((column + 0.5) / width - 0.5) * 2 * Math.PI;
|
|
88
|
+
const radiance = environmentRadiance(spec, [Math.cos(azimuth) * cosEl, y, Math.sin(azimuth) * cosEl]);
|
|
89
|
+
data.set([...radiance, 1], (row * width + column) * 4);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The built-in HDR environments (stage-hdr.js HDR_BUILTINS), drawn as
|
|
2
|
+
// equirectangular radiance maps in linear RGB: a night street, an overcast
|
|
3
|
+
// field and a photo studio. Pure arithmetic and an integer hash, so a map is
|
|
4
|
+
// the same on every machine and the tests read its pixels. Rows run from
|
|
5
|
+
// straight down (row 0) to straight up, columns around the horizon the way
|
|
6
|
+
// three samples an equirectangular texture: azimuth 0 is +x, +90 is +z (toward
|
|
7
|
+
// the default camera), -90 is -z.
|
|
8
|
+
|
|
9
|
+
const D = Math.PI / 180;
|
|
10
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
11
|
+
const smooth = (value) => { const t = clamp01(value); return t * t * (3 - 2 * t); };
|
|
12
|
+
const mix = (a, b, t) => a.map((value, i) => value + (b[i] - value) * t);
|
|
13
|
+
const add = (out, colour, weight) => { if (weight > 0) for (let i = 0; i < 3; i++) out[i] += colour[i] * weight; };
|
|
14
|
+
/** A 0..1 integer hash of two integers and a seed: windows, rooflines and trees. */
|
|
15
|
+
function hash(a, b, seed) {
|
|
16
|
+
let h = Math.imul(a, 0x27d4eb2d) ^ Math.imul(b, 0x165667b1) ^ Math.imul(seed, 0x9e3779b1);
|
|
17
|
+
h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
|
|
18
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
19
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
20
|
+
}
|
|
21
|
+
/** Smooth 0..1 value noise around the horizon: `knots` hashed values, eased between, wrapping at azimuth +-180. */
|
|
22
|
+
const noise = (az, knots, seed) => {
|
|
23
|
+
const x = ((az + 180) / 360) * knots, i = Math.floor(x), t = smooth(x - i);
|
|
24
|
+
return hash(i % knots, 0, seed) * (1 - t) + hash((i + 1) % knots, 0, seed) * t;
|
|
25
|
+
};
|
|
26
|
+
const unit = (az, el) => [Math.cos(el * D) * Math.cos(az * D), Math.sin(el * D), Math.cos(el * D) * Math.sin(az * D)];
|
|
27
|
+
/** A round light of peak radiance 1 at `centre`, falling to 1/e `radius` degrees off it. */
|
|
28
|
+
const spot = (dir, centre, radius) => Math.exp((dir[0] * centre[0] + dir[1] * centre[1] + dir[2] * centre[2] - 1) / (1 - Math.cos(radius * D)));
|
|
29
|
+
/** Signed azimuth difference in degrees, -180..180. */
|
|
30
|
+
const azDelta = (az, centre) => ((((az - centre + 180) % 360) + 360) % 360) - 180;
|
|
31
|
+
/** A soft-edged panel in azimuth/elevation (degrees): 1 inside, 0 outside, `edge` wide. */
|
|
32
|
+
const panel = (az, el, { az: a, el: e, w, h, edge = 1.5 }) => smooth((w - Math.abs(azDelta(az, a)) * Math.cos(e * D)) / edge) * smooth((h - Math.abs(el - e)) / edge);
|
|
33
|
+
|
|
34
|
+
const NIGHT = { lampA: unit(42.5, 11.6), lampB: unit(-140.5, 12.8), moon: unit(-60, 48) };
|
|
35
|
+
function nightStreet(az, el, dir) {
|
|
36
|
+
const out = el >= 0 ? mix([0.03, 0.028, 0.042], [0.004, 0.006, 0.016], Math.sqrt(el / 90)) : [0.006, 0.006, 0.0075];
|
|
37
|
+
if (el >= 0) {
|
|
38
|
+
// A skyline of blocks 5..14 degrees wide and 3..16 tall, with lit windows.
|
|
39
|
+
const block = Math.floor((az + 180) / 9), roof = 3 + 13 * hash(block, 1, 7) * (0.5 + 0.5 * hash(block, 2, 7));
|
|
40
|
+
if (el < roof) {
|
|
41
|
+
out.splice(0, 3, 0.007, 0.007, 0.01);
|
|
42
|
+
const column = Math.floor((az + 180) / 1.1), row = Math.floor(el / 1.5), lit = hash(column, row, 11);
|
|
43
|
+
const inside = Math.abs(((az + 180) / 1.1) % 1 - 0.5) < 0.28 && Math.abs((el / 1.5) % 1 - 0.5) < 0.24;
|
|
44
|
+
if (inside && lit < 0.3) add(out, lit < 0.2 ? [0.42, 0.3, 0.15] : [0.2, 0.22, 0.28], 1);
|
|
45
|
+
}
|
|
46
|
+
add(out, [3.2, 3.4, 3.8], spot(dir, NIGHT.moon, 0.45));
|
|
47
|
+
add(out, [0.03, 0.035, 0.05], spot(dir, NIGHT.moon, 9));
|
|
48
|
+
} else {
|
|
49
|
+
// Wet asphalt: a warm pool under each lamp and a streak reflecting it.
|
|
50
|
+
for (const [lamp, az0] of [[NIGHT.lampA, 42.5], [NIGHT.lampB, -140.5]]) {
|
|
51
|
+
const across = azDelta(az, az0), pool = Math.exp(-(across * across) / (2 * 22 * 22)) * smooth((-el - 2) / 10) * Math.exp(-Math.max(0, -el - 18) / 18);
|
|
52
|
+
add(out, [0.09, 0.05, 0.018], pool);
|
|
53
|
+
add(out, [0.6, 0.33, 0.1], Math.exp(-(across * across) / 4) * smooth((-el) / 2) * Math.exp(el / 10) * (lamp === NIGHT.lampA ? 1 : 0.8));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
add(out, [70, 38, 12], spot(dir, NIGHT.lampA, 1.3));
|
|
57
|
+
add(out, [55, 30, 9.5], spot(dir, NIGHT.lampB, 1.3));
|
|
58
|
+
add(out, [0.9, 0.5, 0.17], spot(dir, NIGHT.lampA, 5) + 0.8 * spot(dir, NIGHT.lampB, 5));
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const OVERCAST = { sun: unit(-118, 40) };
|
|
63
|
+
function overcastField(az, el, dir) {
|
|
64
|
+
if (el < 0) {
|
|
65
|
+
// A green field, hazier toward the horizon.
|
|
66
|
+
const out = mix([0.13, 0.155, 0.1], [0.085, 0.125, 0.045], smooth(-el / 25));
|
|
67
|
+
add(out, [0.012, 0.015, 0.004], hash(Math.floor((az + 180) * 2), Math.floor(-el * 2), 3) - 0.5);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
// The overcast sky is three times brighter overhead than at the horizon.
|
|
71
|
+
const out = [0.8, 0.83, 0.88].map((value) => value * (1 + 2 * Math.sin(el * D)) / 3);
|
|
72
|
+
add(out, [0.9, 0.85, 0.74], spot(dir, OVERCAST.sun, 14));
|
|
73
|
+
// A tree line: two octaves of smooth value noise around the horizon, 0.5..3 degrees tall.
|
|
74
|
+
const crown = 0.5 + 1.8 * noise(az, 52, 5) + 0.7 * noise(az, 225, 6);
|
|
75
|
+
if (el < crown) return mix(out, [0.032, 0.046, 0.028], smooth((crown - el) / 0.25));
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const STUDIO = { keyCentre: unit(130.6, 23.5), key: { az: 130.6, el: 23.5, w: 16, h: 12 }, strip: { az: 13, el: 15, w: 4, h: 22 }, rim: { az: -84, el: 19, w: 10, h: 14 } };
|
|
80
|
+
function studioSoftbox(az, el, dir) {
|
|
81
|
+
const out = el >= 0 ? [0.018, 0.018, 0.02] : mix([0.032, 0.032, 0.034], [0.022, 0.022, 0.024], smooth(-el / 60));
|
|
82
|
+
// The softbox is a touch brighter at its centre, like a real diffuser.
|
|
83
|
+
add(out, [5, 4.9, 4.7], panel(az, el, STUDIO.key) * (0.92 + 0.08 * spot(dir, STUDIO.keyCentre, 8)));
|
|
84
|
+
add(out, [2.2, 2.3, 2.5], panel(az, el, STUDIO.strip));
|
|
85
|
+
add(out, [2, 2, 2], panel(az, el, STUDIO.rim));
|
|
86
|
+
add(out, [0.5, 0.5, 0.5], smooth((el - 66) / 6));
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const DRAW = { "builtin:night-street": nightStreet, "builtin:overcast-field": overcastField, "builtin:studio-softbox": studioSoftbox };
|
|
91
|
+
export const BUILTIN_WIDTH = 1024;
|
|
92
|
+
export const BUILTIN_HEIGHT = 512;
|
|
93
|
+
|
|
94
|
+
/** A built-in map as RGBA floats (linear radiance), or null for an unknown id. */
|
|
95
|
+
export function builtinEnvironmentPixels(id, width = BUILTIN_WIDTH, height = BUILTIN_HEIGHT) {
|
|
96
|
+
const draw = DRAW[id];
|
|
97
|
+
if (!draw) return null;
|
|
98
|
+
const data = new Float32Array(width * height * 4);
|
|
99
|
+
for (let row = 0; row < height; row++) {
|
|
100
|
+
const el = ((row + 0.5) / height - 0.5) * 180;
|
|
101
|
+
for (let column = 0; column < width; column++) {
|
|
102
|
+
const az = ((column + 0.5) / width - 0.5) * 360, radiance = draw(az, el, unit(az, el));
|
|
103
|
+
data.set([Math.max(0, radiance[0]), Math.max(0, radiance[1]), Math.max(0, radiance[2]), 1], (row * width + column) * 4);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
@@ -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
|
+
}
|