cozyclay 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +40 -113
- package/bin/agent/agent-runner.mjs +9 -10
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +3 -3
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-C9sUnndd.js +1 -0
- package/dist/assets/app-CCh--1Hc.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-C4yKH1p4.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +150 -2624
- package/src/agent-progress.js +79 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +5 -0
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +8 -6
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +207 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +49 -11
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +51 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +46 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +59 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +42 -413
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +75 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +105 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +170 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +162 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +143 -0
- package/src/motion3/compiler/validate-parts.js +116 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +73 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +174 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +173 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +121 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +179 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +42 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +218 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +120 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +111 -0
- package/src/motion3/schema/types.js +147 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/skills/basic.js +82 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/index.js +83 -0
- package/src/motion3/skills/info.js +65 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +9 -51
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +12 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +41 -71
- package/src/studio-app-binding.js +25 -45
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +8 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +107 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +8 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -182
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /package/src/{ardy → motion}/range-pin.js +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Validation of spec.objects (C3): a hinge { pivot, axis?, channel } or a carry { prop, grab?,
|
|
2
|
+
// release?, at?, along?, visible?, level?, keys? }. Both take a body or a group (Empty) id; at/along
|
|
3
|
+
// pin the palm offset (a group: its root; without them a group keeps its grab-frame offset); keys
|
|
4
|
+
// (C14) pose a carried object while it is in hand (rot, or aim + roll: C16). A switch { visible?,
|
|
5
|
+
// level? } (C15: no prop, no hinge field) takes any record, a light or an effect too, and moves
|
|
6
|
+
// nothing; so does a follow { follow, offset?, aim?, visible?, level? } (C16), which rides a point
|
|
7
|
+
// of the body. A free entry { keys?, fall?, visible?, level? } (C17: no prop, no follow, no hinge field) moves a body or a
|
|
8
|
+
// group on its own: world pose keys over the whole take, and gravity falls; a carry takes fall too, before its grab
|
|
9
|
+
// or from its release on. Fields: schema/fields.js OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS. See validate.js.
|
|
10
|
+
import { isVec3, fmt, didYouMean } from "./vocab.js";
|
|
11
|
+
import { objectOk } from "./validate-common.js";
|
|
12
|
+
import { OBJECT_FIELDS, OBJECT_KEY_FIELDS, FALL_FIELDS } from "../schema/fields.js";
|
|
13
|
+
import { isSwitch, isFollow, movesRecord } from "../carried.js";
|
|
14
|
+
import { isFree } from "../free-motion.js";
|
|
15
|
+
import { checkRef } from "../refs.js";
|
|
16
|
+
import { partsOf } from "../object-pose.js";
|
|
17
|
+
|
|
18
|
+
const KEY = "{ f, pos: [x, y, z], rot: [x, y, z, w] } or { f, pos, aim: [x, y, z], roll? }", FALL = "{ at, onto?, bounce? }";
|
|
19
|
+
const isQuat = q => Array.isArray(q) && q.length === 4 && q.every(Number.isFinite) && Math.abs(Math.hypot(...q) - 1) <= 0.01;
|
|
20
|
+
const isDir = v => isVec3(v) && Math.hypot(...v) > 1e-9;
|
|
21
|
+
|
|
22
|
+
/** A key's rotation: rot (a unit quaternion) or aim (a world direction for its +Y) with an optional roll (degrees about it), not both. */
|
|
23
|
+
function keyTurnOk(err, kat, k) {
|
|
24
|
+
if (k.rot !== undefined && k.aim !== undefined) return err(`${kat} gives both rot and aim; give one: rot [x, y, z, w], or aim [x, y, z] (the world direction its +Y points) with roll`);
|
|
25
|
+
if (k.aim === undefined) {
|
|
26
|
+
if (k.roll !== undefined) err(`${kat}.roll turns the object about aim; give aim [x, y, z] with it, or leave roll out (rot fixes the roll)`);
|
|
27
|
+
if (k.lean !== undefined) err(`${kat}.lean lets the compiler lean aim; give aim [x, y, z] with it (rot is used exactly)`);
|
|
28
|
+
if (!isQuat(k.rot)) err(`${kat}.rot must be a unit quaternion [x, y, z, w], got ${fmt(k.rot)}${k.rot === undefined ? "; or give aim [x, y, z], the world direction its +Y points" : ""}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!isDir(k.aim)) err(`${kat}.aim must be a non-zero world direction [x, y, z] for the object's +Y (a bat: knob to barrel), got ${fmt(k.aim)}`);
|
|
32
|
+
if (k.roll !== undefined && !Number.isFinite(k.roll)) err(`${kat}.roll must be degrees about aim (a number), got ${fmt(k.roll)}`);
|
|
33
|
+
if (k.lean !== undefined && !(Number.isFinite(k.lean) && k.lean >= 0 && k.lean <= 45)) err(`${kat}.lean must be the most degrees the compiler may lean aim (0..45), got ${fmt(k.lean)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** objects.<id>.keys (C14): whole frames inside grab..release (free, C17: the whole take), one per frame, a world position and a unit quaternion. */
|
|
37
|
+
function validateKeys(err, unknown, at, o, frames, free = false) {
|
|
38
|
+
if (!Array.isArray(o.keys)) return err(`${at}.keys must be an array of ${KEY}`);
|
|
39
|
+
const last = Number.isInteger(frames) ? frames - 1 : Infinity, lo = free ? 0 : o.grab ?? 0, hi = free ? last : o.release ?? last, seen = new Map();
|
|
40
|
+
o.keys.forEach((k, i) => {
|
|
41
|
+
const kat = `${at}.keys[${i}]`;
|
|
42
|
+
if (!k || typeof k !== "object" || Array.isArray(k)) return err(`${kat} must be ${KEY}, got ${fmt(k)}`);
|
|
43
|
+
for (const f of Object.keys(k)) if (!OBJECT_KEY_FIELDS.includes(f)) unknown(`${kat} has unknown field "${f}"${didYouMean(f, OBJECT_KEY_FIELDS)}; a key is ${KEY}`);
|
|
44
|
+
if (!Number.isInteger(k.f)) err(`${kat}.f must be a whole frame, got ${fmt(k.f)}`);
|
|
45
|
+
else if (k.f < lo || k.f > hi) err(free ? `${kat}.f = ${k.f} is outside the take (0..${hi})` : `${kat}.f = ${k.f} is outside grab..release (${lo}..${hi}); keys pose a carried object only while it is in hand (drop its prop to move it on its own, C17)`);
|
|
46
|
+
else if (seen.has(k.f)) err(`${kat}.f = ${k.f}: keys[${seen.get(k.f)}] is at that frame already; one key per frame`);
|
|
47
|
+
else seen.set(k.f, i);
|
|
48
|
+
if (!isVec3(k.pos)) err(`${kat}.pos must be [x, y, z], the world position of the object's base (a group: its root), got ${fmt(k.pos)}`);
|
|
49
|
+
keyTurnOk(err, kat, k);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** An object: ref a follow entry may use: one no follow entry moves (else it would follow itself). */
|
|
54
|
+
function stillRef(spec, err, scene, at, ref) {
|
|
55
|
+
const id = /^object:(.+?)(?:\.(?:center|top|base))?$/s.exec(ref)?.[1];
|
|
56
|
+
if (id === undefined) return;
|
|
57
|
+
const movers = partsOf(scene, id).flatMap(p => [p, ...(scene.objects[p].ancestors ?? [])].find(k => movesRecord(spec.objects?.[k])) ?? []);
|
|
58
|
+
const by = movers.find(k => isFollow(spec.objects[k]));
|
|
59
|
+
if (by) err(`${at} "${ref}": "${id}" moves with the follow entry objects.${by}; a follow entry rides a joint, landmark, prop or anchor point, or an object no follow entry moves`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** follow (C16): a point ref, offset along the record's own axes, aim "face" | "up" | [x, y, z] | a point ref. */
|
|
63
|
+
function validateFollow(spec, err, ctx, at, o) {
|
|
64
|
+
if (checkRef(spec, err, ctx, `${at}.follow`, o.follow)) stillRef(spec, err, ctx.scene, `${at}.follow`, o.follow);
|
|
65
|
+
if (o.offset !== undefined && !isVec3(o.offset)) err(`${at}.offset must be [x, y, z], metres along the record's own axes after aim (+Y along aim), got ${fmt(o.offset)}`);
|
|
66
|
+
const aim = o.aim;
|
|
67
|
+
if (aim === undefined || aim === "face" || aim === "up") return;
|
|
68
|
+
if (Array.isArray(aim)) { if (!isDir(aim)) err(`${at}.aim must be a non-zero world direction [x, y, z], got ${fmt(aim)}`); return; }
|
|
69
|
+
if (typeof aim !== "string" || !/^[a-z]+:/.test(aim)) return err(`${at}.aim must be "face", "up", a world direction [x, y, z] or a point ref (toward it), where the record's +Y points; got ${fmt(aim)}`);
|
|
70
|
+
if (checkRef(spec, err, ctx, `${at}.aim`, aim)) stillRef(spec, err, ctx.scene, `${at}.aim`, aim);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function frameOk(err, at, v, frames) {
|
|
74
|
+
if (v === undefined) return true;
|
|
75
|
+
if (Number.isInteger(v) && v >= 0 && (!Number.isInteger(frames) || v < frames)) return true;
|
|
76
|
+
err(`${at} must be a whole frame 0..${Number.isInteger(frames) ? frames - 1 : "frames-1"}, got ${fmt(v)}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** objects.<id>.fall (C17): { at, onto?, bounce? } or a list, each on its own frame; a carry falls only out of hand. */
|
|
81
|
+
function validateFalls(err, unknown, at, o, { frames, scene, carry }) {
|
|
82
|
+
const list = Array.isArray(o.fall) ? o.fall : [o.fall], seen = new Set();
|
|
83
|
+
if (!list.length) err(`${at}.fall is an empty list; give ${FALL} or a list of them`);
|
|
84
|
+
list.forEach((fl, i) => {
|
|
85
|
+
const fat = Array.isArray(o.fall) ? `${at}.fall[${i}]` : `${at}.fall`;
|
|
86
|
+
if (!fl || typeof fl !== "object" || Array.isArray(fl)) return err(`${fat} must be ${FALL} (at: the frame it starts to fall), got ${fmt(fl)}`);
|
|
87
|
+
for (const k of Object.keys(fl)) if (!FALL_FIELDS.includes(k)) unknown(`${fat} has unknown field "${k}"${didYouMean(k, FALL_FIELDS)}; a fall is ${FALL}`);
|
|
88
|
+
if (fl.at === undefined) err(`${fat}.at must be the whole frame it starts to fall`);
|
|
89
|
+
else if (frameOk(err, `${fat}.at`, fl.at, frames)) {
|
|
90
|
+
if (seen.has(fl.at)) err(`${fat}.at = ${fl.at}: another fall starts on that frame`);
|
|
91
|
+
seen.add(fl.at);
|
|
92
|
+
const g = o.grab ?? 0, r = o.release;
|
|
93
|
+
if (carry && fl.at >= g && (r === undefined || fl.at < r)) err(`${fat}.at = ${fl.at} is while it is in hand (grab ${g}..release ${r ?? "end"}); a carried object falls before its grab or from its release on`);
|
|
94
|
+
}
|
|
95
|
+
if (fl.onto !== undefined) objectOk(err, scene, `${fat}.onto`, fl.onto, { groups: true });
|
|
96
|
+
if (fl.bounce !== undefined && !(Number.isFinite(fl.bounce) && fl.bounce >= 0 && fl.bounce <= 0.6)) err(`${fat}.bounce must be 0..0.6 (the share of its landing speed it bounces back with; 0 lands dead), got ${fmt(fl.bounce)}`);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** visible and level name channels (C15): visible shows the record above 0.5, level scales its light, glow and effect rate. */
|
|
101
|
+
function channelsOk(err, at, o) {
|
|
102
|
+
if (o.visible !== undefined && (typeof o.visible !== "string" || !o.visible)) err(`${at}.visible must name a channel (shown above 0.5)`);
|
|
103
|
+
if (o.level !== undefined && (typeof o.level !== "string" || !o.level)) err(`${at}.level must name a channel (the record's level 0..4: 0 dark, 1 as authored; it scales its light, glow and effect rate)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function validateObjects(spec, err, { sk, scene, frames = spec.frames, unknown = err }) {
|
|
107
|
+
for (const [id, o] of Object.entries(spec.objects ?? {})) {
|
|
108
|
+
const at = `objects.${id}`, kind = o?.prop !== undefined ? "carry" : isFollow(o) ? "follow" : isFree(o) ? "free" : isSwitch(o) ? "switch" : "hinge", fields = OBJECT_FIELDS[kind];
|
|
109
|
+
// a switch and a follow take any record (a light, an effect); the others a body or a group
|
|
110
|
+
if ((kind !== "switch" && kind !== "follow") || !scene?.others?.[id]) objectOk(err, scene, at, id, { groups: true });
|
|
111
|
+
for (const f of Object.keys(o ?? {})) if (!fields.includes(f) && !(kind === "hinge" && (f === "keys" || f === "fall"))) unknown(`${at} (${kind}) has unknown field "${f}"${didYouMean(f, fields)}; fields: ${fields.join(", ")}`);
|
|
112
|
+
if (kind === "follow") {
|
|
113
|
+
validateFollow(spec, err, { sk, scene }, at, o);
|
|
114
|
+
channelsOk(err, at, o);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (kind === "switch") {
|
|
118
|
+
if (o.visible === undefined && o.level === undefined) err(`${at} needs visible or level (a channel): a switch shows, hides or dims "${id}" without moving it; a carry needs prop, a hinge pivot and channel`);
|
|
119
|
+
channelsOk(err, at, o);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (kind === "free") {
|
|
123
|
+
if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames, true);
|
|
124
|
+
if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: false });
|
|
125
|
+
channelsOk(err, at, o);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (kind === "carry") {
|
|
129
|
+
if (!spec.props?.[o.prop]) err(`${at}.prop "${o.prop}" is not in spec.props`);
|
|
130
|
+
for (const f of ["at", "along"]) if (o[f] !== undefined && !isVec3(o[f])) err(`${at}.${f} must be [d, l, n] along the palm axes, got ${fmt(o[f])}`);
|
|
131
|
+
channelsOk(err, at, o);
|
|
132
|
+
if (frameOk(err, `${at}.grab`, o.grab, frames) && frameOk(err, `${at}.release`, o.release, frames)
|
|
133
|
+
&& o.grab !== undefined && o.release !== undefined && !(o.grab < o.release)) err(`${at}: grab (${o.grab}) must come before release (${o.release})`);
|
|
134
|
+
if (o.keys !== undefined) validateKeys(err, unknown, at, o, frames);
|
|
135
|
+
if (o.fall !== undefined) validateFalls(err, unknown, at, o, { frames, scene, carry: true });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (o?.keys !== undefined || o?.fall !== undefined) err(`${at}: a hinge turns by its channel "${o.channel}", keyed in spec.keys; keys and fall move an object with no pivot, axis or channel (C17), or a carried one (prop)`);
|
|
139
|
+
if (!isVec3(o?.pivot)) err(`${at}.pivot must be [x, y, z] (world hinge point), got ${fmt(o?.pivot)}`);
|
|
140
|
+
if (o?.axis !== undefined && (!isVec3(o.axis) || Math.hypot(...o.axis) < 1e-9)) err(`${at}.axis must be a non-zero [x, y, z], got ${fmt(o.axis)}`);
|
|
141
|
+
if (typeof o?.channel !== "string" || !o.channel) err(`${at}.channel must name the angle channel (degrees)`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Validation of the spec parts other than keys: anchors, props, fits, generators, checks and
|
|
2
|
+
// fix; objects (validate-objects.js) and goals (validate-goals.js) run from here too. Unknown
|
|
3
|
+
// fields go through opts.unknown, which ignores them on a lenient reload. See validate.js.
|
|
4
|
+
import { didYouMean, suggestField, CONVENTIONS, isVec3, fmt } from "./vocab.js";
|
|
5
|
+
import { FIX_KEYS } from "../fix/index.js";
|
|
6
|
+
import { QA_LIMITS } from "../qa/checks.js";
|
|
7
|
+
import { ANCHOR_FIELDS, PROP_FIELDS, FIT_FIELDS, WALK_FIELDS, CHECK_FIELDS, CHECK_ENTRY_FIELDS } from "../schema/fields.js";
|
|
8
|
+
import { boneOk, objectOk, refOk } from "./validate-common.js";
|
|
9
|
+
import { validateObjects } from "./validate-objects.js";
|
|
10
|
+
import { validateGoals } from "./validate-goals.js";
|
|
11
|
+
|
|
12
|
+
const SIDE = new Set(["l", "r"]);
|
|
13
|
+
const isObject = v => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
|
|
15
|
+
/** Every field of obj not in fields goes to unknown, with a suggestion and the list. */
|
|
16
|
+
function fieldsOk(unknown, at, obj, fields) {
|
|
17
|
+
if (!isObject(obj)) return;
|
|
18
|
+
for (const f of Object.keys(obj)) if (!fields.includes(f)) unknown(`${at} has unknown field "${f}"${suggestField(f, fields)} (fields: ${fields.join(", ")})`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateAnchors(spec, err, { sk, scene, unknown = err }) {
|
|
22
|
+
for (const [name, a] of Object.entries(spec.anchors ?? {})) {
|
|
23
|
+
const at = `anchors.${name}`;
|
|
24
|
+
if (/_[lr]$/.test(name)) err(`${at}: an anchor name may not end in _l or _r (the weight channel w_${name}_l would read as anchor "${name.slice(0, -2)}"); the side goes on the weight channel, so name it "${name.slice(0, -2)}"`);
|
|
25
|
+
if (!a || !ANCHOR_FIELDS[a.kind]) { err(`${at}.kind must be one of ${Object.keys(ANCHOR_FIELDS).join(", ")}, got ${fmt(a?.kind)}`); continue; }
|
|
26
|
+
fieldsOk(unknown, `${at} (${a.kind})`, a, ["kind", ...ANCHOR_FIELDS[a.kind]]);
|
|
27
|
+
for (const f of ["p", "offset", "local"]) if (a[f] !== undefined && !isVec3(a[f])) err(`${at}.${f} must be [x, y, z], got ${fmt(a[f])}`);
|
|
28
|
+
if ((a.kind === "world" || a.kind === "body") && a.p === undefined) err(`${at} (${a.kind}) needs p: [x, y, z]${a.kind === "body" ? `, ${CONVENTIONS.pole}` : ""}`);
|
|
29
|
+
if (a.kind === "bone") boneOk(err, sk, at, a.bone);
|
|
30
|
+
if (a.kind === "segment") { boneOk(err, sk, `${at}.from`, a.from); boneOk(err, sk, `${at}.to`, a.to); if (a.reach !== undefined && !SIDE.has(a.reach)) err(`${at}.reach must be "l" or "r"`); }
|
|
31
|
+
if (a.kind === "object") objectOk(err, scene, at, a.object, { groups: true });
|
|
32
|
+
if (a.kind === "landmark" && a.name !== "mouth") err(`${at}: the only landmark is "mouth", got ${fmt(a.name)}`);
|
|
33
|
+
if (a.kind === "prop" && !spec.props?.[a.prop]?.points?.[a.point] && a.point !== "origin") err(`${at} needs prop point ${a.prop}.${a.point}, which spec.props does not define`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateProps(spec, err, { sk, unknown = err }) {
|
|
38
|
+
for (const [name, p] of Object.entries(spec.props ?? {})) {
|
|
39
|
+
const at = `props.${name}`;
|
|
40
|
+
fieldsOk(unknown, at, p, PROP_FIELDS);
|
|
41
|
+
if (!SIDE.has(p?.hand)) err(`${at}.hand must be "l" or "r", got ${fmt(p?.hand)}`);
|
|
42
|
+
if (!Array.isArray(p?.origin) || !p.origin.length) err(`${at}.origin must list one or more bones`);
|
|
43
|
+
else p.origin.forEach(b => boneOk(err, sk, `${at}.origin`, b));
|
|
44
|
+
for (const [pt, v] of Object.entries(p?.points ?? {})) if (!isVec3(v)) err(`${at}.points.${pt} must be [d, l, n] along the palm axes, got ${fmt(v)}`);
|
|
45
|
+
}
|
|
46
|
+
(spec.fits ?? []).forEach((fit, i) => {
|
|
47
|
+
const at = `fits[${i}]`;
|
|
48
|
+
fieldsOk(unknown, at, fit, FIT_FIELDS);
|
|
49
|
+
if (!SIDE.has(fit?.side)) err(`${at}.side must be "l" or "r", got ${fmt(fit?.side)}`);
|
|
50
|
+
if (!spec.anchors?.[fit?.anchor]) err(`${at}.anchor "${fit?.anchor}" is not in spec.anchors${didYouMean(String(fit?.anchor), Object.keys(spec.anchors ?? {}))}`);
|
|
51
|
+
const e = fit?.effector;
|
|
52
|
+
if (e?.prop !== undefined) { if (!spec.props?.[e.prop]?.points?.[e.point]) err(`${at}.effector ${e.prop}.${e.point} is not a point of spec.props`); }
|
|
53
|
+
else if (Array.isArray(e?.joints)) e.joints.forEach(j => boneOk(err, sk, `${at}.effector`, j));
|
|
54
|
+
else err(`${at}.effector must be { prop, point } or { joints: [bone, ...] }`);
|
|
55
|
+
if (fit?.offset !== undefined && !isVec3(fit.offset)) err(`${at}.offset must be [x, y, z]`);
|
|
56
|
+
if (fit?.orient !== undefined && typeof fit.orient !== "boolean") err(`${at}.orient must be true or false (false: the hand takes no grip turn), got ${fmt(fit.orient)}`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateGenerators(spec, err, { frames, unknown = err, past = "" }) {
|
|
61
|
+
(spec.generators ?? []).forEach((g, i) => {
|
|
62
|
+
const at = `generators[${i}]`;
|
|
63
|
+
if (g?.type !== "walk") return err(`${at}.type must be "walk", got ${fmt(g?.type)}`);
|
|
64
|
+
fieldsOk(unknown, at, g, WALK_FIELDS);
|
|
65
|
+
if (!Number.isInteger(g.f0) || g.f0 < 0 || g.f0 > frames - 1) err(`${at}.f0 must be an integer frame within 0..${frames - 1}, got ${fmt(g.f0)}${g.f0 > frames - 1 ? past : ""}`);
|
|
66
|
+
if (g.heading !== undefined && !(Array.isArray(g.heading) && g.heading.length === 2 && g.heading.every(Number.isFinite))) err(`${at}.heading must be [h0, h1] in degrees (${CONVENTIONS.yaw}), got ${fmt(g.heading)}`);
|
|
67
|
+
for (const f of WALK_FIELDS.slice(2)) if (f !== "heading" && g[f] !== undefined && !Number.isFinite(g[f])) err(`${at}.${f} must be a finite number, got ${fmt(g[f])}`);
|
|
68
|
+
if (g.steps !== undefined && !(Number.isInteger(g.steps) && g.steps >= 1)) err(`${at}.steps must be an integer >= 1, got ${fmt(g.steps)}`);
|
|
69
|
+
if (g.turnDistance !== undefined && !(g.turnDistance > 0)) err(`${at}.turnDistance must be > 0 m, got ${fmt(g.turnDistance)}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateChecks(spec, err, { sk, scene, frames, unknown = err, past = "" }) {
|
|
74
|
+
const c = spec.checks ?? {};
|
|
75
|
+
for (const k of Object.keys(c)) if (!CHECK_FIELDS.includes(k)) unknown(`checks.${k} is not a check${didYouMean(k, CHECK_FIELDS)} (checks: ${CHECK_FIELDS.join(", ")})`);
|
|
76
|
+
// each list entry is an object with known fields; a bad entry is named once and skipped
|
|
77
|
+
const each = (name, check = () => {}) => {
|
|
78
|
+
const v = c[name], shape = `{ ${CHECK_ENTRY_FIELDS[name].join(", ")} }`;
|
|
79
|
+
if (v === undefined) return;
|
|
80
|
+
if (!Array.isArray(v)) return err(`checks.${name} must be an array of ${shape}, got ${fmt(v)}`);
|
|
81
|
+
v.forEach((k, i) => {
|
|
82
|
+
if (!isObject(k)) return err(`checks.${name}[${i}] must be ${shape}, got ${fmt(k)}`);
|
|
83
|
+
fieldsOk(unknown, `checks.${name}[${i}]`, k, CHECK_ENTRY_FIELDS[name]);
|
|
84
|
+
check(k, i);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
const when = (at, w) => {
|
|
88
|
+
if (w === undefined || typeof w === "string") return;
|
|
89
|
+
if (!(Array.isArray(w) && w.length === 2 && w.every(Number.isInteger) && w[0] <= w[1] && w[0] >= 0 && w[1] <= frames - 1)) err(`${at}.when must be a weight channel or [f0, f1] within 0..${frames - 1}, got ${fmt(w)}${Array.isArray(w) && w[1] > frames - 1 ? past : ""}`);
|
|
90
|
+
};
|
|
91
|
+
each("contacts", (k, i) => { refOk(spec, err, sk, `checks.contacts[${i}].point`, k.point); refOk(spec, err, sk, `checks.contacts[${i}].target`, k.target); when(`checks.contacts[${i}]`, k.when); });
|
|
92
|
+
each("orient", (k, i) => {
|
|
93
|
+
refOk(spec, err, sk, `checks.orient[${i}].from`, k.from); refOk(spec, err, sk, `checks.orient[${i}].to`, k.to); when(`checks.orient[${i}]`, k.when);
|
|
94
|
+
if (!(k.along === "face" || k.along === "up" || isVec3(k.along))) err(`checks.orient[${i}].along must be "face", "up" or [x, y, z]`);
|
|
95
|
+
});
|
|
96
|
+
each("below", (k, i) => { boneOk(err, sk, `checks.below[${i}].joint`, k.joint); boneOk(err, sk, `checks.below[${i}].ref`, k.ref); when(`checks.below[${i}]`, k.when); });
|
|
97
|
+
each("clearance", (k, i) => { objectOk(err, scene, `checks.clearance[${i}]`, k.object); when(`checks.clearance[${i}]`, k.frames); });
|
|
98
|
+
each("appear", (k, i) => { boneOk(err, sk, `checks.appear[${i}].joint`, k.joint); if (!spec.anchors?.[k.anchor]) err(`checks.appear[${i}].anchor "${k.anchor}" is not in spec.anchors`); });
|
|
99
|
+
each("supports", (k, i) => objectOk(err, scene, `checks.supports[${i}]`, k.object));
|
|
100
|
+
each("allow");
|
|
101
|
+
for (const name of Object.keys(c.limits ?? {})) if (!(name in QA_LIMITS)) unknown(`checks.limits.${name} is not a QA limit${didYouMean(name, Object.keys(QA_LIMITS))}`);
|
|
102
|
+
const fix = spec.fix;
|
|
103
|
+
if (fix !== undefined && fix !== false && (typeof fix !== "object" || fix === null)) err("spec.fix must be false or { reach, slide, penetration, self, retime }");
|
|
104
|
+
else for (const k of Object.keys(fix || {})) if (!FIX_KEYS.includes(k)) unknown(`spec.fix.${k} is not a fix${didYouMean(k, FIX_KEYS)} (fixes: ${FIX_KEYS.join(", ")})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** opts = { sk, scene, frames, lenient, unknown: err for an unknown field (a no-op when lenient), past: the
|
|
108
|
+
* advice a frame past the end gets (validate.js) }. */
|
|
109
|
+
export function validateParts(spec, err, opts) {
|
|
110
|
+
validateAnchors(spec, err, opts);
|
|
111
|
+
validateProps(spec, err, opts);
|
|
112
|
+
validateObjects(spec, err, opts);
|
|
113
|
+
validateGenerators(spec, err, opts);
|
|
114
|
+
validateChecks(spec, err, opts);
|
|
115
|
+
validateGoals(spec, err, opts);
|
|
116
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Strict motion spec validation with exact messages, run before every compile through the
|
|
2
|
+
// pipeline (after skills expand, with open parameters at a trial value). Every problem found is
|
|
3
|
+
// listed at once, one per line, so the agent fixes them in one edit.
|
|
4
|
+
import { VEC_CHANNELS, POLE_CHANNELS, NUM_CHANNELS, MAP_CHANNELS, FINGER_PRESETS, rangeOf, CONVENTIONS, didYouMean, suggestField, isVec3, fmt } from "./vocab.js";
|
|
5
|
+
import { validateParts } from "./validate-parts.js";
|
|
6
|
+
import { SPEC_FIELDS, KEY_FIELDS } from "../schema/fields.js";
|
|
7
|
+
const MAX_ERRORS = 12;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/** Channels other than the body's own: hinge channels, visible and level channels (C15), channels a check names, *_vis switches, spec.channels. */
|
|
11
|
+
function customChannels(spec) {
|
|
12
|
+
const out = new Set(spec.channels ?? []);
|
|
13
|
+
for (const o of Object.values(spec.objects ?? {})) for (const ch of [o?.channel, o?.visible, o?.level]) if (typeof ch === "string" && ch) out.add(ch);
|
|
14
|
+
const c = spec.checks ?? {};
|
|
15
|
+
const list = v => (Array.isArray(v) ? v.filter(k => k && typeof k === "object") : []);
|
|
16
|
+
for (const k of [c.contacts, c.orient, c.below, c.clearance].flatMap(list)) if (typeof k.when === "string") out.add(k.when);
|
|
17
|
+
for (const k of list(c.appear)) if (typeof k.channel === "string") out.add(k.channel);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function checkValue(err, at, ch, v) {
|
|
22
|
+
if (VEC_CHANNELS.has(ch)) {
|
|
23
|
+
if (!isVec3(v)) return err(`${at} ${ch} must be [x, y, z] (3 finite numbers), got ${fmt(v)}`);
|
|
24
|
+
if (POLE_CHANNELS.has(ch)) {
|
|
25
|
+
const l = Math.hypot(...v);
|
|
26
|
+
if (l < 1e-6) return err(`${at} ${ch} = ${fmt(v)} has no direction; a pole is ${CONVENTIONS.pole}`);
|
|
27
|
+
if (ch.startsWith("knee") && v[2] < -0.7 * l) err(`${at} ${ch} = ${fmt(v)} points backward; knees bend toward +z. A pole is ${CONVENTIONS.pole}`);
|
|
28
|
+
}
|
|
29
|
+
if (ch.startsWith("foot") && v[1] < 0.04) err(`${at} ${ch} y = ${v[1]} puts the ankle into the floor; foot_* is ${CONVENTIONS.foot}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (MAP_CHANNELS.has(ch)) {
|
|
33
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return err(`${at} ${ch} must be { preset: weight }, presets ${FINGER_PRESETS.join(", ")}; got ${fmt(v)}`);
|
|
34
|
+
for (const [p, w] of Object.entries(v)) {
|
|
35
|
+
if (!FINGER_PRESETS.includes(p)) err(`${at} ${ch} preset "${p}" is unknown${didYouMean(p, FINGER_PRESETS)} (presets: ${FINGER_PRESETS.join(", ")})`);
|
|
36
|
+
else if (!(Number.isFinite(w) && w >= 0 && w <= 1)) err(`${at} ${ch}.${p} must be a weight within 0..1, got ${fmt(w)}`);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isFinite(v)) {
|
|
41
|
+
const conv = ch === "yaw" ? ` (degrees; ${CONVENTIONS.yaw})` : ch.startsWith("fyaw") ? ` (${CONVENTIONS.fyaw})` : "";
|
|
42
|
+
return err(`${at} ${ch} must be a finite number${conv}, got ${fmt(v)}`);
|
|
43
|
+
}
|
|
44
|
+
const r = rangeOf(ch);
|
|
45
|
+
if (r && (v < r[1] || v > r[2])) err(`${at} ${ch} = ${v} is outside ${r[1]}..${r[2]} (${r[3]})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const list = v => (Array.isArray(v) ? v : []);
|
|
49
|
+
const lastOf = r => (Array.isArray(r) ? r[1] : r);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The advice an "outside 0..frames-1" message ends with: the length every frame the spec names
|
|
53
|
+
* needs (keys, walk starts, check windows, goals), and frames: "auto" when that covers it ("auto"
|
|
54
|
+
* counts keys and walks, not check windows or goals; skills/frames-auto.js).
|
|
55
|
+
*/
|
|
56
|
+
function pastEndAdvice(spec, frames) {
|
|
57
|
+
const counted = [...list(spec.keys).map(k => k?.f), ...list(spec.generators).map(g => g?.f0)].filter(Number.isInteger);
|
|
58
|
+
const c = spec.checks ?? {};
|
|
59
|
+
const windows = [...["contacts", "orient", "below"].flatMap(n => list(c[n]).map(k => lastOf(k?.when))), ...list(c.clearance).map(k => lastOf(k?.frames)),
|
|
60
|
+
...list(spec.goals).map(g => lastOf(g?.at))].filter(Number.isInteger);
|
|
61
|
+
const need = Math.max(frames - 1, ...counted, ...windows) + 1;
|
|
62
|
+
return `; set frames >= ${need}${Math.max(0, ...counted) + 1 >= need ? " or frames: \"auto\"" : ""}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateKeys(spec, err, frames, unknown, past = "") {
|
|
66
|
+
if (spec.keys !== undefined && !Array.isArray(spec.keys)) return err("spec.keys must be an array of { f, set, step? }");
|
|
67
|
+
const anchors = Object.keys(spec.anchors ?? {}), custom = customChannels(spec);
|
|
68
|
+
const known = [...VEC_CHANNELS, ...NUM_CHANNELS, ...MAP_CHANNELS, ...custom, ...anchors.flatMap(a => [`w_${a}_l`, `w_${a}_r`])];
|
|
69
|
+
(spec.keys ?? []).forEach((key, i) => {
|
|
70
|
+
const name = key?.src ? (key.src.startsWith("keys[") ? key.src : `${key.src} key`) : `keys[${i}]`;
|
|
71
|
+
if (!key || typeof key !== "object") return err(`${name} must be { f, set, step? }`);
|
|
72
|
+
for (const field of Object.keys(key)) if (!KEY_FIELDS.includes(field)) unknown(`${name} has unknown field "${field}"${suggestField(field, KEY_FIELDS)}; a key is { f, set, step? }`);
|
|
73
|
+
if (!Number.isInteger(key.f)) return err(`${name}.f must be an integer frame, got ${fmt(key.f)}`);
|
|
74
|
+
if (key.f < 0 || key.f > frames - 1) err(`${name}.f = ${key.f} is outside 0..${frames - 1} (spec.frames ${frames})${key.f > 0 ? past : ""}`);
|
|
75
|
+
const at = `${name} (f${key.f})`;
|
|
76
|
+
if (!key.set || typeof key.set !== "object" || Array.isArray(key.set)) return err(`${at} needs set: { channel: value }`);
|
|
77
|
+
for (const [ch, v] of Object.entries(key.set)) {
|
|
78
|
+
const w = /^w_(.+)_([lr])$/.exec(ch);
|
|
79
|
+
if (w && !anchors.includes(w[1])) err(`${at} sets ${ch}, but there is no anchor "${w[1]}"${didYouMean(ch, known)}`);
|
|
80
|
+
else if (w) checkValue(err, at, ch, v);
|
|
81
|
+
else if (VEC_CHANNELS.has(ch) || NUM_CHANNELS.has(ch) || MAP_CHANNELS.has(ch)) checkValue(err, at, ch, v);
|
|
82
|
+
else if (custom.has(ch) || ch.endsWith("_vis")) { if (!Number.isFinite(v)) err(`${at} ${ch} must be a finite number, got ${fmt(v)}`); }
|
|
83
|
+
else err(`${at} sets unknown channel "${ch}"${didYouMean(ch, known)}`);
|
|
84
|
+
}
|
|
85
|
+
for (const ch of key.step ?? []) if (!(ch in key.set)) err(`${at} step names "${ch}", which this key does not set`);
|
|
86
|
+
if (key.step !== undefined && !Array.isArray(key.step)) err(`${at} step must be an array of channel names`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Throws MOTION_SPEC_INVALID listing every problem; scene objects are checked when given.
|
|
92
|
+
* lenient (a saved take compiled again, take-source.js) ignores unknown fields.
|
|
93
|
+
*/
|
|
94
|
+
/** spec.flow: false, or { smooth?: boolean, lag?: false | 0..6 frames | { spine?, head?, arms? } } (channels.js). */
|
|
95
|
+
function validateFlow(flow, err, unknown) {
|
|
96
|
+
if (flow === undefined || flow === false) return;
|
|
97
|
+
if (!flow || typeof flow !== "object" || Array.isArray(flow)) return err(`spec.flow must be false or { smooth?, lag? }, got ${fmt(flow)}`);
|
|
98
|
+
for (const k of Object.keys(flow)) if (k !== "smooth" && k !== "lag") unknown(`spec.flow has unknown field "${k}" (fields: smooth, lag)`);
|
|
99
|
+
if (flow.smooth !== undefined && typeof flow.smooth !== "boolean") err(`spec.flow.smooth must be true or false, got ${fmt(flow.smooth)}`);
|
|
100
|
+
const frames = v => Number.isFinite(v) && v >= 0 && v <= 6, lag = flow.lag, LAG = "false, 0..6 frames or { spine?, head?, arms? }";
|
|
101
|
+
if (lag === undefined || lag === false || frames(lag)) return;
|
|
102
|
+
if (!lag || typeof lag !== "object" || Array.isArray(lag)) return err(`spec.flow.lag must be ${LAG}, got ${fmt(lag)}`);
|
|
103
|
+
for (const [k, v] of Object.entries(lag)) {
|
|
104
|
+
if (!["spine", "head", "arms"].includes(k)) unknown(`spec.flow.lag has unknown field "${k}" (fields: spine, head, arms)`);
|
|
105
|
+
else if (!frames(v)) err(`spec.flow.lag.${k} must be 0..6 frames, got ${fmt(v)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function validateSpec(spec, { sk, scene, lenient = false } = {}) {
|
|
110
|
+
const errors = [], err = m => { if (errors.length < MAX_ERRORS) errors.push(m); }, unknown = lenient ? () => {} : err;
|
|
111
|
+
if (!spec || typeof spec !== "object") throw new Error("MOTION_SPEC_INVALID: spec must be an object");
|
|
112
|
+
if (spec.schema !== "cozyclay.motion-spec.v1") err(`spec.schema must be "cozyclay.motion-spec.v1", got ${fmt(spec.schema)}`);
|
|
113
|
+
for (const k of Object.keys(spec)) if (!SPEC_FIELDS.includes(k)) unknown(`spec.${k} is not a spec field${didYouMean(k, SPEC_FIELDS)} (fields: ${SPEC_FIELDS.join(", ")})`);
|
|
114
|
+
if (spec.fps !== undefined && spec.fps !== 24) err(`spec.fps must be 24, the timeline rate, got ${fmt(spec.fps)}`);
|
|
115
|
+
validateFlow(spec.flow, err, unknown);
|
|
116
|
+
const frames = spec.frames;
|
|
117
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > 20000) err(`spec.frames must be an integer 1..20000 or "auto", got ${fmt(frames)}`);
|
|
118
|
+
const past = Number.isInteger(frames) ? pastEndAdvice(spec, frames) : "";
|
|
119
|
+
validateKeys(spec, err, Number.isInteger(frames) ? frames : 20000, unknown, past);
|
|
120
|
+
validateParts(spec, err, { sk, scene, frames: Number.isInteger(frames) ? frames : 20000, lenient, unknown, past });
|
|
121
|
+
if (errors.length) throw new Error(`MOTION_SPEC_INVALID: ${errors.join("\n")}${errors.length >= MAX_ERRORS ? "\n(more problems not listed)" : ""}`);
|
|
122
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The motion spec's channel vocabulary and sign conventions, shared by the validator (exact
|
|
2
|
+
// error messages) and QA hints. Degrees for angles, metres for points.
|
|
3
|
+
|
|
4
|
+
const sided = names => names.flatMap(n => [`${n}_l`, `${n}_r`]);
|
|
5
|
+
|
|
6
|
+
export const VEC_CHANNELS = new Set(["root", ...sided(["foot", "knee", "hand", "elbow"])]);
|
|
7
|
+
export const POLE_CHANNELS = new Set(sided(["knee", "elbow"]));
|
|
8
|
+
export const NUM_CHANNELS = new Set(["yaw", "pitch", "roll", "spine_yaw", "spine_pitch", "spine_roll", "head_yaw", "head_pitch", "head_roll", "arm_rest", "arm_hang",
|
|
9
|
+
...sided(["fyaw", "fpitch", "armw", "wtwist", "wflex", "aswing", "abend"])]);
|
|
10
|
+
export const MAP_CHANNELS = new Set(sided(["fingers"]));
|
|
11
|
+
// Channels the skills key (standIdle, walks): valid in a spec, left out of motion.describe so a first spec need not read them.
|
|
12
|
+
export const SKILL_CHANNELS = new Set(["arm_hang"]);
|
|
13
|
+
export const FINGER_PRESETS = ["relaxed", "fist", "grip", "cigarette", "point"];
|
|
14
|
+
|
|
15
|
+
// [min, max, convention] for numeric channels whose values have a hard range.
|
|
16
|
+
const RANGES = [
|
|
17
|
+
[/^(armw)_[lr]$/, 0, 1, "0 = FK arm, 1 = IK to the hand target"],
|
|
18
|
+
[/^arm_hang$/, 0, 1, "1 = a free FK forearm hangs plumb"],
|
|
19
|
+
[/^w_.+_[lr]$/, 0, 1, "a hand-target weight; the weights of one hand add up to at most 1"],
|
|
20
|
+
[/^wtwist_[lr]$/, -85, 85, "wrist twist about the forearm in degrees"],
|
|
21
|
+
[/^wflex_[lr]$/, -70, 70, "wrist flex about the knuckle line in degrees"],
|
|
22
|
+
[/^fpitch_[lr]$/, -60, 90, "toe-down pitch in degrees: + lifts the heel (the toes stay flat), - lifts the toes"],
|
|
23
|
+
[/^(pitch|spine_pitch|head_pitch)$/, -120, 120, "degrees, + bends forward"],
|
|
24
|
+
[/^(roll|spine_roll|head_roll)$/, -90, 90, "degrees"],
|
|
25
|
+
];
|
|
26
|
+
export const rangeOf = ch => RANGES.find(([re]) => re.test(ch)) ?? null;
|
|
27
|
+
|
|
28
|
+
export const CONVENTIONS = {
|
|
29
|
+
yaw: "yaw 0 faces +Z and + turns toward +X",
|
|
30
|
+
fyaw: "foot heading in degrees, as yaw",
|
|
31
|
+
pole: "a body-frame direction (turned by the root yaw only): +z forward, +x the character's left, +y up",
|
|
32
|
+
foot: "the ANKLE target in world metres (standing: 0.087 X Bot, 0.105 Y Bot)",
|
|
33
|
+
hand: "the WRIST target in world metres",
|
|
34
|
+
root: "the hips joint in world metres (standing: 1.03 X Bot, 0.99 Y Bot)",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** What each channel family means (a family drops the _l/_r side), for motion.describe "channels". */
|
|
38
|
+
export const CHANNEL_DOCS = {
|
|
39
|
+
root: CONVENTIONS.root, yaw: `degrees; ${CONVENTIONS.yaw}`, pitch: "whole-body pitch, degrees", roll: "whole-body roll, degrees",
|
|
40
|
+
spine_yaw: "degrees", spine_pitch: "degrees, + bends forward", spine_roll: "degrees", head_yaw: "degrees", head_pitch: "degrees, + looks down", head_roll: "degrees",
|
|
41
|
+
arm_rest: "FK arms down from the T-pose (default 72)", arm_hang: "1 = a free FK forearm hangs plumb (standIdle and walks key it)", foot: CONVENTIONS.foot, fyaw: CONVENTIONS.fyaw, fpitch: "toe-down pitch, degrees, + heel up",
|
|
42
|
+
knee: `knee pole, ${CONVENTIONS.pole}`, armw: "0 = FK arm, 1 = IK to hand_* / the anchors", hand: CONVENTIONS.hand, elbow: `elbow pole, ${CONVENTIONS.pole}`,
|
|
43
|
+
wtwist: "wrist twist, degrees", wflex: "wrist flex, degrees", aswing: "FK arm swing, degrees, + forward", abend: "FK elbow bend, degrees",
|
|
44
|
+
fingers: "{ preset: weight 0..1 }",
|
|
45
|
+
};
|
|
46
|
+
export const channelFamily = ch => ch.replace(/_[lr]$/, "");
|
|
47
|
+
|
|
48
|
+
/** "; did you mean ..." for an unknown field: frame words point at the field that takes frames, distance words at the tolerance. */
|
|
49
|
+
const FIELD_ALIASES = [[["frames", "when", "range", "f", "from", "within", "during", "at"], ["when", "frames", "at", "f"]], [["max", "maxDist", "distance", "tolerance", "limit"], ["tol", "min"]]];
|
|
50
|
+
export function suggestField(name, fields) {
|
|
51
|
+
for (const [words, targets] of FIELD_ALIASES) {
|
|
52
|
+
const to = words.includes(name) && targets.find(t => fields.includes(t) && t !== name);
|
|
53
|
+
if (to) return `; did you mean "${to}"?`;
|
|
54
|
+
}
|
|
55
|
+
return didYouMean(name, fields, Math.max(2, Math.floor(name.length / 3)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const isVec3 = v => Array.isArray(v) && v.length === 3 && v.every(Number.isFinite);
|
|
59
|
+
export const fmt = v => JSON.stringify(v)?.slice(0, 60);
|
|
60
|
+
|
|
61
|
+
/** Levenshtein distance, for "did you mean" suggestions. */
|
|
62
|
+
function editDistance(a, b) {
|
|
63
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
64
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
65
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
66
|
+
return d[a.length][b.length];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function didYouMean(name, options, maxD = Math.max(3, Math.floor(name.length / 3))) {
|
|
70
|
+
let best = null, bestD = Infinity;
|
|
71
|
+
for (const o of options) { const d = editDistance(name, o); if (d < bestD) { best = o; bestD = d; } }
|
|
72
|
+
return best && bestD <= maxD ? `; did you mean "${best}"?` : "";
|
|
73
|
+
}
|