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,63 @@
|
|
|
1
|
+
// Take v3: what the compiler bakes and the player reads. Bone rotations are LOCAL quaternions
|
|
2
|
+
// on the Mixamo control bones (the first depth-first bone of each name), so playback writes
|
|
3
|
+
// them straight onto the rig; the hips also carry a position. Everything is expressed in the
|
|
4
|
+
// character's placement frame (its base position and yaw), in metres, so moving or turning
|
|
5
|
+
// the character carries the take with it. Scene objects the motion animates ride along in
|
|
6
|
+
// world space: one track per spec.objects key (a group: its root only). A switch entry (C15)
|
|
7
|
+
// bakes only vis and lvl, no pos/quat: its record keeps its own place (object-travel.js). A follow
|
|
8
|
+
// entry (C16) bakes pos/quat with hold on every frame: its track is world space throughout. A free entry
|
|
9
|
+
// (C17) bakes pos/quat (and vis/lvl when it names them), world space, no hold.
|
|
10
|
+
import { toLocal } from "./compiler/skeleton.js";
|
|
11
|
+
import { sub, apply, ry, quatFromMat, mul } from "./compiler/math.js";
|
|
12
|
+
import { isCarried, isFollow, isSwitch } from "./carried.js";
|
|
13
|
+
import { isFree } from "./free-motion.js";
|
|
14
|
+
import { rootPoseAt, spanOf } from "./object-pose.js";
|
|
15
|
+
|
|
16
|
+
const TAKE_SCHEMA = "cozyclay.take.v3";
|
|
17
|
+
|
|
18
|
+
/** out = compileMotion(...) result; placement = { position: [x,y,z], yaw } of the character. */
|
|
19
|
+
export function bakeTake(sk, out, placement = { position: [0, 0, 0], yaw: 0 }, meta = {}) {
|
|
20
|
+
const F = out.frames.length, B = sk.names.length;
|
|
21
|
+
const rot = new Float32Array(F * B * 4), hips = new Float32Array(F * 3);
|
|
22
|
+
const unYaw = ry(-(placement.yaw ?? 0));
|
|
23
|
+
for (let f = 0; f < F; f++) {
|
|
24
|
+
const s = out.frames[f].s;
|
|
25
|
+
const local = { P: s.P.map(p => apply(unYaw, sub(p, placement.position))), R: s.R.map(R => mul(unYaw, R)) };
|
|
26
|
+
const { quats, hips: h } = toLocal(sk, local);
|
|
27
|
+
quats.forEach((q, b) => rot.set(q, (f * B + b) * 4));
|
|
28
|
+
hips.set(h, f * 3);
|
|
29
|
+
}
|
|
30
|
+
// one track per spec.objects key: a body, or a group root its parts ride (object-pose.js);
|
|
31
|
+
// `hold` marks the frames a carried record is in hand, and every frame of a follow entry (world
|
|
32
|
+
// space: object-travel.js cuts the carry of its own parents there); a switch bakes vis/lvl only
|
|
33
|
+
const objects = {}, f0 = out.f0 ?? 0;
|
|
34
|
+
for (const [id, spec] of Object.entries(out.ctx.spec.objects ?? {})) {
|
|
35
|
+
const carried = isCarried(spec), follows = isFollow(spec), rides = carried || follows, moves = !isSwitch(spec);
|
|
36
|
+
const pos = moves ? new Float32Array(F * 3) : null, quat = moves ? new Float32Array(F * 4) : null;
|
|
37
|
+
const named = rides || !moves || isFree(spec), vis = named && spec.visible ? new Uint8Array(F) : null, hold = rides ? new Uint8Array(F) : null;
|
|
38
|
+
const lvl = named && spec.level ? new Float32Array(F) : null;
|
|
39
|
+
const [grab, release] = carried ? spanOf(out, spec) : [0, 0];
|
|
40
|
+
for (let f = 0; f < F; f++) {
|
|
41
|
+
if (vis) vis[f] = (out.baked?.[spec.visible]?.[f + f0] ?? 0) > 0.5 ? 1 : 0;
|
|
42
|
+
if (lvl) lvl[f] = Math.max(0, Math.min(4, out.baked?.[spec.level]?.[f + f0] ?? 1));
|
|
43
|
+
if (!moves) continue;
|
|
44
|
+
const o = rootPoseAt(out, id, f + f0);
|
|
45
|
+
if (!o) continue;
|
|
46
|
+
pos.set(o.pos, f * 3); quat.set(quatFromMat(o.R), f * 4);
|
|
47
|
+
if (hold) hold[f] = follows || (f + f0 >= grab && f + f0 <= release) ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
objects[id] = { ...(moves ? { pos, quat } : {}), ...(vis ? { vis } : {}), ...(hold ? { hold } : {}), ...(lvl ? { lvl } : {}) };
|
|
50
|
+
}
|
|
51
|
+
return { schema: TAKE_SCHEMA, fps: out.fps, frameCount: F, rig: sk.model, bones: sk.names.slice(), rot, hips, objects, placement, ...meta };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Local quaternion [x,y,z,w] of bone b at frame f (nearest frame, clamped). */
|
|
55
|
+
export function takeQuat(take, f, b) {
|
|
56
|
+
const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))), i = (ff * take.bones.length + b) * 4;
|
|
57
|
+
return [take.rot[i], take.rot[i + 1], take.rot[i + 2], take.rot[i + 3]];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function takeHips(take, f) {
|
|
61
|
+
const ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f))) * 3;
|
|
62
|
+
return [take.hips[ff], take.hips[ff + 1], take.hips[ff + 2]];
|
|
63
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// C4. The compiled motion as beats, for the agent (output.timeline) and the beats contact sheet.
|
|
2
|
+
// At most 12 entries, ordered by f[0]:
|
|
3
|
+
// { beat: skill name | "walk" | "keys", src: "use[i]" | "generators[i]" | "keys",
|
|
4
|
+
// f: [first, last] (inclusive, absolute frames),
|
|
5
|
+
// end: { root: [x,y,z], yaw, hand_l: [x,y,z], hand_r: [x,y,z], on_l: ref | null, on_r: ref | null },
|
|
6
|
+
// planted: { l: [[f0, f1], ...], r: [...] }, // at most 4 per foot
|
|
7
|
+
// events: [{ f, object, event: "grab" | "release" | "show" | "hide" }] }
|
|
8
|
+
// Metres to 3 decimals, degrees to 1. Beats come from the skills expandUse ran (used), the
|
|
9
|
+
// generators, and the runs of keys no skill or generator covers; a retime (out.timeMap) moves
|
|
10
|
+
// them with the keys. end is the pose at f[1]: root = the Hips joint, hand_* = the wrists,
|
|
11
|
+
// on_* = the "object:<id>" that hand holds there (object-pose.js inHandAt), else the
|
|
12
|
+
// "anchor:<a>" whose w_<a>_<k> > 0.5 (the largest). planted = the heel or the ball below
|
|
13
|
+
// QA_LIMITS.plantH over the ground (the floor or an object's top, qa/ground.js) and still (a foot on its ball, heel up, is planted), for 3 frames or more (the first, the last and the longest others
|
|
14
|
+
// when there are more than 4). Events: grab/release when an object enters or leaves a hand,
|
|
15
|
+
// show/hide when a visibility channel (a carry's or a switch's `visible`, or any *_vis channel)
|
|
16
|
+
// crosses 0.5; `object` is the object id (one event per object the channel shows), or the
|
|
17
|
+
// channel's name when no object owns it.
|
|
18
|
+
import { QA_LIMITS } from "./qa/checks.js";
|
|
19
|
+
import { groundSole } from "./qa/ground.js";
|
|
20
|
+
import { inHandAt } from "./object-pose.js";
|
|
21
|
+
|
|
22
|
+
const MAX_BEATS = 12, PER_FOOT = 4, MIN_PLANT = 3, STILL_Y = 0.0005;
|
|
23
|
+
const r3 = v => Math.round(v * 1000) / 1000, r1 = v => Math.round(v * 10) / 10;
|
|
24
|
+
const vec = p => p.map(r3);
|
|
25
|
+
|
|
26
|
+
/** The beats' frame ranges before their poses are read: [{ beat, src, f }]. */
|
|
27
|
+
function beatsOf(out, used, spec) {
|
|
28
|
+
const last = out.frameCount - 1, map = out.timeMap ?? (f => f);
|
|
29
|
+
const beats = used.map((u, i) => ({ beat: u.skill, src: `use[${i}]`, f: [u.f0, u.end ?? u.f0] }));
|
|
30
|
+
(spec.generators ?? []).forEach((g, i) => beats.push({ beat: g.type, src: `generators[${i}]`, f: [g.f0, g.f0 + 16 + (g.steps ?? 8) * (g.period ?? 20) + 18] }));
|
|
31
|
+
const covered = f => beats.some(b => f >= b.f[0] && f <= b.f[1]);
|
|
32
|
+
const loose = [...new Set((spec.keys ?? []).map(k => k.f))].filter(f => Number.isFinite(f) && !covered(f)).sort((a, b) => a - b);
|
|
33
|
+
let run = null;
|
|
34
|
+
for (const f of loose) {
|
|
35
|
+
if (run && !beats.some(b => b.src !== "keys" && b.f[1] > run.f[1] && b.f[0] < f)) run.f[1] = f;
|
|
36
|
+
else beats.push(run = { beat: "keys", src: "keys", f: [f, f] });
|
|
37
|
+
}
|
|
38
|
+
const clamp = f => Math.max(0, Math.min(last, Math.round(map(f))));
|
|
39
|
+
for (const b of beats) b.f = [clamp(b.f[0]), clamp(b.f[1])];
|
|
40
|
+
return beats.sort((a, b) => a.f[0] - b.f[0]).slice(0, MAX_BEATS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** planted[side][f]: the heel or the ball low and still at absolute frame f (false outside the window). */
|
|
44
|
+
function plantedFlags(out) {
|
|
45
|
+
const n = out.frameCount, a = out.f0 ?? 0, flags = { l: new Uint8Array(n), r: new Uint8Array(n) };
|
|
46
|
+
for (const [k, S] of [["l", "Left"], ["r", "Right"]]) {
|
|
47
|
+
const sole = out.frames.map((fr, i) => (fr ? groundSole(out, i, S) : null));
|
|
48
|
+
sole.forEach((p, i) => {
|
|
49
|
+
const q = sole[i > 0 ? i - 1 : i + 1];
|
|
50
|
+
if (!p || !q) return;
|
|
51
|
+
const ok = ["heel", "ball"].some(key => p[key][1] < QA_LIMITS.plantH && Math.abs(p[key][1] - q[key][1]) < STILL_Y
|
|
52
|
+
&& Math.hypot(p[key][0] - q[key][0], p[key][2] - q[key][2]) <= QA_LIMITS.slide);
|
|
53
|
+
flags[k][a + i] = ok ? 1 : 0;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function plantedIn(flags, [f0, f1]) {
|
|
60
|
+
const runs = [];
|
|
61
|
+
let start = null;
|
|
62
|
+
for (let f = f0; f <= f1 + 1; f++) {
|
|
63
|
+
const on = f <= f1 && flags[f] === 1;
|
|
64
|
+
if (on && start === null) start = f;
|
|
65
|
+
if (!on && start !== null) { if (f - start >= MIN_PLANT) runs.push([start, f - 1]); start = null; }
|
|
66
|
+
}
|
|
67
|
+
if (runs.length <= PER_FOOT) return runs;
|
|
68
|
+
const inner = runs.slice(1, -1).sort((x, y) => (y[1] - y[0]) - (x[1] - x[0]) || x[0] - y[0]).slice(0, PER_FOOT - 2);
|
|
69
|
+
return [runs[0], ...inner, runs[runs.length - 1]].sort((x, y) => x[0] - y[0]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Every grab/release/show/hide of the take, by absolute frame. */
|
|
73
|
+
function eventsOf(out, spec) {
|
|
74
|
+
const n = out.frameCount, events = [], objects = Object.entries(spec.objects ?? {});
|
|
75
|
+
for (const [id, o] of objects) for (let f = 1; f < n; f++) {
|
|
76
|
+
const was = inHandAt(o, f - 1), is = inHandAt(o, f);
|
|
77
|
+
if (is && !was) events.push({ f, object: id, event: "grab" });
|
|
78
|
+
if (was && !is) events.push({ f: f - 1, object: id, event: "release" });
|
|
79
|
+
}
|
|
80
|
+
// every object a channel shows (a carry's or a switch's visible) gets its own event
|
|
81
|
+
const owners = {};
|
|
82
|
+
for (const [id, o] of objects) if (typeof o?.visible === "string") (owners[o.visible] ??= []).push(id);
|
|
83
|
+
for (const ch of new Set([...Object.keys(owners), ...Object.keys(out.baked ?? {}).filter(c => c.endsWith("_vis"))])) {
|
|
84
|
+
const v = out.baked?.[ch];
|
|
85
|
+
if (!v) continue;
|
|
86
|
+
for (let f = 1; f < n; f++) {
|
|
87
|
+
const was = v[f - 1] > 0.5, is = v[f] > 0.5;
|
|
88
|
+
if (was !== is) for (const object of owners[ch] ?? [ch]) events.push({ f, object, event: is ? "show" : "hide" });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return events.sort((x, y) => x.f - y.f);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** "object:<id>" held by hand k at f, else the "anchor:<a>" with the largest weight > 0.5, else null. */
|
|
95
|
+
function onOf(spec, c, k, f) {
|
|
96
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) if (spec.props?.[o?.prop]?.hand === k && inHandAt(o, f)) return `object:${id}`;
|
|
97
|
+
let best = null, bw = 0.5;
|
|
98
|
+
for (const [ch, w] of Object.entries(c)) {
|
|
99
|
+
const m = /^w_(.+)_([lr])$/.exec(ch);
|
|
100
|
+
if (m && m[2] === k && w > bw) { best = m[1]; bw = w; }
|
|
101
|
+
}
|
|
102
|
+
return best && `anchor:${best}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function endOf(out, spec, f) {
|
|
106
|
+
const fr = out.frames[f - (out.f0 ?? 0)];
|
|
107
|
+
if (!fr) return null;
|
|
108
|
+
const { s, c } = fr, I = out.ctx.sk.index;
|
|
109
|
+
return { root: vec(s.P[I.Hips]), yaw: r1(c.yaw ?? 0), hand_l: vec(s.P[I.LeftHand]), hand_r: vec(s.P[I.RightHand]),
|
|
110
|
+
on_l: onOf(spec, c, "l", f), on_r: onOf(spec, c, "r", f) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* timelineOf(out, { used, spec }) -> entries
|
|
115
|
+
* @param {object} out compileMotion result
|
|
116
|
+
* @param {{ used: { skill: string, f0: number, end: number }[], spec: object }} ctx used = the
|
|
117
|
+
* skills expandUse ran, spec = the expanded spec
|
|
118
|
+
* @returns {object[]}
|
|
119
|
+
*/
|
|
120
|
+
export function timelineOf(out, { used, spec }) {
|
|
121
|
+
const beats = beatsOf(out, used ?? [], spec);
|
|
122
|
+
if (!beats.length) return [];
|
|
123
|
+
const flags = plantedFlags(out), events = eventsOf(out, spec);
|
|
124
|
+
return beats.map(b => ({ ...b, end: endOf(out, spec, b.f[1]),
|
|
125
|
+
planted: { l: plantedIn(flags.l, b.f), r: plantedIn(flags.r, b.f) },
|
|
126
|
+
events: events.filter(e => e.f >= b.f[0] && e.f <= b.f[1]) }));
|
|
127
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// C11 own views: the cameras of a verify_result contact_sheet with `views`. They are the
|
|
2
|
+
// agent's own cameras around the subject, never the user's shot camera. A view stays at one
|
|
3
|
+
// place for every frame of the sheet and frames what matters over those frames (the focus
|
|
4
|
+
// refs, else the whole character, plus any trail), so a small action fills the tile and
|
|
5
|
+
// motion reads as change inside one still frame. Pure over a compile result (compileSpec .out).
|
|
6
|
+
// front in front of the character, 10 degrees up
|
|
7
|
+
// side from the side the focus is on (the right when centred), 10 degrees up
|
|
8
|
+
// three-quarter between front and that side, 25 degrees up
|
|
9
|
+
// top straight down, the character's forward up the image
|
|
10
|
+
// depthViews adds a view when the asked ones would hide depth (see APART below).
|
|
11
|
+
import { refPoint } from "./refs.js";
|
|
12
|
+
import { objectBoxesAt } from "./object-pose.js";
|
|
13
|
+
import { resolveBone } from "./bones.js";
|
|
14
|
+
import { add, sub, scale, dot, cross, norm, apply, quatFromMat } from "./compiler/math.js";
|
|
15
|
+
|
|
16
|
+
export const VIEW_NAMES = ["front", "side", "three-quarter", "top"];
|
|
17
|
+
// FOV is the widest lens; a deep subject (a body seen from above) moves the camera back until
|
|
18
|
+
// its far side is at most DEPTH_RATIO times as far as its near side, and the lens narrows (down
|
|
19
|
+
// to MIN_FOV) to fill the tile again, so perspective stays mild.
|
|
20
|
+
const FOV = 35, MIN_FOV = 12, DEPTH_RATIO = 1.8, MARGIN = 1.12, PAD = 0.06, MIN_HALF = 0.15, MIN_DEPTH = 0.35, UP = [0, 1, 0];
|
|
21
|
+
const ELEVATION = { front: 10, side: 10, "three-quarter": 25 };
|
|
22
|
+
const BARE_OBJECT = /^object:(.+?)(\.(?:center|top|base))?$/s;
|
|
23
|
+
const rad = d => (d * Math.PI) / 180;
|
|
24
|
+
const mean = points => scale(points.reduce(add, [0, 0, 0]), 1 / Math.max(1, points.length));
|
|
25
|
+
|
|
26
|
+
const jointsAt = (out, f) => out.frames?.[f - (out.f0 ?? 0)]?.s.P ?? [];
|
|
27
|
+
const corners = b => [-1, 1].flatMap(x => [0, 2].flatMap(y => [-1, 1].map(z => add(b.pos, apply(b.R, [x * b.half[0], y * b.half[1], z * b.half[2]])))));
|
|
28
|
+
|
|
29
|
+
/** World points of a ref at frame f: a bare object:<id> is its part boxes' corners, any other ref its point. */
|
|
30
|
+
function refPoints(out, f, ref) {
|
|
31
|
+
const bare = BARE_OBJECT.exec(ref);
|
|
32
|
+
if (bare && !bare[2]) return (objectBoxesAt(out, bare[1], f) ?? []).flatMap(corners);
|
|
33
|
+
const p = refPoint(out, f, ref);
|
|
34
|
+
return p ? [p] : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The character's horizontal left and forward over the frames (the hip line, summed). */
|
|
38
|
+
function facingOf(out, frames) {
|
|
39
|
+
const sk = out.ctx.sk, l = sk.index[resolveBone(sk, "LeftUpLeg").bone], r = sk.index[resolveBone(sk, "RightUpLeg").bone];
|
|
40
|
+
let v = [0, 0, 0];
|
|
41
|
+
for (const f of frames) { const P = jointsAt(out, f); if (P.length) { const d = sub(P[l], P[r]); v = add(v, [d[0], 0, d[2]]); } }
|
|
42
|
+
const left = Math.hypot(v[0], v[2]) > 1e-6 ? norm(v) : [1, 0, 0];
|
|
43
|
+
return { left, forward: cross(left, UP) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Path of a ref over absolute frames lo..hi: [{ f, p }] where it is known. */
|
|
47
|
+
export function trailPoints(out, ref, [lo, hi]) {
|
|
48
|
+
const points = [];
|
|
49
|
+
for (let f = lo; f <= hi; f++) { const p = refPoint(out, f, ref); if (p) points.push({ f, p }); }
|
|
50
|
+
return points;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A camera at `back` (unit, target to eye) whose frustum holds every point (with PAD) at MARGIN. */
|
|
54
|
+
function fit(name, points, back, upHint, aspect) {
|
|
55
|
+
const right = norm(cross(upHint, back)), up = cross(back, right);
|
|
56
|
+
const c0 = mean(points), rel = points.map(p => sub(p, c0)), xs = rel.map(q => dot(q, right)), ys = rel.map(q => dot(q, up)), zs = rel.map(q => dot(q, back));
|
|
57
|
+
const cx = (Math.min(...xs) + Math.max(...xs)) / 2, cy = (Math.min(...ys) + Math.max(...ys)) / 2;
|
|
58
|
+
const target = add(c0, add(scale(right, cx), scale(up, cy)));
|
|
59
|
+
// The vertical half-angle tangent each point needs from distance d (x through the aspect).
|
|
60
|
+
const need = d => Math.max(...rel.map((q, i) => MARGIN * Math.max(Math.max(Math.abs(xs[i] - cx) + PAD, MIN_HALF) / aspect, Math.max(Math.abs(ys[i] - cy) + PAD, MIN_HALF)) / (d - zs[i])));
|
|
61
|
+
const near = Math.max(...zs), far = Math.min(...zs);
|
|
62
|
+
let d = Math.max(near + MIN_DEPTH, near + (near - far) / (DEPTH_RATIO - 1)), tanV = need(d);
|
|
63
|
+
if (tanV > Math.tan(rad(FOV / 2))) {
|
|
64
|
+
// Too close for the widest lens: back off at FOV until every point fits.
|
|
65
|
+
tanV = Math.tan(rad(FOV / 2));
|
|
66
|
+
d = Math.max(d, ...rel.map((q, i) => zs[i] + MARGIN * Math.max(Math.max(Math.abs(xs[i] - cx) + PAD, MIN_HALF) / aspect, Math.max(Math.abs(ys[i] - cy) + PAD, MIN_HALF)) / tanV));
|
|
67
|
+
} else tanV = Math.max(tanV, Math.tan(rad(MIN_FOV / 2)));
|
|
68
|
+
const eye = add(target, scale(back, d)), fovDeg = Math.round((2 * Math.atan(tanV) * 180 / Math.PI) * 100) / 100;
|
|
69
|
+
tanV = Math.tan(rad(fovDeg / 2));
|
|
70
|
+
const quaternion = quatFromMat([0, 1, 2].map(i => [right[i], up[i], back[i]]));
|
|
71
|
+
return { name, eye, target, quaternion, fovDeg, aspect, right, up, back, tanV, tanH: tanV * aspect };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One camera per view name over `frames`: framed on the focus refs' points at those frames
|
|
76
|
+
* (none, or none known there: every joint of the character) and every trail point. Answers
|
|
77
|
+
* [{ name, eye, target, quaternion [x,y,z,w], fovDeg, aspect, right, up, back, tanV, tanH }].
|
|
78
|
+
* Deterministic: the same compile, frames and arguments give the same cameras.
|
|
79
|
+
*/
|
|
80
|
+
export function viewCameras(out, { views, frames, focus = [], trails = [], aspect = 1 }) {
|
|
81
|
+
let points = frames.flatMap(f => focus.flatMap(ref => refPoints(out, f, ref)));
|
|
82
|
+
const body = frames.flatMap(f => jointsAt(out, f));
|
|
83
|
+
if (!points.length) points = body;
|
|
84
|
+
points = [...points, ...trails.flatMap(t => t.points.map(q => q.p))];
|
|
85
|
+
if (!points.length) return [];
|
|
86
|
+
const { left, forward } = facingOf(out, frames), hips = mean(frames.map(f => jointsAt(out, f)[out.ctx.sk.index.Hips]).filter(Boolean));
|
|
87
|
+
const side = focus.length && dot(sub(mean(points), hips), left) > 0.02 ? left : scale(left, -1);
|
|
88
|
+
return views.map(name => {
|
|
89
|
+
if (name === "top") return fit(name, points, UP, forward, aspect);
|
|
90
|
+
const e = rad(ELEVATION[name]), level = name === "front" ? forward : name === "side" ? side : norm(add(forward, side));
|
|
91
|
+
return fit(name, points, add(scale(level, Math.cos(e)), scale(UP, Math.sin(e))), UP, aspect);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A picture hides distance along its own line of sight, so a sheet holds two views at least APART
|
|
96
|
+
// degrees apart. Front and three-quarter (45 degrees apart) look along nearly one line: the
|
|
97
|
+
// guitarist's right hand 20 cm in front of the guitar (session df388a9b) showed in neither.
|
|
98
|
+
// LOOK is each view's nominal direction: azimuth from the character's forward, elevation.
|
|
99
|
+
const LOOK = { front: [0, 10], side: [90, 10], "three-quarter": [45, 25], top: [0, 90] }, APART = 60;
|
|
100
|
+
const lookDir = name => { const [a, e] = LOOK[name].map(rad); return [Math.cos(e) * Math.sin(a), Math.sin(e), Math.cos(e) * Math.cos(a)]; };
|
|
101
|
+
const apartDeg = (a, b) => Math.round(Math.acos(Math.min(1, dot(lookDir(a), lookDir(b)))) * 180 / Math.PI);
|
|
102
|
+
const spread = views => views.some((a, i) => views.slice(i + 1).some(b => apartDeg(a, b) >= APART));
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The views a sheet renders: the asked ones when two of them are APART degrees apart, else the
|
|
106
|
+
* asked ones plus the first of side, front and top that makes it so. Answers { views, added }:
|
|
107
|
+
* added is null, or { view, reason } for visualRefs.
|
|
108
|
+
*/
|
|
109
|
+
export function depthViews(views) {
|
|
110
|
+
if (spread(views)) return { views, added: null };
|
|
111
|
+
const view = ["side", "front", "top"].find(name => !views.includes(name) && spread([...views, name]));
|
|
112
|
+
const line = views.length > 1 ? `${views.join(" and ")} look along nearly one line (${apartDeg(views[0], views[1])} degrees apart)` : `${views[0]} alone looks along one line`;
|
|
113
|
+
return { views: [...views, view], added: { view, reason: `${line}, so a gap along it (toward or away from the camera) does not show; ${view} looks across it` } };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Pixel [x, y] of world point p in a width x height image of cam, y counted from the bottom row; null behind it. */
|
|
117
|
+
export function projectPoint(cam, p, width, height) {
|
|
118
|
+
const rel = sub(p, cam.eye), depth = -dot(rel, cam.back);
|
|
119
|
+
if (depth < 0.05) return null;
|
|
120
|
+
return [((dot(rel, cam.right) / (depth * cam.tanH) + 1) / 2) * width, ((dot(rel, cam.up) / (depth * cam.tanV) + 1) / 2) * height];
|
|
121
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// walkTo { to, arrive }: the engine picks the cadence instead of the agent sweeping period and step by
|
|
2
|
+
// hand. A use[i] walkTo with arrive (the frame the walk should end by, feet together: f0 + 16 + steps *
|
|
3
|
+
// period + 18) and none of steps, period, step, swing gets them planned here, before the skills expand:
|
|
4
|
+
// candidates every steps n and period p (frames per step) whose walk ends by arrive, the strides
|
|
5
|
+
// covering the distance from where the feet are at f0 to `to` (the first stride ~0.8 of
|
|
6
|
+
// the others, as walkTo's defaults), a stride at most STRIDE_MAX, the foot in the air
|
|
7
|
+
// SWING of the period;
|
|
8
|
+
// order a person's walk ratio first (stride x cadence: stride m * period frames ~ WALK_RATIO, the
|
|
9
|
+
// default 0.5 m every 20 frames), then using the time given (a walk that ends long before
|
|
10
|
+
// arrive stands still the rest), and no stroll slower than SLOW m/s;
|
|
11
|
+
// checked each candidate is compiled on a probe spec (the keys and skills up to the walk, the walk,
|
|
12
|
+
// nothing after it; no goals, checks or moving objects) and the first with no POP, REACH,
|
|
13
|
+
// SLIDE, FLOAT, FLIP or KNEE issue between f0 and its end wins (at most TRIALS compiles): the
|
|
14
|
+
// best on-time ones, then the top few again with a longer swing, a longer first stride or a
|
|
15
|
+
// lower lift (a quick walk often needs one of them), then the late ones, earliest first.
|
|
16
|
+
// When no walk ends by arrive (the distance is too far for the frames), the earliest clean one is
|
|
17
|
+
// taken and the note says the speed the request needs and the frame it ends. The spec keeps arrive (a
|
|
18
|
+
// recompile plans the same walk again, cached); observations.planned reads what was chosen.
|
|
19
|
+
import { expandUse } from "./skills/index.js";
|
|
20
|
+
import { stateAt } from "./compiler/walk.js";
|
|
21
|
+
|
|
22
|
+
const WALK_RATIO = 9.7, STRIDE_MIN = 0.25, STRIDE_MAX = 0.75, SWING = 0.88, SLOW = 0.5, TRIALS = 22, LATE = 4, FPS = 24;
|
|
23
|
+
const PERIODS = Array.from({ length: 16 }, (_, i) => 9 + i);
|
|
24
|
+
const PLANNED = ["steps", "period", "step", "swing"];
|
|
25
|
+
const WALK_KINDS = new Set(["POP", "REACH", "SLIDE", "FLOAT", "FLIP", "KNEE"]);
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
const r2 = x => Math.round(x * 100) / 100;
|
|
28
|
+
|
|
29
|
+
/** The walkTo uses that plan their cadence: [{ i, u }]. */
|
|
30
|
+
const toPlan = spec => (Array.isArray(spec?.use) ? spec.use : []).map((u, i) => ({ i, u }))
|
|
31
|
+
.filter(({ u }) => u?.skill === "walkTo" && Number.isInteger(u.args?.arrive) && Array.isArray(u.args?.to) && !PLANNED.some(k => u.args[k] !== undefined)
|
|
32
|
+
&& Object.values(u.args).every(v => !(v && typeof v === "object" && !Array.isArray(v))));
|
|
33
|
+
|
|
34
|
+
/** The strides of an n-step walk over d m: { first, step } or null when no stride fits. */
|
|
35
|
+
function strides(d, n) {
|
|
36
|
+
if (n === 2) return d >= STRIDE_MIN && d <= STRIDE_MAX ? { first: d, step: d } : null;
|
|
37
|
+
const first = Math.min(0.62, Math.max(0.3, (0.8 * d) / (n - 2 + 0.8))), step = (d - first) / (n - 2);
|
|
38
|
+
return step >= STRIDE_MIN && step <= STRIDE_MAX ? { first, step } : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Candidates for a walk of d m from f0, best first: on time (ending by arrive) by their score, then late ones by their end. */
|
|
42
|
+
function candidates(d, f0, arrive) {
|
|
43
|
+
const all = [];
|
|
44
|
+
for (const period of PERIODS) for (let n = 2; n <= 40; n++) {
|
|
45
|
+
const s = strides(d, n);
|
|
46
|
+
if (!s) continue;
|
|
47
|
+
const end = f0 + 16 + n * period + 18, v = (s.step * FPS) / period;
|
|
48
|
+
const ratio = Math.abs(Math.log((s.step * period) / WALK_RATIO)), idle = Math.max(0, arrive - end) / Math.max(1, arrive - f0);
|
|
49
|
+
all.push({ args: { steps: n, period, swing: Math.round(SWING * period), step: r2(s.step), firstStep: r2(s.first) }, end, v, late: end > arrive,
|
|
50
|
+
score: ratio + 1.5 * idle + Math.max(0, SLOW - v) * 4 });
|
|
51
|
+
}
|
|
52
|
+
const onTime = all.filter(c => !c.late).sort((a, b) => a.score - b.score), late = all.filter(c => c.late).sort((a, b) => a.end - b.end || a.score - b.score);
|
|
53
|
+
return { onTime, late };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A quick walk's other shapes: a longer swing, with a longer first stride, a lower lift, or both. */
|
|
57
|
+
function variants(c, d) {
|
|
58
|
+
const { steps: n, period } = c.args, swing = Math.round(0.8 * period), long = n > 2 && c.args.firstStep < 0.62 ? { firstStep: 0.62, step: r2((d - 0.62) / (n - 2)) } : null;
|
|
59
|
+
return [{}, long, { lift: 0.06 }, long && { ...long, lift: 0.06 }].filter(Boolean).map(x => ({ ...c, args: { ...c.args, swing, ...x } }))
|
|
60
|
+
.filter(v => v.args.step >= STRIDE_MIN && v.args.step <= STRIDE_MAX);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Issues in the walk's own frames (the kinds a cadence causes) of a compiled probe: a count. */
|
|
64
|
+
const walkIssues = (qa, f0, end) => qa.groups.filter(g => WALK_KINDS.has(g.kind) && g.last >= f0 && g.first <= end).reduce((n, g) => n + g.count, 0);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* planWalks(spec, { sk, scene, trial }) -> [{ i, args, note }] for every walkTo with arrive (applyWalkPlans puts
|
|
68
|
+
* the args in). trial(spec) -> QA of a compile of that spec (pipeline.js), or throws.
|
|
69
|
+
*/
|
|
70
|
+
export function planWalks(spec, { sk, scene, trial }) {
|
|
71
|
+
const plans = [];
|
|
72
|
+
for (const { i, u } of toPlan(spec)) {
|
|
73
|
+
const key = JSON.stringify([spec.keys, spec.use.slice(0, i + 1), spec.anchors, spec.props, spec.fits, spec.fix, scene?.objects ?? null]);
|
|
74
|
+
if (cache.has(key)) { plans.push(cache.get(key)); continue; }
|
|
75
|
+
const before = spec.use.slice(0, i).filter(x => x.f0 < u.f0);
|
|
76
|
+
let state, ends = [];
|
|
77
|
+
try {
|
|
78
|
+
const { spec: exp, used } = expandUse({ ...spec, frames: 20000, use: before }, { sk, scene, lenient: true });
|
|
79
|
+
state = stateAt(exp.keys ?? [], u.f0); ends = used.map(x => x.end ?? 0);
|
|
80
|
+
} catch { state = null; }
|
|
81
|
+
if (!state?.foot_l || !state?.foot_r) continue;
|
|
82
|
+
const c0 = [(state.foot_l[0] + state.foot_r[0]) / 2, (state.foot_l[2] + state.foot_r[2]) / 2], to = u.args.to, d = Math.hypot(to[0] - c0[0], to[to.length - 1] - c0[1]);
|
|
83
|
+
const { onTime, late } = candidates(d, u.f0, u.args.arrive);
|
|
84
|
+
const probe = c => {
|
|
85
|
+
const frames = Math.max(c.end, ...ends) + 1;
|
|
86
|
+
return { schema: spec.schema, fps: spec.fps ?? FPS, frames, keys: (spec.keys ?? []).filter(k => k.f < frames), use: [...before, { ...u, args: { ...u.args, ...c.args } }],
|
|
87
|
+
...(spec.anchors ? { anchors: spec.anchors } : {}), ...(spec.props ? { props: spec.props } : {}), ...(spec.fits ? { fits: spec.fits } : {}), ...(spec.fix !== undefined ? { fix: spec.fix } : {}) };
|
|
88
|
+
};
|
|
89
|
+
let chosen = null, tried = 0, best = null, onTried = 0;
|
|
90
|
+
const order = [...onTime.slice(0, 6), ...onTime.slice(0, 3).flatMap(c => variants(c, d))].slice(0, TRIALS - LATE);
|
|
91
|
+
for (const c of [...order, ...late.slice(0, TRIALS - order.length)]) {
|
|
92
|
+
let issues;
|
|
93
|
+
try { issues = walkIssues(trial(probe(c)), u.f0, c.end); } catch { issues = Infinity; }
|
|
94
|
+
tried++; onTried += c.late ? 0 : 1;
|
|
95
|
+
if (!best || issues < best.issues) best = { c, issues };
|
|
96
|
+
if (issues === 0) { chosen = c; break; }
|
|
97
|
+
}
|
|
98
|
+
chosen ??= best?.issues < Infinity ? best.c : onTime[0] ?? late[0];
|
|
99
|
+
if (!chosen) continue;
|
|
100
|
+
const a = chosen.args, A = u.args.arrive;
|
|
101
|
+
const what = `use[${i}] walkTo arrive f${A}: steps ${a.steps}, period ${a.period}, swing ${a.swing}, step ${a.step} m, firstStep ${a.firstStep} m${a.lift ? `, lift ${a.lift} m` : ""} (${r2(chosen.v)} m/s), ends f${chosen.end}`;
|
|
102
|
+
const why = onTime.length ? `the ${onTried} walks tried that end by f${A} pop or miss a foot`
|
|
103
|
+
: `even the quickest walk over ${r2(d)} m (a step every ${PERIODS[0]} frames, strides up to ${STRIDE_MAX} m, 34 frames to start and stop) ends f${late[0]?.end}`;
|
|
104
|
+
const late1 = chosen.late ? `; f${A} is too soon: ${why}; give arrive >= f${chosen.end} or a shorter distance` : "";
|
|
105
|
+
const dirty = best && best.c === chosen && best.issues > 0 ? `; no candidate walked clean (${best.issues} issues in its frames)` : "";
|
|
106
|
+
const plan = { i, args: a, end: chosen.end, note: `${what}${late1}${dirty} (${tried} checked)` };
|
|
107
|
+
if (cache.size >= 64) cache.delete(cache.keys().next().value);
|
|
108
|
+
cache.set(key, plan);
|
|
109
|
+
plans.push(plan);
|
|
110
|
+
}
|
|
111
|
+
return plans;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The spec with every planned walk's args filled in (arrive leaves them: walkTo ignores it); the spec itself when there is none. */
|
|
115
|
+
export function applyWalkPlans(spec, plans) {
|
|
116
|
+
if (!plans.length) return spec;
|
|
117
|
+
const use = spec.use.map((u, i) => { const p = plans.find(x => x.i === i); return p && u?.skill === "walkTo" ? { ...u, args: { ...u.args, ...p.args } } : u; });
|
|
118
|
+
return { ...spec, use };
|
|
119
|
+
}
|
package/src/object-catalog.jsx
CHANGED
|
@@ -5,6 +5,8 @@ import { EMPTY_KIND, OBJECT_MENU_ENTRIES } from "./scene-objects.js";
|
|
|
5
5
|
const GROUP_LABELS_KO = {
|
|
6
6
|
Primitives: "기본 도형",
|
|
7
7
|
"Set pieces": "세트 소품",
|
|
8
|
+
Lights: "조명",
|
|
9
|
+
Effects: "효과",
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
const OBJECT_LABELS_KO = {
|
|
@@ -18,6 +20,10 @@ const OBJECT_LABELS_KO = {
|
|
|
18
20
|
Chair: "의자",
|
|
19
21
|
Car: "자동차",
|
|
20
22
|
"Plane (aircraft)": "비행기",
|
|
23
|
+
"Point light": "점 조명",
|
|
24
|
+
"Spot light": "스포트 조명",
|
|
25
|
+
Light: "조명",
|
|
26
|
+
Effect: "효과",
|
|
21
27
|
};
|
|
22
28
|
|
|
23
29
|
function displayObjectGroupName(name) {
|
|
@@ -67,7 +73,7 @@ export function CatalogueEntries({ onPick }) {
|
|
|
67
73
|
<span className={`add-object-swatch ${entry.kind}`} style={{ background: entry.color }} aria-hidden="true" />
|
|
68
74
|
<span>{displayObjectLabel(entry.label)}</span>
|
|
69
75
|
{/* An empty has no size to quote: say what it is instead of "0 × 0 m". */}
|
|
70
|
-
<small>{entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
|
|
76
|
+
<small>{entry.light ? ko("practical light", "실제 조명") : entry.kind === EMPTY_KIND ? ko("node only", "노드 전용") : entry.kind === "effect" ? ko("smoke, fire, rain…", "연기, 불, 비…") : `${entry.footprint.width} × ${entry.footprint.depth} m`}</small>
|
|
71
77
|
</button>
|
|
72
78
|
))}
|
|
73
79
|
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inspector's object colours: the blockout palette, what a typed colour
|
|
3
|
+
* means, and the per-browser memory of hand-mixed tints. Pure, so the record
|
|
4
|
+
* normalizers and the light/glow fields read the same hex rules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Blockout grey: what a primitive is until someone says otherwise. */
|
|
8
|
+
export const GREY_BOX = "#c2c6c8";
|
|
9
|
+
|
|
10
|
+
/** Blockout greys first, then the clay accents, then the blocking chromatics,
|
|
11
|
+
* for re-tinting from the inspector. The greys lead because a blockout starts
|
|
12
|
+
* grey; the saturated four exist so a shot can say "the red ball, the blue
|
|
13
|
+
* box" at a glance — the inspector had no way to say that at all (#88). They
|
|
14
|
+
* are muted off pure RGB so they still read as clay under the set's lighting.
|
|
15
|
+
* Any other colour is authored through the inspector's free colour input. */
|
|
16
|
+
export const OBJECT_COLORS = [
|
|
17
|
+
"#e2e5e6",
|
|
18
|
+
GREY_BOX,
|
|
19
|
+
"#9aa1a5",
|
|
20
|
+
"#767d81",
|
|
21
|
+
"#d9b18c",
|
|
22
|
+
"#8fae9b",
|
|
23
|
+
"#d94a4a",
|
|
24
|
+
"#4a7bd9",
|
|
25
|
+
"#e2c04a",
|
|
26
|
+
"#4fa86a",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** How many hand-mixed colours the inspector remembers. Six is one palette
|
|
30
|
+
* row: enough to keep a scene's custom tints reachable, short enough that the
|
|
31
|
+
* popover never turns into a second, unruly palette. */
|
|
32
|
+
const RECENT_OBJECT_COLORS_MAX = 6;
|
|
33
|
+
/** Session memory of hand-mixed tints; versioned like every other key here. */
|
|
34
|
+
const RECENT_OBJECT_COLORS_KEY = "cozyclay.objectColors.recent";
|
|
35
|
+
|
|
36
|
+
const HEX_SHORT = /^#?([0-9a-f]{3})$/i;
|
|
37
|
+
const HEX_LONG = /^#?([0-9a-f]{6})$/i;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What a colour input typed by a human means, or null if it means nothing.
|
|
41
|
+
*
|
|
42
|
+
* The hex field accepts what people actually type — `#f36`, `ff3366`, upper
|
|
43
|
+
* case, stray spaces — and folds it to the one form the record stores, so a
|
|
44
|
+
* colour typed by hand and the same colour picked from the native swatch are
|
|
45
|
+
* byte-identical (and therefore compare equal against the palette). Anything
|
|
46
|
+
* else returns null rather than throwing or guessing: a half-typed `#12` is a
|
|
47
|
+
* keystroke on the way somewhere, not an instruction to repaint the prop.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeObjectColor(value) {
|
|
50
|
+
if (typeof value !== "string") return null;
|
|
51
|
+
const text = value.trim();
|
|
52
|
+
const short = HEX_SHORT.exec(text);
|
|
53
|
+
if (short) return `#${short[1].toLowerCase().replace(/./g, (digit) => digit + digit)}`;
|
|
54
|
+
const long = HEX_LONG.exec(text);
|
|
55
|
+
return long ? `#${long[1].toLowerCase()}` : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The recent-colours list after mixing `hex`: newest first, no duplicates,
|
|
60
|
+
* capped, presets excluded.
|
|
61
|
+
*
|
|
62
|
+
* Pure so the list can be proved without a browser, and so a corrupt stored
|
|
63
|
+
* list is repaired on the way through instead of painting a swatch with junk.
|
|
64
|
+
* Presets are skipped because they are already one row up — remembering them
|
|
65
|
+
* would push the author's own colours off the end with colours they never
|
|
66
|
+
* mixed. Returns the SAME array when nothing changes, so a re-pick of an
|
|
67
|
+
* unchanged colour cannot trigger a storage write or a re-render.
|
|
68
|
+
*/
|
|
69
|
+
export function rememberObjectColor(list, hex) {
|
|
70
|
+
const clean = Array.isArray(list)
|
|
71
|
+
? list.map(normalizeObjectColor).filter((color, index, all) => color && all.indexOf(color) === index && !OBJECT_COLORS.includes(color))
|
|
72
|
+
: [];
|
|
73
|
+
const color = normalizeObjectColor(hex);
|
|
74
|
+
const next = color && !OBJECT_COLORS.includes(color) ? [color, ...clean.filter((entry) => entry !== color)] : clean;
|
|
75
|
+
const capped = next.slice(0, RECENT_OBJECT_COLORS_MAX);
|
|
76
|
+
const unchanged = Array.isArray(list) && capped.length === list.length && capped.every((entry, index) => entry === list[index]);
|
|
77
|
+
return unchanged ? list : capped;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Recent colours as last left by this browser; storage can be unavailable
|
|
81
|
+
* (private mode), and a corrupt body must not take the inspector down with
|
|
82
|
+
* it — either way the palette simply starts without a memory. */
|
|
83
|
+
export function readStoredObjectColors(storage) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(storage?.getItem(RECENT_OBJECT_COLORS_KEY) ?? "[]");
|
|
86
|
+
return rememberObjectColor(Array.isArray(parsed) ? parsed : [], null);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function writeStoredObjectColors(storage, list) {
|
|
93
|
+
try {
|
|
94
|
+
storage?.setItem(RECENT_OBJECT_COLORS_KEY, JSON.stringify(list));
|
|
95
|
+
} catch {
|
|
96
|
+
// Storage can be unavailable (private mode); the colours simply stop persisting.
|
|
97
|
+
}
|
|
98
|
+
}
|