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
package/src/studio-elements.js
CHANGED
|
@@ -31,15 +31,13 @@ const entries = [
|
|
|
31
31
|
{ path: "character.identityImage", type: "image", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", note: "data:image only" },
|
|
32
32
|
{ path: "character.pose", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
33
33
|
{ path: "character.waypoints", type: "array", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry", actions: ["character.addWaypoint", "character.moveWaypoint", "character.removeWaypoint", "character.clearWaypoints"], note: "root path pins, addressed by frame" },
|
|
34
|
-
|
|
35
|
-
{ path: "character.motionRef.url", type: "string", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
36
|
-
{ path: "character.motionRef.motionId", type: "id", persisted: true, undoDomain: "cast", normalizer: "createCharacterEntry" },
|
|
34
|
+
|
|
37
35
|
{ path: "character.sessionMotion", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", note: "dropped by createCharacterEntry" },
|
|
38
36
|
{ path: "character.ikKeys", type: "array", persisted: false, undoDomain: "cast", normalizer: "createCharacterEntry", actions: ["character.setIkKey", "character.removeIkKey", "character.clearIkKeys"], note: "dropped by createCharacterEntry; keys travel as JSON quaternions/positions" },
|
|
39
|
-
{ path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane", "empty"] },
|
|
37
|
+
{ path: "object.renderer", type: "enum", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", enum: ["cube", "sphere", "capsule", "cylinder", "cone", "plane", "chair", "car", "small-plane", "empty", "light", "effect"], note: "light is made from catalogue kind point-light or spot-light; effect (smoke, fire, rain, trail...) from kind effect" },
|
|
40
38
|
{ path: "object.position", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -240, y: 0, z: -240 }, max: { x: 240, y: 240, z: 240 }, note: "y is the object's base: y=0 rests on the floor; height/supportY rise from it" },
|
|
41
|
-
{ path: "object.rotation", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -180, y: -180, z: -180 }, max: { x: 180, y: 180, z: 180 }, note: "rot/rotX/rotZ degrees, wrapped at the upper bound" },
|
|
42
|
-
{ path: "object.scale", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: 0.
|
|
39
|
+
{ path: "object.rotation", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: -180, y: -180, z: -180 }, max: { x: 180, y: 180, z: 180 }, angle: true, note: "rot/rotX/rotZ degrees, wrapped at the upper bound" },
|
|
40
|
+
{ path: "object.scale", type: "vec3", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: { x: 0.01, y: 0.01, z: 0.01 }, max: { x: 100, y: 100, z: 100 } },
|
|
43
41
|
{ path: "object.name", type: "string", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
44
42
|
{ path: "object.color", type: "color", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
45
43
|
{ path: "object.opacity", type: "number", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", min: 0.05, max: 1, note: "1 is solid; lower is see-through, e.g. 0.3 for clear glass" },
|
|
@@ -48,6 +46,9 @@ const entries = [
|
|
|
48
46
|
{ path: "object.path", type: "array", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject" },
|
|
49
47
|
{ path: "object.remove", type: "boolean", persisted: true, undoDomain: "objects", normalizer: null, note: "lifecycle operation, not a document field" },
|
|
50
48
|
{ path: "object.cutout", type: "image", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", actions: ["asset.import"], note: "assetId-backed cutout record; imported pictures and models are placed by asset.import" },
|
|
49
|
+
{ path: "object.light", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "renderer light only. { type point|spot, color, intensity 0..1000 candela (lighter 0.45, candle 0.3, desk lamp 5, street lamp 40), range m (0 none), angle 1..89 deg spot half-angle, softness 0..1, shadow (first 2 lights only; inside a solid record it is blocked: receipt LIGHT_BLOCKED), flicker 0..1 (lighter 0.6), aim {x,y,z} world point or null = local -Y }, e.g. { intensity: 40, aim: { x: 0, y: 0, z: 1 } }; a take level track scales it" },
|
|
50
|
+
{ path: "object.glow", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "any record: self-lit colour plus additive halo, same under any stage light; casts no shadow, so a light inside a glowing bulb shines out. { color, intensity 0..20, halo 0..4 x the record's radius }, e.g. ember { color: \"#ff5a1f\", intensity: 6, halo: 2 }; null removes; a take level track scales it" },
|
|
51
|
+
{ path: "object.effect", type: "object", persisted: true, undoDomain: "objects", normalizer: "normalizeSceneObject", note: "renderer effect only (object.add kind \"effect\"). { preset: smoke|exhale|steam|fire|sparks|dust|rain|snow|trail, rate particles/s, life s, size m start diameter, grow end/start size, speed m/s along the record's local +Y (rotX 90 aims +Z), spread 0..1 cone, gravity m/s2 (+ falls, - rises), wind {x,y,z} m/s, color, opacity 0..1, additive (emissive: fire, sparks), seed int, area {x,y,z} m birth box (rain/snow/dust fall through it from its top), width m trail ribbon across local Y }. A new preset loads its defaults and fields in the same set override them, e.g. { preset: \"fire\", rate: 40 }; the record stores the merged numbers. Particles are born where the record is at each frame (route, parent, take carry), only while it and its parents are shown, at rate x the take level. To emit from the body (a breath at the mouth), a motion spec entry objects.<id> = { follow: \"landmark:mouth\", aim: \"face\" } moves it there every frame. Measure with inspect_studio { scope: \"entities\", ids }: effect { frame (the playhead), alive, emitter [x,y,z], bounds { min, max } m }" },
|
|
51
52
|
{ path: "stage.keyLight.x", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -30, max: 30 },
|
|
52
53
|
{ path: "stage.keyLight.y", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0.5, max: 30 },
|
|
53
54
|
{ path: "stage.keyLight.z", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -30, max: 30 },
|
|
@@ -60,6 +61,25 @@ const entries = [
|
|
|
60
61
|
{ path: "stage.camera", documentPath: "shotAspect", type: "enum", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["16:9", "2.39:1", "9:16", "1:1", "4:3", "12:7", "fal 480P"], note: "output aspect ratio" },
|
|
61
62
|
{ path: "stage.cameraPresetId", type: "id", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage" },
|
|
62
63
|
{ path: "stage.sensorId", type: "enum", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["super16", "super35", "fullFrame", "65mm"] },
|
|
64
|
+
// The stage look (src/stage-lighting.js). stage.applyLighting writes all of it from a recipe; each field stays tweakable.
|
|
65
|
+
{ path: "stage.keyLight.color", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "key light colour #rrggbb, overrides warmth, e.g. \"#9db6e6\" cool moonlight or \"#ffad5c\" low sun; null = white tinted by warmth" },
|
|
66
|
+
{ path: "stage.lighting.preset", type: "enum", persisted: true, readOnly: true, undoDomain: "stage", normalizer: "createSceneStage", enum: ["studio", "day", "overcast", "golden", "dusk", "night", "interior", "stage"], note: "label of the last recipe stage.applyLighting applied; studio is the neutral grey default" },
|
|
67
|
+
{ path: "stage.lighting.sky", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "background and fog colour at the horizon, e.g. \"#0b1426\" night; null = the UI theme's studio grey" },
|
|
68
|
+
{ path: "stage.lighting.skyTop", type: "color", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "sky colour overhead: shot camera and exports grade from sky at the horizon to this, e.g. \"#020408\"; null = flat sky" },
|
|
69
|
+
{ path: "stage.lighting.ambient", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 3, note: "multiplier on the hemisphere and ambient light; 1 = studio, 0.6 = night, 0 = black stage" },
|
|
70
|
+
{ path: "stage.lighting.hemiSky", type: "color", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "hemisphere light from above, e.g. \"#34507f\" cool night sky; studio \"#ffffff\"" },
|
|
71
|
+
{ path: "stage.lighting.hemiGround", type: "color", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "hemisphere bounce from below, e.g. \"#6e5136\" warm ground; studio \"#5a5d64\"" },
|
|
72
|
+
{ path: "stage.lighting.fill", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 3, note: "multiplier on the fixed fill and rim lights; 1 = studio, 0 = key only" },
|
|
73
|
+
{ path: "stage.lighting.fog", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "haze 0..1 fading the set into the sky colour: 0 = far horizon only, 0.45 = from ~18 m to ~38 m, 1 = from 2 m to 14 m" },
|
|
74
|
+
{ path: "stage.lighting.exposure", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0.2, max: 3, note: "camera exposure multiplier on lit surfaces; 1 = studio, e.g. 1.4 to lift a dark frame" },
|
|
75
|
+
{ path: "stage.lighting.shadowSoftness", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "key shadow penumbra; 0 = crisp (studio), 0.75 = soft moonlight, 1 = overcast" },
|
|
76
|
+
{ path: "stage.lighting.envLight", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 2, note: "sky environment light on every surface: a map from sky, skyTop, hemiGround and a glow toward the key (sun or moon) tints and fills clay from all sides; it takes over the flat hemisphere as it rises. 0 = hemisphere only (studio), 1 = the map alone at the same brightness (every other recipe), e.g. 1.4 for a brighter fill" },
|
|
77
|
+
{ path: "stage.lighting.ao", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 1, note: "ambient occlusion strength: contact darkening within ~0.5 m where a body meets the floor or a prop and in creases (armpits, the back of a squat's knees), in the viewport and every export. 0 = off (studio), 0.8 = most recipes, 1 = overcast" },
|
|
78
|
+
// An HDR environment map (src/stage-hdr.js): the light and, with hdrBackground, the backdrop.
|
|
79
|
+
{ path: "stage.lighting.hdr", type: "id", nullable: true, persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "HDR environment map that replaces the sky environment light: a built-in (\"builtin:night-street\", \"builtin:overcast-field\", \"builtin:studio-softbox\") or a file the user loaded (\"hdr-<32 hex>\"); inspect_studio { scope: \"catalogue\" } environments lists them with each map's brightest direction. It lights the set only while envLight > 0 (its light = map x hdrIntensity x envLight; studio has envLight 0, so set envLight 1). stage.applyLighting clears it unless keep has \"lighting.hdr\". null = the look's own sky" },
|
|
80
|
+
{ path: "stage.lighting.hdrRotation", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: -180, max: 180, angle: true, note: "degrees the HDR map turns about the vertical axis, light and backdrop together: +90 carries a light at +x (screen right from the default camera) to -z (behind the subject), so a brightest [x, y, z] lands at [x cos r + z sin r, y, z cos r - x sin r]; wrapped at the upper bound" },
|
|
81
|
+
{ path: "stage.lighting.hdrIntensity", type: "number", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", min: 0, max: 4, note: "multiplier on the HDR map's radiance in its light and its backdrop; 1 = the file as authored, e.g. 0.5 to calm a bright daylight map" },
|
|
82
|
+
{ path: "stage.lighting.hdrBackground", type: "boolean", persisted: true, undoDomain: "stage", normalizer: "createSceneStage", note: "true = the HDR map is the backdrop behind the set (the floor fades into its horizon), in the viewport, shot and every export; false = keep the look's sky as the backdrop and use the map only as light" },
|
|
63
83
|
{ path: "shot.crud", type: "array", persisted: true, undoDomain: "shot", normalizer: null, actions: ["shot.create", "shot.split", "shot.duplicate", "shot.remove", "shot.setRange", "shot.reorder"], note: "create/split/duplicate/reorder/remove/range through the shared action registry" },
|
|
64
84
|
{ path: "shot.cameraKeys", type: "array", persisted: true, undoDomain: "shot", normalizer: null, frameMin: 0 },
|
|
65
85
|
{ path: "shot.cameraRail", type: "array", persisted: true, undoDomain: "shot", normalizer: "repairCamera", actions: ["shot.setCameraRail", "shot.clearCameraRail"], note: "rail points; crane/dolly timing follow the shot's camera block" },
|
|
@@ -74,7 +94,7 @@ const entries = [
|
|
|
74
94
|
{ path: "selection", type: "id", persisted: false, undoDomain: null, normalizer: null },
|
|
75
95
|
{ path: "timeline", type: "number", persisted: false, undoDomain: null, normalizer: null },
|
|
76
96
|
{ path: "view.mode", type: "enum", persisted: false, undoDomain: null, normalizer: null, enum: ["scene", "pose", "camera", "motion"] },
|
|
77
|
-
{ path: "view.partColoursGuideModeInset", type: "array", persisted: false, undoDomain: null, normalizer: null, actions: ["view.setPartColours", "view.setGuideMode", "view.setInset"], note: "partColours/guideMode/inset; viewer settings, not document fields" },
|
|
97
|
+
{ path: "view.partColoursGuideModeInset", type: "array", persisted: false, undoDomain: null, normalizer: null, actions: ["view.setPartColours", "view.setGuideMode", "view.setInset", "view.setGhosts"], note: "partColours/guideMode/inset/ghosts; viewer settings, not document fields" },
|
|
78
98
|
{ path: "read.sceneDescription", type: "string", persisted: false, undoDomain: null, normalizer: null },
|
|
79
99
|
{ path: "read.captureFrame", type: "image", persisted: false, undoDomain: null, normalizer: null },
|
|
80
100
|
{ path: "undo", type: "boolean", persisted: false, undoDomain: null, normalizer: null },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The world box an object row reports, in inspect_studio entities, the per-turn context and an
|
|
2
|
+
// action readback alike: a body's own box at the playhead (entityWorldBounds, metres, 4 decimals);
|
|
3
|
+
// for a node with no body (an empty or a light) the union of every body under it, so a group
|
|
4
|
+
// built from parts shows where the whole thing ends; null when nothing there can be measured.
|
|
5
|
+
import { entityWorldBounds } from "./studio-agent-commands.js";
|
|
6
|
+
import { descendantsOf, isEmptyObject } from "./scene-objects.js";
|
|
7
|
+
|
|
8
|
+
const AXES = ["x", "y", "z"];
|
|
9
|
+
|
|
10
|
+
/** World AABB {min,max} of scene object `object` in refreshed Studio state `s`, or null. */
|
|
11
|
+
export function objectRowBounds(object, s, ports) {
|
|
12
|
+
const state = { ...s, frame: s.view.frame }, measure = o => entityWorldBounds(o, state, { bounds: ports.bounds });
|
|
13
|
+
if (!isEmptyObject(object)) return measure(object);
|
|
14
|
+
const boxes = descendantsOf(s.objects, object.id).filter(o => !isEmptyObject(o)).map(measure).filter(Boolean);
|
|
15
|
+
if (!boxes.length) return null;
|
|
16
|
+
return { min: Object.fromEntries(AXES.map(a => [a, Math.min(...boxes.map(b => b.min[a]))])),
|
|
17
|
+
max: Object.fromEntries(AXES.map(a => [a, Math.max(...boxes.map(b => b.max[a]))])) };
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The error codes a Studio receipt or failure may carry. Shared by the browser
|
|
2
|
+
// and sidecar; no imports.
|
|
3
|
+
export const STUDIO_ERROR_CODES = Object.freeze([
|
|
4
|
+
"INVALID_ARGUMENT", "INVALID_CONTEXT", "INVALID_IDENTITY", "INVALID_TURN_ID", "INVALID_SESSION_ID", "INVALID_RECEIPT", "INVALID_RANGE", "INVALID_REQUEST",
|
|
5
|
+
"UNKNOWN_TOOL", "UNKNOWN_VARIANT", "DUPLICATE_NAME", "CONTEXT_LIMIT", "CONTEXT_TOO_LARGE", "AMBIGUOUS_TARGET", "AMBIGUOUS_BASIS", "TARGET_NOT_READY", "TARGET_BUSY",
|
|
6
|
+
"STALE_TARGET", "STALE_SCENE", "STALE_ENVIRONMENT", "STALE_CURSOR", "CAPABILITY_MISSING", "LIVE_HUB_UNAVAILABLE", "AUTH_REQUIRED", "GENERATION_LIMIT", "RATE_LIMITED", "BACKEND_UNAVAILABLE",
|
|
7
|
+
"VERIFICATION_FAILED", "REPAIR_REGRESSED", "CANCELLED", "UNCERTAIN_APPLY", "UNDO_CONFLICT", "CONFIRMATION_REQUIRED", "TIMEOUT", "UNDO_EXPIRED",
|
|
8
|
+
]);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// What the export camera shows of a take (verify_result motion, exportFraming): at each sampled frame
|
|
2
|
+
// the camera the export renders there (ports.sampleStateAt: the shot's camera where a shot covers the
|
|
3
|
+
// frame, else the Studio camera as it stands), and through it the character's joints (as motion.sample
|
|
4
|
+
// reads them) and the take's objects (moved by the take's tracks, object-travel.js), projected as
|
|
5
|
+
// frame_shot projects a subject (studio-agent-commands.js projectSubject). Measurements, not verdicts:
|
|
6
|
+
// how much of the character and of each object is inside the frame, and over which sampled frames a
|
|
7
|
+
// part (head, hands, feet) or an object (the outline of each of its bodies) is cut or out. Frames are
|
|
8
|
+
// absolute timeline frames; a span joins consecutive samples ([first, last] sampled frame).
|
|
9
|
+
import { Euler, Vector3 } from "three";
|
|
10
|
+
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
11
|
+
import { projectSubject, entityWorldBounds } from "./studio-agent-commands.js";
|
|
12
|
+
import { focalMmToFov, SENSOR_FORMATS } from "./shot.js";
|
|
13
|
+
import { shotAtFrame } from "./cuts.js";
|
|
14
|
+
import { sceneObjectsAt } from "./object-travel.js";
|
|
15
|
+
import { descendantsOf, isEmptyObject } from "./scene-objects.js";
|
|
16
|
+
import { skeletonFor } from "./motion3/rigs/index.js";
|
|
17
|
+
import { fk } from "./motion3/compiler/skeleton.js";
|
|
18
|
+
import { add, apply, ry } from "./motion3/compiler/math.js";
|
|
19
|
+
|
|
20
|
+
const SAMPLES = 24, TAKE_SCHEMA = "cozyclay.take.v3", BARE_OBJECT = /^object:(.+?)(\.(?:center|top|base))?$/s;
|
|
21
|
+
const PARTS = Object.freeze({ head: ["Head", "HeadTop_End"], hand_l: ["LeftHand", "LeftHandMiddle4", "LeftHandIndex4"], hand_r: ["RightHand", "RightHandMiddle4", "RightHandIndex4"],
|
|
22
|
+
foot_l: ["LeftFoot", "LeftToe_End"], foot_r: ["RightFoot", "RightToe_End"] });
|
|
23
|
+
const r2 = v => Math.round(v * 100) / 100;
|
|
24
|
+
|
|
25
|
+
/** Up to n frames spread evenly over [lo, hi], both ends included. */
|
|
26
|
+
export function exportSampleFrames(lo, hi, n = SAMPLES) {
|
|
27
|
+
if (hi <= lo) return [lo];
|
|
28
|
+
const count = Math.min(n, hi - lo + 1);
|
|
29
|
+
return [...new Set(Array.from({ length: count }, (_, i) => Math.round(lo + (i * (hi - lo)) / (count - 1))))];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** [[first, last], ...] of the sampled frames whose flag is set, consecutive samples joined. */
|
|
33
|
+
export function sampleSpans(frames, flags) {
|
|
34
|
+
const spans = [];
|
|
35
|
+
frames.forEach((frame, i) => {
|
|
36
|
+
if (!flags[i]) return;
|
|
37
|
+
if (i > 0 && flags[i - 1]) spans.at(-1)[1] = frame; else spans.push([frame, frame]);
|
|
38
|
+
});
|
|
39
|
+
return spans;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** World joint positions of a v3 take at timeline frame f, in take bone order (motion.sample's reading). */
|
|
43
|
+
function jointsAt(sk, take, f) {
|
|
44
|
+
const B = take.bones.length, ff = Math.max(0, Math.min(take.frameCount - 1, Math.round(f)));
|
|
45
|
+
const quats = Array.from({ length: B }, (_, b) => Array.from(take.rot.subarray((ff * B + b) * 4, (ff * B + b) * 4 + 4)));
|
|
46
|
+
const state = fk(sk, quats, Array.from(take.hips.subarray(ff * 3, ff * 3 + 3)));
|
|
47
|
+
const yaw = ry(take.placement?.yaw ?? 0), at = take.placement?.position ?? [0, 0, 0];
|
|
48
|
+
return state.P.map(p => add(at, apply(yaw, p)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Scene object ids a spec names: spec.objects keys, object:<id> refs and any string value that is an object id. */
|
|
52
|
+
export function takeObjectIds(spec, objects) {
|
|
53
|
+
const known = new Set(objects.map(o => o.id)), ids = new Set(Object.keys(spec?.objects ?? {}).filter(id => known.has(id)));
|
|
54
|
+
const walk = value => {
|
|
55
|
+
if (typeof value === "string") { const id = BARE_OBJECT.exec(value)?.[1] ?? value; if (known.has(id)) ids.add(id); }
|
|
56
|
+
else if (Array.isArray(value)) value.forEach(walk);
|
|
57
|
+
else if (value && typeof value === "object") Object.values(value).forEach(walk);
|
|
58
|
+
};
|
|
59
|
+
walk(spec);
|
|
60
|
+
return [...ids];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** How a set of world points lands in the camera: the projection and how much of its screen box is inside the frame. */
|
|
64
|
+
function landing(points, view) {
|
|
65
|
+
const forward = new Vector3().subVectors(view.lookAt, view.position);
|
|
66
|
+
const behind = points.filter(p => p.clone().sub(view.position).dot(forward) <= 0).length;
|
|
67
|
+
if (behind === points.length) return { behind: true, cut: true, fraction: 0, screenFraction: 0 };
|
|
68
|
+
const { screenBounds: b, checks } = projectSubject(points, view.camera, view.fov, view.aspect);
|
|
69
|
+
if (behind) return { behind: true, cut: true, fraction: null, screenFraction: checks.screenFraction };
|
|
70
|
+
// the share of each screen axis inside [-1, 1]; a box flat along an axis is in or out whole
|
|
71
|
+
const share = ({ min, max }) => max - min > 1e-6 ? Math.max(0, Math.min(1, max) - Math.max(-1, min)) / (max - min) : Math.abs(min) <= 1 ? 1 : 0;
|
|
72
|
+
return { behind: false, cut: checks.clipped, fraction: share(b.x) * share(b.y), screenFraction: checks.screenFraction };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const corners = box => [box.min.x, box.max.x].flatMap(x => [box.min.y, box.max.y].flatMap(y => [box.min.z, box.max.z].map(z => new Vector3(x, y, z))));
|
|
76
|
+
const RING = Array.from({ length: 8 }, (_, i) => [Math.cos(i * Math.PI / 4), Math.sin(i * Math.PI / 4)]);
|
|
77
|
+
// A body's outline in its own frame (base pivot, local +Y up, 1 m primitives scaled): rims for the round
|
|
78
|
+
// primitives, so a disc seen face on is not measured by the corners of its box; box corners otherwise.
|
|
79
|
+
const OUTLINES = { cylinder: [[0, 1], [1, 1]], cone: [[0, 1], [1, 0]], sphere: [[0, 0], [0.5, 1], [1, 0]], capsule: [[0, 0], [0.25, 1], [0.75, 1], [1, 0]] };
|
|
80
|
+
/** World points outlining a body as drawn at its posed record: the same placement geometry() uses for a box. */
|
|
81
|
+
function bodyPoints(object, state, ports) {
|
|
82
|
+
const outline = OUTLINES[object.renderer];
|
|
83
|
+
if (object.attach || object.path || !object.footprint) { const box = entityWorldBounds(object, state, { bounds: ports.bounds }); return box ? corners(box) : []; }
|
|
84
|
+
const w = object.footprint.width * object.scaleX / 2, d = object.footprint.depth * object.scaleZ / 2, h = object.height * object.scaleY;
|
|
85
|
+
const local = outline ? outline.flatMap(([y, r]) => r ? RING.map(([c, s]) => [w * r * c, y * h, d * r * s]) : [[0, y * h, 0]])
|
|
86
|
+
: [-w, w].flatMap(x => [0, h].flatMap(y => [-d, d].map(z => [x, y, z])));
|
|
87
|
+
const rotation = new Euler((object.rotX ?? 0) * Math.PI / 180, (object.rot ?? 0) * Math.PI / 180, (object.rotZ ?? 0) * Math.PI / 180, "XYZ");
|
|
88
|
+
return local.map(([x, y, z]) => new Vector3(x, y, z).applyEuler(rotation).add(new Vector3(object.x, object.y ?? 0, object.z)));
|
|
89
|
+
}
|
|
90
|
+
const lowest = (rows, frames) => rows.reduce((best, row, i) => row !== null && (best === null || row < best[0]) ? [r2(row), frames[i]] : best, null);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* exportFraming for one character's installed v3 take. scene: the binding's command state (objects,
|
|
94
|
+
* characters, camera, filmback, shotDocument); ports: { sampleStateAt, bounds }; range: verify_result's
|
|
95
|
+
* range ("whole_clip" or { startFrame, endFrameExclusive }).
|
|
96
|
+
*/
|
|
97
|
+
export function exportFraming({ character, take, scene, ports, range }) {
|
|
98
|
+
const sensorId = scene.filmback?.sensorId, aspect = scene.filmback?.aspectRatio;
|
|
99
|
+
if (!SENSOR_FORMATS[sensorId] || !(aspect > 0)) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera and filmback must be available.");
|
|
100
|
+
if (typeof ports.sampleStateAt !== "function") throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot sample the export camera at timeline frames.");
|
|
101
|
+
const sk = skeletonFor(take.rig ?? character.model), last = take.frameCount - 1;
|
|
102
|
+
const lo = range && range !== "whole_clip" ? Math.max(0, range.startFrame) : 0, hi = range && range !== "whole_clip" ? Math.min(last, range.endFrameExclusive - 1) : last;
|
|
103
|
+
const frames = exportSampleFrames(lo, hi), shots = scene.shotDocument?.shots ?? [];
|
|
104
|
+
const tracks = new Map(Object.entries(take.objects ?? {}).map(([id, track]) => [id, { frameCount: take.frameCount, characterId: character.id, ...track }]));
|
|
105
|
+
const objectIds = takeObjectIds(take.spec, scene.objects), partIndex = Object.fromEntries(Object.entries(PARTS).map(([part, bones]) => [part, bones.map(b => sk.index[b]).filter(i => i !== undefined)]));
|
|
106
|
+
const shotIds = new Set(), sources = new Set(), body = [], parts = Object.fromEntries(Object.keys(PARTS).map(p => [p, []])), objects = objectIds.map(() => []);
|
|
107
|
+
for (const frame of frames) {
|
|
108
|
+
const sampled = ports.sampleStateAt(frame, scene), camera = sampled?.camera ?? scene.camera;
|
|
109
|
+
if (!camera) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera must be available.");
|
|
110
|
+
const shot = shotAtFrame(shots, frame);
|
|
111
|
+
if (shot) shotIds.add(shot.id);
|
|
112
|
+
sources.add(shot ? "shot" : "studio");
|
|
113
|
+
const view = { camera, position: new Vector3(camera.position.x, camera.position.y, camera.position.z), lookAt: new Vector3(camera.lookAt.x, camera.lookAt.y, camera.lookAt.z),
|
|
114
|
+
fov: focalMmToFov(camera.focalMm, sensorId, aspect), aspect };
|
|
115
|
+
const joints = jointsAt(sk, take, frame).map(([x, y, z]) => new Vector3(x, y, z));
|
|
116
|
+
body.push(landing(joints, view));
|
|
117
|
+
for (const [part, index] of Object.entries(partIndex)) parts[part].push(landing(index.map(i => joints[i]), view));
|
|
118
|
+
const posed = sceneObjectsAt(scene.objects, frame, { frameCount: take.frameCount, fps: 24, objectTracks: tracks }), state = { ...scene, objects: posed, frame };
|
|
119
|
+
objectIds.forEach((id, k) => {
|
|
120
|
+
const object = posed.find(o => o.id === id), bodies = object && (isEmptyObject(object) ? descendantsOf(posed, id).filter(o => !isEmptyObject(o)) : [object]);
|
|
121
|
+
const points = (bodies ?? []).filter(o => !o.hidden).flatMap(o => bodyPoints(o, state, ports));
|
|
122
|
+
objects[k].push(points.length ? landing(points, view) : null);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const spans = (rows, key) => sampleSpans(frames, rows.map(row => !!row?.[key]));
|
|
126
|
+
const cut = Object.fromEntries(Object.entries(parts).map(([part, rows]) => [part, spans(rows, "cut")]).filter(([, s]) => s.length));
|
|
127
|
+
const fractions = body.map(row => row.screenFraction);
|
|
128
|
+
return {
|
|
129
|
+
coverage: "sampled-export-camera-joints-and-object-outlines",
|
|
130
|
+
camera: sources.size === 1 ? [...sources][0] : "mixed", shotIds: [...shotIds], sampledFrames: frames,
|
|
131
|
+
character: { id: character.id, screenFraction: [r2(Math.min(...fractions)), r2(Math.max(...fractions))], inFrame: lowest(body.map(row => row.fraction), frames),
|
|
132
|
+
...(spans(body, "behind").length ? { behindCamera: spans(body, "behind") } : {}), ...(Object.keys(cut).length ? { cut } : {}) },
|
|
133
|
+
objects: objectIds.map((id, k) => {
|
|
134
|
+
const rows = objects[k], out = sampleSpans(frames, rows.map(row => row !== null && row.fraction === 0));
|
|
135
|
+
const partly = sampleSpans(frames, rows.map(row => row !== null && row.cut && row.fraction !== 0));
|
|
136
|
+
return { id, name: scene.objects.find(o => o.id === id)?.name ?? id, inFrame: lowest(rows.map(row => row?.fraction ?? null), frames), ...(out.length ? { out } : {}), ...(partly.length ? { cut: partly } : {}) };
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** verify_result motion: result.exportFraming for every subject character with an installed v3 take (one: the row itself). */
|
|
142
|
+
export function exportFramingInto(result, { args, s, receipt, scene, ports }) {
|
|
143
|
+
const rows = [];
|
|
144
|
+
for (const id of (args.targets ?? receipt?.affectedIds ?? []).filter(id => s.characters.some(c => c.id === id))) {
|
|
145
|
+
const take = s.targets.get(id)?.motion, character = s.characters.find(c => c.id === id);
|
|
146
|
+
if (take?.schema !== TAKE_SCHEMA || !take.rot || !take.hips) continue;
|
|
147
|
+
// what could not be measured says why in its own row, beside the motion check's reasons
|
|
148
|
+
try { rows.push(exportFraming({ character, take, scene, ports, range: args.range })); }
|
|
149
|
+
catch (error) { rows.push({ character: { id }, reason: String(error?.message ?? error) }); }
|
|
150
|
+
}
|
|
151
|
+
if (rows.length) result.exportFraming = rows.length === 1 ? rows[0] : rows;
|
|
152
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// verify_result checks ["lighting"]: how the shot frame is exposed, measured from
|
|
2
|
+
// the pixels the export path renders (the same frame capture_frame and the
|
|
3
|
+
// contact sheets show), not from the light settings. The agent reads the
|
|
4
|
+
// numbers; it does not have to judge darkness from a thumbnail.
|
|
5
|
+
import { PerspectiveCamera, Vector3 } from "three";
|
|
6
|
+
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
7
|
+
import { entityWorldBounds } from "./studio-agent-commands.js";
|
|
8
|
+
import { focalMmToFov } from "./shot.js";
|
|
9
|
+
import { normalizeLighting } from "./stage-lighting.js";
|
|
10
|
+
import { readyHdrMap } from "./stage-hdr-map.js";
|
|
11
|
+
|
|
12
|
+
// Rec. 709 weights on the 8-bit sRGB values: luma as a monitor shows it.
|
|
13
|
+
const lumaOf = (data, i) => 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
|
|
14
|
+
const round = (value, digits) => Number(value.toFixed(digits));
|
|
15
|
+
// A clipped pixel has a channel at full scale; a crushed one is black in every channel.
|
|
16
|
+
const CLIP = 254, CRUSH = 3;
|
|
17
|
+
|
|
18
|
+
/** Percentile of a 256-bin luma histogram, as 0..1. */
|
|
19
|
+
function percentile(histogram, count, fraction) {
|
|
20
|
+
let seen = 0;
|
|
21
|
+
for (let bin = 0; bin < 256; bin++) if ((seen += histogram[bin]) >= fraction * count) return bin / 255;
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Exposure statistics of one RGBA frame (rows bottom-up, as the GPU reads them
|
|
27
|
+
* back) and of pixel boxes inside it: meanLuma 0..1, clippedPct and crushedPct
|
|
28
|
+
* of the whole frame; per box its meanLuma and contrast (the spread between
|
|
29
|
+
* the 5th and 95th luma percentile, 0..1). A box is { id, x0, y0, x1, y1 } in
|
|
30
|
+
* pixels with y up, or { id } alone when the subject is out of frame.
|
|
31
|
+
*/
|
|
32
|
+
export function frameLighting({ data, width, height }, boxes = []) {
|
|
33
|
+
let sum = 0, clipped = 0, crushed = 0;
|
|
34
|
+
for (let i = 0; i < width * height * 4; i += 4) {
|
|
35
|
+
sum += lumaOf(data, i);
|
|
36
|
+
const high = Math.max(data[i], data[i + 1], data[i + 2]);
|
|
37
|
+
if (high >= CLIP) clipped++;
|
|
38
|
+
else if (high <= CRUSH) crushed++;
|
|
39
|
+
}
|
|
40
|
+
const pixels = width * height;
|
|
41
|
+
const subjects = boxes.map(({ id, x0, y0, x1, y1 }) => {
|
|
42
|
+
if (x0 === undefined || x1 < x0 || y1 < y0) return { id, meanLuma: null, contrast: null };
|
|
43
|
+
const histogram = new Uint32Array(256);
|
|
44
|
+
let total = 0, count = 0;
|
|
45
|
+
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
|
|
46
|
+
const luma = lumaOf(data, (y * width + x) * 4);
|
|
47
|
+
histogram[Math.min(255, Math.round(luma))]++;
|
|
48
|
+
total += luma;
|
|
49
|
+
count++;
|
|
50
|
+
}
|
|
51
|
+
return { id, meanLuma: round(total / count / 255, 3), contrast: round(percentile(histogram, count, 0.95) - percentile(histogram, count, 0.05), 3) };
|
|
52
|
+
});
|
|
53
|
+
return { meanLuma: round(sum / pixels / 255, 3), clippedPct: round((100 * clipped) / pixels, 1), crushedPct: round((100 * crushed) / pixels, 1), subjects };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A world box projected through the shot camera to a clamped pixel box (y up), or null off screen. */
|
|
57
|
+
function pixelBox(bounds, state, width, height) {
|
|
58
|
+
const { camera, filmback } = state, aspect = width / height;
|
|
59
|
+
const view = new PerspectiveCamera((focalMmToFov(camera.focalMm, filmback.sensorId, aspect) * 180) / Math.PI, aspect, 0.01, 10000);
|
|
60
|
+
view.position.set(camera.position.x, camera.position.y, camera.position.z);
|
|
61
|
+
view.lookAt(camera.lookAt.x, camera.lookAt.y, camera.lookAt.z);
|
|
62
|
+
view.updateMatrixWorld(true);
|
|
63
|
+
const forward = new Vector3(camera.lookAt.x - camera.position.x, camera.lookAt.y - camera.position.y, camera.lookAt.z - camera.position.z);
|
|
64
|
+
const corners = [bounds.min.x, bounds.max.x].flatMap((x) => [bounds.min.y, bounds.max.y].flatMap((y) => [bounds.min.z, bounds.max.z].map((z) => new Vector3(x, y, z))))
|
|
65
|
+
.filter((corner) => corner.clone().sub(view.position).dot(forward) > 0).map((corner) => corner.project(view));
|
|
66
|
+
if (!corners.length) return null;
|
|
67
|
+
const px = (ndc, size) => Math.round(((ndc + 1) / 2) * size);
|
|
68
|
+
const box = { x0: px(Math.min(...corners.map((c) => c.x)), width), x1: px(Math.max(...corners.map((c) => c.x)), width) - 1,
|
|
69
|
+
y0: px(Math.min(...corners.map((c) => c.y)), height), y1: px(Math.max(...corners.map((c) => c.y)), height) - 1 };
|
|
70
|
+
const clamped = { x0: Math.max(0, box.x0), x1: Math.min(width - 1, box.x1), y0: Math.max(0, box.y0), y1: Math.min(height - 1, box.y1) };
|
|
71
|
+
return clamped.x1 >= clamped.x0 && clamped.y1 >= clamped.y0 ? clamped : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The lighting check for verify_result: the shot frame at the playhead, its
|
|
75
|
+
* subjects the target characters (every visible one when no target is a
|
|
76
|
+
* character). Fails as TARGET_NOT_READY when nothing can be rendered. */
|
|
77
|
+
export function lightingChecks(ids, state, ports) {
|
|
78
|
+
if (typeof ports.renderFrameBuffer !== "function") throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot render the shot frame.");
|
|
79
|
+
if (!state.camera || !(state.camera.focalMm > 0)) throw new StudioProtocolError("TARGET_NOT_READY", "The shot camera must be available.");
|
|
80
|
+
const named = state.characters.filter((character) => ids.includes(character.id));
|
|
81
|
+
const characters = named.length ? named : state.characters.filter((character) => !character.hidden);
|
|
82
|
+
let image;
|
|
83
|
+
try { image = ports.renderFrameBuffer(state.frame); }
|
|
84
|
+
catch (error) { throw new StudioProtocolError("TARGET_NOT_READY", `The shot frame could not be rendered: ${error.message}`); }
|
|
85
|
+
const boxes = characters.map((character) => {
|
|
86
|
+
const bounds = entityWorldBounds(character, state, ports);
|
|
87
|
+
return { id: character.id, ...(bounds && !character.hidden ? pixelBox(bounds, state, image.width, image.height) : {}) };
|
|
88
|
+
});
|
|
89
|
+
// What lit the frame beyond the lights: the look, its environment light and its occlusion pass.
|
|
90
|
+
// An HDR environment is named with whether its map had loaded for this frame (until then the look's sky lit it).
|
|
91
|
+
const { preset, envLight, ao, hdr, hdrRotation, hdrIntensity, hdrBackground } = normalizeLighting(state.stage?.lighting);
|
|
92
|
+
return { coverage: "rendered-shot-frame", frame: state.frame, ...frameLighting(image, boxes),
|
|
93
|
+
look: { preset, envLight, ao, ...(hdr ? { hdr, hdrLoaded: Boolean(readyHdrMap(hdr)), hdrRotation, hdrIntensity, hdrBackground } : {}) } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Puts the lighting check on a verify_result answer, or its reason in unsupportedReasons. */
|
|
97
|
+
export function verifyLighting(result, ids, state, ports) {
|
|
98
|
+
try { result.checks = { ...result.checks, lighting: lightingChecks(ids, state, ports) }; }
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!(error instanceof StudioProtocolError)) throw error;
|
|
101
|
+
result.unsupportedReasons.lighting = error.message;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// verify_result's visual: the one image the agent asks to see. "frame" and "plan" are captures;
|
|
2
|
+
// "contact_sheet" tiles frames spread evenly, picked from the installed take's beats
|
|
3
|
+
// (frames: "beats") or named (frames: [f, ...]), seen by the shot camera or by views of its own
|
|
4
|
+
// (views, focus, trail); "trace" plots hand heights and distances of the take over the range (C11).
|
|
5
|
+
// The beats, the views and the trace read one lenient compile of the take's stored spec, as motion.qa does.
|
|
6
|
+
import { StudioProtocolError } from "./studio-agent-protocol.js";
|
|
7
|
+
import { buildContactSheet, paintDots, sampleContactSheetFrames, sheetGrid, viewTileSide } from "./studio-contact-sheet.js";
|
|
8
|
+
import { compileSpec } from "./motion3/pipeline.js";
|
|
9
|
+
import { motionScene } from "./motion3/scene.js";
|
|
10
|
+
import { skeletonFor } from "./motion3/rigs/index.js";
|
|
11
|
+
import { beatFrames, grabReleases, traceSeries, TRACE_COLORS } from "./motion3/beats.js";
|
|
12
|
+
import { depthViews, projectPoint, trailPoints, viewCameras } from "./motion3/views.js";
|
|
13
|
+
import { checkRef } from "./motion3/refs.js";
|
|
14
|
+
import { renderWithGhosts } from "./ghost-set.js";
|
|
15
|
+
import { GHOST_COLORS, sheetGhostSettings } from "./view-ghosts.js";
|
|
16
|
+
import { publishSheet } from "./agent-progress.js";
|
|
17
|
+
import { clampFrames } from "./motion3/lenient.js";
|
|
18
|
+
import { sheetCaption } from "./workflow/agent-progress-view.js";
|
|
19
|
+
// verify_result's lighting check also reads the rendered shot frame; the binding imports it from here.
|
|
20
|
+
export { verifyLighting } from "./studio-lighting-check.js";
|
|
21
|
+
|
|
22
|
+
const TIMELINE_FPS = 24, TRACE_WIDTH = 960, TRACE_HEIGHT = 360, PAD = 12;
|
|
23
|
+
const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
|
24
|
+
|
|
25
|
+
/** [first, last] absolute frames of a verify_result range ("whole_clip" or none: the timeline). */
|
|
26
|
+
const rangeOf = (range, frameCount) => range && range !== "whole_clip" ? [range.startFrame, range.endFrameExclusive - 1] : [0, frameCount - 1];
|
|
27
|
+
|
|
28
|
+
/** The first subject character with a compiled take, compiled again (null when none or it fails). */
|
|
29
|
+
function takeCompile(args, s, receipt) {
|
|
30
|
+
for (const id of args.targets ?? receipt?.affectedIds ?? []) {
|
|
31
|
+
const character = s.characters.find(c => c.id === id), take = s.targets.get(id)?.motion;
|
|
32
|
+
const sk = take?.schema === "cozyclay.take.v3" && take.spec ? skeletonFor(take.rig ?? character?.model) : null;
|
|
33
|
+
if (!character || !sk) continue;
|
|
34
|
+
try {
|
|
35
|
+
return { ...compileSpec({ fps: TIMELINE_FPS, ...take.spec }, { sk, scene: motionScene(s.objects) }, { lenient: true }), characterId: id };
|
|
36
|
+
} catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Line series on a dark plot, as RGBA rows bottom-up (WebGL read-back order, what the encodePng
|
|
43
|
+
* port flips): x is frames lo..hi, y one metre scale for every series. Grey lines mark each second
|
|
44
|
+
* and every 0.1 m; `marks` are frames drawn as light verticals (beat starts, grab, release).
|
|
45
|
+
*/
|
|
46
|
+
export function traceRgba(series, [lo, hi], { width = TRACE_WIDTH, height = TRACE_HEIGHT, marks = [] } = {}) {
|
|
47
|
+
const data = new Uint8Array(width * height * 4);
|
|
48
|
+
const put = (x, y, [r, g, b]) => {
|
|
49
|
+
if (x < 0 || y < 0 || x >= width || y >= height) return;
|
|
50
|
+
const i = (y * width + x) * 4; data[i] = r; data[i + 1] = g; data[i + 2] = b; data[i + 3] = 255;
|
|
51
|
+
};
|
|
52
|
+
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) put(x, y, [22, 24, 29]);
|
|
53
|
+
let bottom = Math.min(...series.map(s => s.min)), top = Math.max(...series.map(s => s.max));
|
|
54
|
+
if (!(top > bottom)) { bottom = (Number.isFinite(bottom) ? bottom : 0) - 0.05; top = bottom + 0.1; }
|
|
55
|
+
const X = f => Math.round(PAD + ((f - lo) / Math.max(1, hi - lo)) * (width - 2 * PAD));
|
|
56
|
+
const Y = v => Math.round(PAD + ((v - bottom) / (top - bottom)) * (height - 2 * PAD));
|
|
57
|
+
for (let m = Math.ceil(bottom * 10); m <= top * 10; m++) for (let x = PAD; x < width - PAD; x++) put(x, Y(m / 10), m === 0 ? [90, 94, 104] : [44, 47, 55]);
|
|
58
|
+
for (let f = Math.ceil(lo / TIMELINE_FPS) * TIMELINE_FPS; f <= hi; f += TIMELINE_FPS) for (let y = PAD; y < height - PAD; y++) put(X(f), y, [44, 47, 55]);
|
|
59
|
+
for (const f of marks) if (f >= lo && f <= hi) for (let y = PAD; y < height - PAD; y++) put(X(f), y, [150, 154, 164]);
|
|
60
|
+
for (const s of series) {
|
|
61
|
+
const color = rgb(s.color);
|
|
62
|
+
for (let i = 1; i < s.values.length; i++) {
|
|
63
|
+
const a = s.values[i - 1], b = s.values[i];
|
|
64
|
+
if (a === null || b === null) continue;
|
|
65
|
+
const x0 = X(lo + i - 1), y0 = Y(a), x1 = X(lo + i), y1 = Y(b), n = Math.max(1, Math.abs(x1 - x0), Math.abs(y1 - y0));
|
|
66
|
+
for (let k = 0; k <= n; k++) {
|
|
67
|
+
const x = Math.round(x0 + ((x1 - x0) * k) / n), y = Math.round(y0 + ((y1 - y0) * k) / n);
|
|
68
|
+
put(x, y, color); put(x + 1, y, color); put(x, y + 1, color); put(x + 1, y + 1, color);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { data, width, height };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const listOf = value => value === undefined ? [] : [value].flat();
|
|
76
|
+
const round2 = p => p.map(v => Math.round(v * 100) / 100);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A contact sheet from views of its own (motion3/views.js), a row per view, never the shot
|
|
80
|
+
* camera: each view is placed once for every frame, framed on args.focus (default the whole
|
|
81
|
+
* character) and the trails. A trail is a dot per frame of its ref over the range, so spacing is
|
|
82
|
+
* speed: frames up to the tile's frame solid, later ones faint, the tile's frame ringed. With
|
|
83
|
+
* args.ghosts every tile also draws the take's poses around its frame (ghost-set.js). args.views
|
|
84
|
+
* are the rendered ones (depthViews); `added` names the view added to them and why.
|
|
85
|
+
*/
|
|
86
|
+
function viewSheet(args, r, { s, frames, span, grid, picks, ports, added }) {
|
|
87
|
+
if (!r) throw new StudioProtocolError("TARGET_NOT_READY", "views frame a character's compiled take; name a character that has one (run motion.compile first).");
|
|
88
|
+
const focus = listOf(args.focus), refs = listOf(args.trail), ctx = { sk: r.out.ctx.sk, scene: r.out.ctx.scene };
|
|
89
|
+
for (const [field, list] of [["focus", focus], ["trail", refs]]) list.forEach((ref, i) => checkRef(r.expanded, message => { throw new StudioProtocolError("INVALID_ARGUMENT", message); }, ctx, `${field}[${i}]`, ref));
|
|
90
|
+
const trails = refs.map((ref, i) => ({ ref, color: TRACE_COLORS[i], points: trailPoints(r.out, ref, span) }));
|
|
91
|
+
const side = viewTileSide(grid), cameras = viewCameras(r.out, { views: args.views, frames, focus, trails });
|
|
92
|
+
if (!cameras.length) throw new StudioProtocolError("TARGET_NOT_READY", "views found no compiled frame of the take in the range.");
|
|
93
|
+
const cells = cameras.flatMap(cam => frames.map(frame => ({ cam, frame }))), target = args.ghosts ? s.targets.get(r.characterId) : null;
|
|
94
|
+
if (args.ghosts && !target?.rig) throw new StudioProtocolError("TARGET_NOT_READY", "ghosts pose a copy of the character's rig; it has not loaded in the editor yet.");
|
|
95
|
+
const sheet = buildContactSheet(cells, ({ cam, frame }) => {
|
|
96
|
+
const render = () => ports.renderFrameBuffer(frame, { eye: cam.eye, quaternion: cam.quaternion, fovDeg: cam.fovDeg, output: { width: side, height: side } });
|
|
97
|
+
const tile = target ? renderWithGhosts({ rig: target.rig, take: target.motion, objects: s.objects }, frame, render) : render();
|
|
98
|
+
const dots = trails.flatMap(t => t.points.map(({ f, p }) => [f, projectPoint(cam, p, tile.width, tile.height), rgb(t.color)])
|
|
99
|
+
.filter(([, q]) => q).map(([f, [x, y], color]) => ({ x, y, color, r: f === frame ? 4 : 1.5, alpha: f <= frame ? 1 : 0.35, ring: f === frame })));
|
|
100
|
+
return { ...tile, data: paintDots(tile.data, tile.width, tile.height, dots) };
|
|
101
|
+
}, grid);
|
|
102
|
+
const views = cameras.map(cam => ({ name: cam.name, eye: round2(cam.eye), target: round2(cam.target) }));
|
|
103
|
+
const ref = { frames, layout: grid.layout, views, ...(added ? { addedView: added } : {}), focus: focus.length ? focus : "character", ...(trails.length ? { trails: trails.map(({ ref: of, color }) => ({ ref: of, color })) } : {}), ...(picks ? { picks } : {}),
|
|
104
|
+
...(target ? { ghosts: { ...sheetGhostSettings(), ...GHOST_COLORS } } : {}) };
|
|
105
|
+
return [{ dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: grid.layout, views: views.map(v => v.name) }, ref];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Answers args.visual into result.visualRefs and keeps the image for resolve_studio_image. */
|
|
109
|
+
export function studioVisual(args, { s, receipt, result, images, ports }) {
|
|
110
|
+
const keep = (image, ref) => {
|
|
111
|
+
const imageId = crypto.randomUUID();
|
|
112
|
+
images.set(imageId, { ...image, revision: s.revision, receiptId: result.receiptId });
|
|
113
|
+
result.visualRefs.push({ imageId, ...ref });
|
|
114
|
+
// the user sees what the agent looked at (agent-progress.js: the agent panel's progress card)
|
|
115
|
+
const kind = image.kind ?? (args.visual === "contact_sheet" ? "contact sheet" : args.visual);
|
|
116
|
+
publishSheet({ characterId: args.targets?.[0] ?? receipt?.affectedIds?.[0] ?? null, dataUrl: image.dataUrl, width: image.width, height: image.height,
|
|
117
|
+
caption: sheetCaption({ kind, views: image.views ?? [], frames: image.frames ?? [] }), frames: image.frames ?? [] });
|
|
118
|
+
};
|
|
119
|
+
if (args.visual !== "contact_sheet" && (args.views || args.focus || args.trail)) throw new StudioProtocolError("INVALID_ARGUMENT", 'views, focus and trail shape a contact sheet; send visual: "contact_sheet".');
|
|
120
|
+
if (args.ghosts && (args.visual !== "contact_sheet" || !args.views)) throw new StudioProtocolError("INVALID_ARGUMENT", 'ghosts are drawn on a contact sheet from views of its own; send visual: "contact_sheet" with views, e.g. ["side"].');
|
|
121
|
+
if (!args.views && (args.focus || args.trail)) throw new StudioProtocolError("INVALID_ARGUMENT", 'focus and trail are drawn by views of its own; add views, e.g. ["front", "side"] (the shot camera stays the user\'s).');
|
|
122
|
+
if (args.visual === "contact_sheet") {
|
|
123
|
+
// One image of frames across the range, each rendered through the export
|
|
124
|
+
// path, which puts the playhead pose, shot camera and props back.
|
|
125
|
+
const span = rangeOf(args.range, s.frameCount), r = args.frames === "beats" || args.views ? takeCompile(args, s, receipt) : null;
|
|
126
|
+
let frames = sampleContactSheetFrames(args.range, s.frameCount), picks, adjusted;
|
|
127
|
+
// named frames past the range are drawn at its nearest end, and visualRefs[0].adjusted says so (lenient.js)
|
|
128
|
+
if (Array.isArray(args.frames)) ({ frames, adjusted } = clampFrames(args.frames, span));
|
|
129
|
+
else if (args.frames === "beats") ({ frames, picks } = beatFrames({ range: span, even: frames, timeline: r?.timeline, groups: r?.qa.groups, objects: r?.expanded.objects }));
|
|
130
|
+
// Views of its own always include two that look across each other (motion3/views.js depthViews).
|
|
131
|
+
const depth = args.views ? depthViews(args.views) : null, grid = sheetGrid(frames.length, depth?.views.length ?? 0);
|
|
132
|
+
const said = ref => (adjusted ? { ...ref, adjusted } : ref);
|
|
133
|
+
if (depth) { const [image, ref] = viewSheet({ ...args, views: depth.views }, r, { s, frames, span, grid, picks, ports, added: depth.added }); return keep(image, said(ref)); }
|
|
134
|
+
const sheet = buildContactSheet(frames, ports.renderFrameBuffer, grid);
|
|
135
|
+
return keep({ dataUrl: ports.encodePng(sheet.data, sheet), width: sheet.width, height: sheet.height, frames, layout: grid.layout }, said({ frames, layout: grid.layout, ...(picks ? { picks } : {}) }));
|
|
136
|
+
}
|
|
137
|
+
if (args.visual === "trace") {
|
|
138
|
+
const r = takeCompile(args, s, receipt);
|
|
139
|
+
if (!r) throw new StudioProtocolError("TARGET_NOT_READY", "visual trace plots a character's compiled take; name a character that has one (run motion.compile first).");
|
|
140
|
+
const { range, series } = traceSeries(r.out, r.expanded, rangeOf(args.range, s.frameCount));
|
|
141
|
+
const marks = [...(r.timeline ?? []).map(entry => entry.f?.[0]), ...grabReleases(r.timeline, r.expanded.objects).map(row => row.f)];
|
|
142
|
+
const plot = traceRgba(series, range, { marks });
|
|
143
|
+
return keep({ dataUrl: ports.encodePng(plot.data, plot), width: plot.width, height: plot.height, kind: "trace" },
|
|
144
|
+
{ kind: "trace", range, series: series.map(({ values, ...summary }) => summary) });
|
|
145
|
+
}
|
|
146
|
+
// A frame is the shot camera's view; a plan is the Top View, the set from above
|
|
147
|
+
// with names and the shot camera's wedge.
|
|
148
|
+
const capture = args.visual === "plan" ? (ports.capturePlan ?? (() => { throw new StudioProtocolError("CAPABILITY_MISSING", "This editor cannot render the top view."); }))() : ports.capture();
|
|
149
|
+
keep(capture, args.visual === "plan" ? { kind: "plan" } : {});
|
|
150
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The refusal texts of validateStudioSchema (studio-agent-protocol.js). Every text starts with the
|
|
2
|
+
// path ("$.args.topic") and says what the schema allows there: the allowed values, the allowed
|
|
3
|
+
// fields, the expected type or bounds. What arrived is named by its kind ("got a string"), or by
|
|
4
|
+
// a short preview of a scalar (at most 40 characters), never as a whole payload such as an image.
|
|
5
|
+
// Shared by the browser and the sidecar: no imports.
|
|
6
|
+
const LIST_MAX = 24, PREVIEW_MAX = 40;
|
|
7
|
+
|
|
8
|
+
const preview = value => {
|
|
9
|
+
const text = JSON.stringify(value) ?? String(value);
|
|
10
|
+
return text.length > PREVIEW_MAX ? `${text.slice(0, PREVIEW_MAX - 3)}...` : text;
|
|
11
|
+
};
|
|
12
|
+
const kindOf = value => (value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object"
|
|
13
|
+
: typeof value === "number" && !Number.isFinite(value) ? String(value) : `a ${typeof value}`);
|
|
14
|
+
/** A list of names, cut after LIST_MAX with how many there are in all. */
|
|
15
|
+
const listOf = values => (values.length > LIST_MAX ? `${values.slice(0, LIST_MAX).join(", ")}, ... (${values.length} in all)` : values.join(", "));
|
|
16
|
+
const typeName = schema => (Object.hasOwn(schema, "const") ? JSON.stringify(schema.const) : schema.type === "integer" ? "an integer"
|
|
17
|
+
: schema.type === "array" ? `an array${schema.items?.type ? ` of ${schema.items.type}s` : ""}`
|
|
18
|
+
: schema.type === "object" ? "an object" : schema.type === "null" ? "null" : `a ${schema.type}`);
|
|
19
|
+
|
|
20
|
+
/** Wrong type, or a constant that differs: what the schema wants and what arrived. */
|
|
21
|
+
export const wrongType = (path, schema, value) => `${path}: expected ${typeName(schema)}, got ${Object.hasOwn(schema, "const") ? preview(value) : kindOf(value)}.`;
|
|
22
|
+
|
|
23
|
+
export const unknownField = (path, schema, key) => {
|
|
24
|
+
const allowed = Object.keys(schema.properties);
|
|
25
|
+
return `${path} has unknown field ${preview(key)}; ${allowed.length ? `allowed: ${listOf(allowed)}` : "it takes no fields"}.`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const missingField = (path, key) => `${path}: missing required field "${key}".`;
|
|
29
|
+
|
|
30
|
+
/** An array of the wrong type or length. */
|
|
31
|
+
export function arrayRefusal(path, schema, value) {
|
|
32
|
+
if (!Array.isArray(value)) return wrongType(path, schema, value);
|
|
33
|
+
const lo = schema.minItems ?? 0, hi = schema.maxItems ?? Infinity;
|
|
34
|
+
const want = lo === hi ? `exactly ${lo}` : hi === Infinity ? `at least ${lo}` : lo === 0 ? `at most ${hi}` : `${lo} to ${hi}`;
|
|
35
|
+
return `${path}: expected ${want} item${lo === hi && lo === 1 ? "" : "s"}, got ${value.length}.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const duplicateItem = (path, items) => {
|
|
39
|
+
const seen = new Set(), twice = items.map(item => JSON.stringify(item)).find(text => (seen.has(text) ? true : (seen.add(text), false)));
|
|
40
|
+
return `${path}: items must be unique; ${twice && twice.length <= PREVIEW_MAX ? twice : "one item"} appears twice.`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A string of the wrong type, empty, too long, or off its pattern. */
|
|
44
|
+
export function stringRefusal(path, schema, value) {
|
|
45
|
+
if (typeof value !== "string") return wrongType(path, schema, value);
|
|
46
|
+
if (!value.trim()) return `${path}: must not be empty.`;
|
|
47
|
+
const length = [...value].length;
|
|
48
|
+
if (schema.maxLength !== undefined && length > schema.maxLength) return schema.description ? `${path}: over ${schema.maxLength} characters. ${schema.description}` : `${path}: at most ${schema.maxLength} characters, got ${length}.`;
|
|
49
|
+
return `${path}: ${preview(value)} does not match ${schema.pattern}.`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const enumRefusal = (path, schema, value) => `${path}: ${preview(value)} is not one of: ${listOf(schema.enum)}.`;
|
|
53
|
+
|
|
54
|
+
/** A number of the wrong type, not an integer, or not above an exclusive minimum. */
|
|
55
|
+
export function numberRefusal(path, schema, value) {
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return wrongType(path, schema, value);
|
|
57
|
+
if (schema.type === "integer" && !Number.isSafeInteger(value)) return `${path}: expected an integer, got ${value}.`;
|
|
58
|
+
return `${path}: must be greater than ${schema.exclusiveMinimum}, got ${value}.`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isPlain = schema => ((schema.oneOf ?? schema.anyOf) ? (schema.oneOf ?? schema.anyOf).every(isPlain) : schema.type !== "object" && schema.type !== "array");
|
|
62
|
+
const bounded = schema => [schema.minimum, schema.maximum].some(bound => bound !== undefined) ? ` within [${[schema.minimum ?? "-inf", schema.maximum ?? "inf"].join(", ")}]` : "";
|
|
63
|
+
/** Each value or type a schema admits, in words: true, false, "ifClean", an integer within [1, 8]. */
|
|
64
|
+
const formsOf = schema => ((schema.oneOf ?? schema.anyOf) ? (schema.oneOf ?? schema.anyOf).flatMap(formsOf)
|
|
65
|
+
: Object.hasOwn(schema, "const") ? [JSON.stringify(schema.const)] : schema.enum ? schema.enum.map(v => JSON.stringify(v))
|
|
66
|
+
: schema.type === "boolean" ? ["true", "false"] : [`${typeName(schema)}${schema.type === "number" || schema.type === "integer" ? bounded(schema) : ""}`]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A union no branch matched, when every branch failed at the union's own path and the branches are
|
|
70
|
+
* plain values (or the value is not an object): `$.args.install must be true, false or "ifClean",
|
|
71
|
+
* got "yes".` Otherwise null, and the caller surfaces the deepest branch failure.
|
|
72
|
+
*/
|
|
73
|
+
export function unionRefusal(path, failures, value) {
|
|
74
|
+
if (!failures.every(({ error }) => error.details?.path === path)) return null;
|
|
75
|
+
if (value !== null && typeof value === "object" && !failures.every(({ branch }) => isPlain(branch))) return null;
|
|
76
|
+
const forms = [...new Set(failures.flatMap(({ branch }) => formsOf(branch)))];
|
|
77
|
+
const shown = forms.length > LIST_MAX ? [...forms.slice(0, LIST_MAX), `... (${forms.length} in all)`] : forms;
|
|
78
|
+
const words = shown.length > 1 ? `${shown.slice(0, -1).join(", ")} or ${shown.at(-1)}` : shown[0];
|
|
79
|
+
return `${path} must be ${words}, got ${value !== null && typeof value === "object" ? kindOf(value) : preview(value)}.`;
|
|
80
|
+
}
|