cozyclay 2.1.1 → 3.0.1
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 +88 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +43 -116
- package/bin/agent/agent-runner.mjs +14 -15
- 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/session-store.mjs +2 -2
- package/bin/agent/studio-context-budget.mjs +93 -0
- 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-BWCqdgWA.js +1 -0
- package/dist/assets/app-DfTCxaKk.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-BxKXh07s.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
- package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-D78eiKTa.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 +151 -2625
- package/src/agent-progress.js +80 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +12 -2
- 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 +10 -8
- 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 +214 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +55 -13
- 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 +53 -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 +47 -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 +60 -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 +79 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +113 -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 +197 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +177 -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 +144 -0
- package/src/motion3/compiler/validate-parts.js +118 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +74 -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 +177 -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 +204 -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/people.js +82 -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 +125 -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 +195 -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 +44 -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/people-checks.js +37 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +228 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +129 -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 +113 -0
- package/src/motion3/schema/types.js +149 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/settle.js +136 -0
- package/src/motion3/skills/basic.js +83 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/flip.js +200 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/impact.js +109 -0
- package/src/motion3/skills/index.js +85 -0
- package/src/motion3/skills/info.js +80 -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/qa-label.js +25 -0
- 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 +16 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +47 -77
- package/src/studio-app-binding.js +27 -47
- 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 +15 -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 +109 -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 +9 -125
- 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,102 @@
|
|
|
1
|
+
// One compile as motion.compile runs it: frames "auto" resolves (skills/frames-auto.js), open
|
|
2
|
+
// parameters are searched (search.js), unnamed checks get their index as a name (on a copy,
|
|
3
|
+
// qa/check-names.js), skills expand (skills/), the spec is validated strictly (leniently for a
|
|
4
|
+
// saved take compiled again), the compiler bakes and auto-fixes the frames, QA measures them with
|
|
5
|
+
// a cause and a hint per issue group against the agent's own spec, the style quantities are
|
|
6
|
+
// measured (qa/style.js observeMotion; keyed objects: every key as drawn, qa/key-checks.js
|
|
7
|
+
// heldReadings; each hand and foot against the nearest object, qa/near.js observeNear; each holding
|
|
8
|
+
// wrist's bend, qa/wrist-obs.js observeWrist; a skill
|
|
9
|
+
// paced to fit a short span: observations.paced; a skill whose release or
|
|
10
|
+
// settle the last frame cut: observations.cut), and the timeline and the
|
|
11
|
+
// requested probes read the result.
|
|
12
|
+
// liftRequest first takes what the agent obviously meant: spec.probes are request probes, and a
|
|
13
|
+
// bare goals[].moved id is object:<id>; relaxSpec (lenient.js) fixes mechanical slips in the expanded
|
|
14
|
+
// spec before validation and the answer lists each fix in notes.
|
|
15
|
+
import { compileMotion } from "./compiler/compile.js";
|
|
16
|
+
import { validateSpec } from "./compiler/validate.js";
|
|
17
|
+
import { runMotionQa } from "./qa/index.js";
|
|
18
|
+
import { observeMotion } from "./qa/style.js";
|
|
19
|
+
import { nameChecks } from "./qa/check-names.js";
|
|
20
|
+
import { expandUse } from "./skills/index.js";
|
|
21
|
+
import { resolveFrames } from "./skills/frames-auto.js";
|
|
22
|
+
import { markFramesAuto } from "./spec-store.js";
|
|
23
|
+
import { findParams, searchParams } from "./search.js";
|
|
24
|
+
import { timelineOf } from "./timeline.js";
|
|
25
|
+
import { runProbes } from "./probes.js";
|
|
26
|
+
import { canonicalGoals } from "./compiler/validate-goals.js";
|
|
27
|
+
import { heldReadings, HELD_UNITS } from "./qa/key-checks.js";
|
|
28
|
+
import { observeNear, NEAR_UNITS } from "./qa/near.js";
|
|
29
|
+
import { observeWrist, WRIST_UNITS } from "./qa/wrist-obs.js";
|
|
30
|
+
import { planWalks, applyWalkPlans } from "./walk-plan.js";
|
|
31
|
+
import { relaxSpec } from "./lenient.js";
|
|
32
|
+
|
|
33
|
+
const ms = t => Math.round(performance.now() - t);
|
|
34
|
+
|
|
35
|
+
/** A spec after retime: frame-range checks follow the keys they were written against. */
|
|
36
|
+
function warpChecks(spec, map) {
|
|
37
|
+
const c = structuredClone(spec.checks ?? {}), w = r => (Array.isArray(r) ? [Math.round(map(r[0])), Math.round(map(r[1]))] : r);
|
|
38
|
+
for (const name of ["contacts", "orient", "below"]) for (const k of c[name] ?? []) k.when = w(k.when);
|
|
39
|
+
for (const k of c.clearance ?? []) k.frames = w(k.frames);
|
|
40
|
+
return { ...spec, checks: c };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* liftRequest(spec, probes) -> { spec, probes }. spec.probes (sent inside the spec, or added by
|
|
45
|
+
* a patch) join the request's probes after them and leave the spec, so they are never stored and
|
|
46
|
+
* never change its specId; bare goals[].moved ids get their object: prefix (validate-goals.js).
|
|
47
|
+
* The spec comes back as the same object when nothing changes.
|
|
48
|
+
*/
|
|
49
|
+
export function liftRequest(spec, probes) {
|
|
50
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) return { spec, probes };
|
|
51
|
+
const goals = canonicalGoals(spec.goals);
|
|
52
|
+
if (spec.probes === undefined && goals === spec.goals) return { spec, probes };
|
|
53
|
+
const { probes: inSpec, ...rest } = spec;
|
|
54
|
+
const lifted = inSpec === undefined ? probes : probes ? [...probes, ...(Array.isArray(inSpec) ? inSpec : [inSpec])] : inSpec;
|
|
55
|
+
return { spec: goals === spec.goals ? rest : { ...rest, goals }, probes: lifted };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* spec -> { out, qa, observations, spec (frames and open values filled in), expanded, used,
|
|
60
|
+
* fixed, search, timeline, probes?, notes?, ms }. opts.checks replaces every check (the spec's and its skills') for a
|
|
61
|
+
* re-measure; opts.lenient ignores unknown fields (take-source.js); opts.probes are measured
|
|
62
|
+
* (with spec.probes, liftRequest).
|
|
63
|
+
*/
|
|
64
|
+
export function compileSpec(sent, { sk, scene }, opts = {}) {
|
|
65
|
+
const { spec, probes: asked } = liftRequest(sent, opts.probes);
|
|
66
|
+
const t0 = performance.now(), ctx = { sk, scene, lenient: opts.lenient === true }, notes = [];
|
|
67
|
+
const checks = opts.checks && nameChecks({ checks: opts.checks }).checks;
|
|
68
|
+
let plans = [];
|
|
69
|
+
const expand = s => {
|
|
70
|
+
const { spec: exp, used } = expandUse(nameChecks(applyWalkPlans(s, plans)), ctx);
|
|
71
|
+
// plain JSON objects compile about twice as fast as structuredClone'd ones (V8 shapes)
|
|
72
|
+
const full = relaxSpec(JSON.parse(JSON.stringify(checks ? { ...exp, checks } : exp)), scene, notes);
|
|
73
|
+
validateSpec(full, ctx);
|
|
74
|
+
return { full, used };
|
|
75
|
+
};
|
|
76
|
+
// a walkTo with arrive gets its cadence planned on probe compiles of the walk alone (walk-plan.js); the spec keeps arrive
|
|
77
|
+
plans = planWalks(spec, { sk, scene, trial: s => { const full = expand(s).full; return runMotionQa(sk, compileMotion({ fps: 24, ...full }, ctx), full, { explain: false }); } });
|
|
78
|
+
const framed = spec?.frames === "auto" && plans.length ? { ...spec, frames: resolveFrames(applyWalkPlans(spec, plans), ctx).frames } : resolveFrames(spec, ctx), params = findParams(framed);
|
|
79
|
+
let resolved = framed, search = null;
|
|
80
|
+
if (params.length) ({ spec: resolved, search } = searchParams(framed, params, ctx, s => expand(s).full));
|
|
81
|
+
// the store keeps the "auto" intent beside the resolved number (spec-store.js)
|
|
82
|
+
if (spec?.frames === "auto") markFramesAuto(resolved);
|
|
83
|
+
const t1 = performance.now();
|
|
84
|
+
const { full: expanded, used } = expand(resolved);
|
|
85
|
+
const out = compileMotion({ fps: 24, ...expanded }, ctx);
|
|
86
|
+
const compileMs = ms(t1), t2 = performance.now();
|
|
87
|
+
const measured = out.timeMap ? warpChecks(expanded, out.timeMap) : expanded;
|
|
88
|
+
const qa = runMotionQa(sk, out, measured, { userSpec: resolved }), observations = observeMotion(sk, out, measured.checks), held = heldReadings(out);
|
|
89
|
+
if (held) Object.assign(observations, { units: `${observations.units}; ${HELD_UNITS}`, held });
|
|
90
|
+
const near = observeNear(sk, out), wrist = observeWrist(sk, out);
|
|
91
|
+
if (near) Object.assign(observations, { units: `${observations.units}; ${NEAR_UNITS}`, near });
|
|
92
|
+
if (wrist) Object.assign(observations, { units: `${observations.units}; ${WRIST_UNITS}`, wrist });
|
|
93
|
+
// a skill that paced its beats to fit a short span says so where every answer keeps it (C7 tier 1)
|
|
94
|
+
const paced = used.filter(u => u.paced).map(u => `${u.skill} paced ${u.paced.toFixed(2)} to fit f${u.f0}..f${u.end}`);
|
|
95
|
+
if (paced.length) observations.paced = paced;
|
|
96
|
+
const cut = used.filter(u => u.cut).map(u => `${u.skill}: ${u.cut}`);
|
|
97
|
+
if (cut.length) observations.cut = cut;
|
|
98
|
+
if (plans.length) observations.planned = plans.map(p => p.note);
|
|
99
|
+
const timeline = timelineOf(out, { used, spec: expanded }), probes = asked && runProbes(out, asked, { spec: expanded, sk, scene, notes });
|
|
100
|
+
return { out, qa, observations, spec: resolved, expanded, used, fixed: out.fixed, search, timeline, ...(probes ? { probes } : {}), ...(notes.length ? { notes } : {}),
|
|
101
|
+
ms: { compile: compileMs, qa: ms(t2), total: ms(t0) } };
|
|
102
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Play a take v3 on a live rig: local quaternions straight onto the Mixamo control bones
|
|
2
|
+
// (first depth-first bone of each name; the nested identity "skinned" copies are never
|
|
3
|
+
// written) and the hips position in rig space. The rig root carries the character's
|
|
4
|
+
// placement and the 0.01 centimetre scale, so metres become centimetres here.
|
|
5
|
+
import { normalizeBoneName } from "../poses.js";
|
|
6
|
+
|
|
7
|
+
const CM_PER_M = 100;
|
|
8
|
+
const resolved = new WeakMap();
|
|
9
|
+
|
|
10
|
+
function controlBones(rig, names) {
|
|
11
|
+
let entry = resolved.get(rig);
|
|
12
|
+
if (entry && entry.key === names.join()) return entry;
|
|
13
|
+
const first = new Map();
|
|
14
|
+
rig.traverse(o => {
|
|
15
|
+
if (!o.isBone) return;
|
|
16
|
+
const n = normalizeBoneName(o.name);
|
|
17
|
+
if (!first.has(n)) first.set(n, o);
|
|
18
|
+
});
|
|
19
|
+
const bones = names.map(name => {
|
|
20
|
+
const target = normalizeBoneName(name);
|
|
21
|
+
for (const [n, bone] of first) if (n === target || n.endsWith(target)) return bone;
|
|
22
|
+
return null;
|
|
23
|
+
});
|
|
24
|
+
// Bind translations: a take never moves a bone except the hips, so every other bone goes
|
|
25
|
+
// back to its bind position (another player may have stretched it).
|
|
26
|
+
const binds = rig.userData?.poseBind;
|
|
27
|
+
const bindPos = bones.map(b => {
|
|
28
|
+
const p = b && binds?.get(b)?.position;
|
|
29
|
+
return b ? (p ? [p.x, p.y, p.z] : b.position.toArray()) : null;
|
|
30
|
+
});
|
|
31
|
+
entry = { key: names.join(), bones, bindPos, hips: names.indexOf("Hips") };
|
|
32
|
+
resolved.set(rig, entry);
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const isTakeV3 = motion => motion?.schema === "cozyclay.take.v3";
|
|
37
|
+
|
|
38
|
+
export function applyTakeFrame(rig, take, frame) {
|
|
39
|
+
if (!rig || !take) return;
|
|
40
|
+
const { bones, bindPos, hips } = controlBones(rig, take.bones);
|
|
41
|
+
const f = Math.max(0, Math.min(take.frameCount - 1, Math.round(frame) || 0)), B = take.bones.length;
|
|
42
|
+
for (let b = 0; b < B; b++) {
|
|
43
|
+
const bone = bones[b];
|
|
44
|
+
if (!bone) continue;
|
|
45
|
+
const i = (f * B + b) * 4;
|
|
46
|
+
bone.quaternion.set(take.rot[i], take.rot[i + 1], take.rot[i + 2], take.rot[i + 3]);
|
|
47
|
+
if (b === hips) bone.position.set(take.hips[f * 3] * CM_PER_M, take.hips[f * 3 + 1] * CM_PER_M, take.hips[f * 3 + 2] * CM_PER_M);
|
|
48
|
+
else bone.position.set(bindPos[b][0], bindPos[b][1], bindPos[b][2]);
|
|
49
|
+
}
|
|
50
|
+
rig.updateMatrixWorld(true);
|
|
51
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// C5. Probes: measurements the agent asks for on this compile (motion.compile { probes }),
|
|
2
|
+
// whether or not it installs. At most 16 probes; each is checked first (schema/fields.js
|
|
3
|
+
// PROBE_FIELDS, refs.js checkRef with the full grammar) and every problem is thrown at once as
|
|
4
|
+
// MOTION_SPEC_INVALID, one per line, e.g.
|
|
5
|
+
// probes[0].distance[0] "joint:LeftHnad": unknown bone; did you mean "LeftHand"?
|
|
6
|
+
// Bone names may be loose ("hips", "hand_l", bones.js resolveBone); they are turned into the
|
|
7
|
+
// rig's names before measuring. Answers per name (a probe without a name is "probes[i]"):
|
|
8
|
+
// distance [a, b] { min, minAt, max, maxAt, mean, first, last } (refs.js refDistance; metres
|
|
9
|
+
// to 6 decimals; every `every` frames, default 1; null when no frame has a value)
|
|
10
|
+
// position ref { at: [{ f, p: [x,y,z] }] } (metres to 4 decimals; at most 16
|
|
11
|
+
// rows; every defaults to the step that fits the range in 16 rows, a smaller one grows to it)
|
|
12
|
+
// frames: [f0, f1] absolute and inclusive, or one frame (default the whole take); a window partly past
|
|
13
|
+
// the take is cut to it. Both fixes leave a note in ctx.notes (lenient.js; output.adjusted).
|
|
14
|
+
import { PROBE_FIELDS } from "./schema/fields.js";
|
|
15
|
+
import { checkRef, refPoint, refDistance } from "./refs.js";
|
|
16
|
+
import { resolveBone } from "./bones.js";
|
|
17
|
+
import { didYouMean, fmt } from "./compiler/vocab.js";
|
|
18
|
+
import { clampProbe, fitEvery } from "./lenient.js";
|
|
19
|
+
|
|
20
|
+
const MAX_PROBES = 16, MAX_ROWS = 16, MAX_ERRORS = 12, SHAPE = "{ name, distance: [refA, refB] | position: ref, frames: [f0, f1], every }";
|
|
21
|
+
const r6 = v => Math.round(v * 1e6) / 1e6, r4 = v => Math.round(v * 1e4) / 1e4;
|
|
22
|
+
|
|
23
|
+
/** joint:/bone: refs with the rig's own bone name ("joint:hips" -> "joint:Hips"). */
|
|
24
|
+
function exactRef(sk, ref) {
|
|
25
|
+
const m = /^(joint|bone):(.+)$/s.exec(ref);
|
|
26
|
+
return m ? `${m[1]}:${resolveBone(sk, m[2]).bone ?? m[2]}` : ref;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rangeOf(p, at, last, err, notes) {
|
|
30
|
+
const fr = p.frames === undefined ? [0, last] : clampProbe(p.frames, last, at, notes);
|
|
31
|
+
if (!fr) err(`${at}.frames must be [f0, f1], integer frames within 0..${last} with f0 <= f1, got ${fmt(p.frames)}`);
|
|
32
|
+
return fr;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Checked probes [{ name, refs, kind, range, every }]; throws MOTION_SPEC_INVALID listing every problem. */
|
|
36
|
+
function checkProbes(out, probes, { spec, sk, scene, notes }) {
|
|
37
|
+
const errors = [], err = m => { if (errors.length < MAX_ERRORS) errors.push(m); }, last = out.frameCount - 1, list = [], names = new Map();
|
|
38
|
+
if (!Array.isArray(probes)) err(`probes must be an array of ${SHAPE}`);
|
|
39
|
+
else if (probes.length > MAX_PROBES) err(`probes has ${probes.length} entries; at most ${MAX_PROBES} per compile`);
|
|
40
|
+
else probes.forEach((p, i) => {
|
|
41
|
+
const at = `probes[${i}]`;
|
|
42
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) return err(`${at} must be ${SHAPE}`);
|
|
43
|
+
for (const k of Object.keys(p)) if (!PROBE_FIELDS.includes(k)) err(`${at} has unknown field "${k}"${didYouMean(k, PROBE_FIELDS)}; a probe is ${SHAPE}`);
|
|
44
|
+
const name = p.name ?? at;
|
|
45
|
+
if (typeof name !== "string" || !name) err(`${at}.name must be a non-empty string, got ${fmt(p.name)}`);
|
|
46
|
+
else if (names.has(name)) err(`${at}.name "${name}" is already used by ${names.get(name)}; probe names must differ`);
|
|
47
|
+
else names.set(name, at);
|
|
48
|
+
if ((p.distance === undefined) === (p.position === undefined)) return err(`${at} needs exactly one of distance: [refA, refB] or position: ref`);
|
|
49
|
+
const kind = p.distance !== undefined ? "distance" : "position";
|
|
50
|
+
const refs = kind === "distance" ? p.distance : [p.position];
|
|
51
|
+
if (kind === "distance" && !(Array.isArray(refs) && refs.length === 2 && refs.every(r => typeof r === "string"))) return err(`${at}.distance must be [refA, refB], two point refs, got ${fmt(p.distance)}`);
|
|
52
|
+
if (kind === "position" && typeof p.position !== "string") return err(`${at}.position must be a point ref, got ${fmt(p.position)}`);
|
|
53
|
+
const good = refs.map((r, j) => checkRef(spec, err, { sk, scene }, kind === "distance" ? `${at}.distance[${j}]` : `${at}.position`, r)).every(Boolean);
|
|
54
|
+
const range = rangeOf(p, at, last, err, notes);
|
|
55
|
+
if (p.every !== undefined && !(Number.isInteger(p.every) && p.every >= 1)) return err(`${at}.every must be a positive integer, got ${fmt(p.every)}`);
|
|
56
|
+
if (!good || !range) return;
|
|
57
|
+
const span = range[1] - range[0] + 1, asked = p.every ?? (kind === "position" ? Math.ceil(span / MAX_ROWS) : 1);
|
|
58
|
+
const every = kind === "position" ? fitEvery(asked, span, MAX_ROWS, at, notes) : asked;
|
|
59
|
+
list.push({ name, kind, refs: refs.map(r => exactRef(sk, r)), range, every });
|
|
60
|
+
});
|
|
61
|
+
if (errors.length) throw new Error(`MOTION_SPEC_INVALID: ${errors.join("\n")}${errors.length >= MAX_ERRORS ? "\n(more problems not listed)" : ""}`);
|
|
62
|
+
return list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function frameList([f0, f1], every) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (let f = f0; f <= f1; f += every) out.push(f);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function distanceOf(out, p) {
|
|
72
|
+
const vals = frameList(p.range, p.every).map(f => [f, refDistance(out, f, p.refs[0], p.refs[1])]).filter(([, v]) => Number.isFinite(v));
|
|
73
|
+
if (!vals.length) return { min: null, minAt: null, max: null, maxAt: null, mean: null, first: null, last: null };
|
|
74
|
+
let lo = vals[0], hi = vals[0], sum = 0;
|
|
75
|
+
for (const fv of vals) { if (fv[1] < lo[1]) lo = fv; if (fv[1] > hi[1]) hi = fv; sum += fv[1]; }
|
|
76
|
+
return { min: r6(lo[1]), minAt: lo[0], max: r6(hi[1]), maxAt: hi[0], mean: r6(sum / vals.length), first: r6(vals[0][1]), last: r6(vals[vals.length - 1][1]) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function positionOf(out, p) {
|
|
80
|
+
return { at: frameList(p.range, p.every).map(f => { const q = refPoint(out, f, p.refs[0]); return { f, p: q ? q.map(r4) : null }; }) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* runProbes(out, probes, { spec, sk, scene }) -> { [name]: result }
|
|
85
|
+
* @param {object} out compileMotion result
|
|
86
|
+
* @param {object[]} probes the request's probes
|
|
87
|
+
* @param {{ spec: object, sk: object, scene: object, notes?: string[] }} ctx spec = the expanded spec; notes gets the windows cut and every grown
|
|
88
|
+
*/
|
|
89
|
+
export function runProbes(out, probes, ctx) {
|
|
90
|
+
return Object.fromEntries(checkProbes(out, probes, ctx).map(p => [p.name, p.kind === "distance" ? distanceOf(out, p) : positionOf(out, p)]));
|
|
91
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// BODY on the fitted skin (src/motion3/body.js): an upper arm, forearm or hand going into any other
|
|
2
|
+
// part of the body (torso, hips, head, legs, the other arm) more than L.skin. Each limb probes as
|
|
3
|
+
// spheres on its bone; the body answers from its signed distance grids. The parts are named as the
|
|
4
|
+
// capsules were (qa/geometry.js CAPSULES), so "hand_r into torso_hi" reads the same as before; a
|
|
5
|
+
// spec's checks.allow { part, object: <part>, depth } lets a declared contact touch deeper.
|
|
6
|
+
import { bodyFor, limbDepth } from "../body.js";
|
|
7
|
+
|
|
8
|
+
const REGIONS = [
|
|
9
|
+
[/^Hips$/, "hips"], [/^(Spine|Spine1)$/, "torso_lo"], [/^Spine2$/, "torso_hi"], [/^Neck$/, "neck"], [/^Head/, "head"],
|
|
10
|
+
[/Shoulder$/, "shoulder"], [/UpLeg$/, "thigh"], [/Leg$/, "shin"], [/(Foot|Toe)/, "foot"], [/ForeArm$/, "farm"], [/Arm$/, "uarm"], [/Hand$/, "hand"],
|
|
11
|
+
];
|
|
12
|
+
/** The QA part name of bone b ("thigh_l", "torso_hi", ...). */
|
|
13
|
+
export function regionOf(sk, b) {
|
|
14
|
+
const name = sk.names[b] ?? "", side = /^Left/.test(name) ? "_l" : /^Right/.test(name) ? "_r" : "";
|
|
15
|
+
const hit = REGIONS.find(([re]) => re.test(name));
|
|
16
|
+
return hit ? `${hit[1]}${side}` : name;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ARM_BONES = ["Arm", "ForeArm", "Hand"].flatMap(n => [`Left${n}`, `Right${n}`]);
|
|
20
|
+
|
|
21
|
+
/** L: limits { skin } (m); push(kind, msg, frame, value, limit); allowed(part, into): a declared depth or null. */
|
|
22
|
+
export function bodyChecks(sk, out, L, push, allowed) {
|
|
23
|
+
const body = bodyFor(sk);
|
|
24
|
+
if (!body) return;
|
|
25
|
+
const limbs = ARM_BONES.map(n => sk.index[n]);
|
|
26
|
+
out.frames.forEach(({ s }, f) => {
|
|
27
|
+
for (const b of limbs) {
|
|
28
|
+
const hit = limbDepth(body, sk, s, b);
|
|
29
|
+
if (hit.into < 0) continue;
|
|
30
|
+
const part = regionOf(sk, b), into = regionOf(sk, hit.into), lim = Math.max(allowed(part, into) ?? 0, L.skin);
|
|
31
|
+
if (hit.depth > lim) push("BODY", `${part} into ${into}`, f, hit.depth, lim);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Unnamed checks get a name from their place in the agent's own spec ("contacts[0]"), so their
|
|
2
|
+
// QA groups say which check failed and two unnamed checks stay two groups (severity.js groupKey
|
|
3
|
+
// keeps a bracketed index). Runs on a copy before the skills expand, so skill checks keep theirs.
|
|
4
|
+
|
|
5
|
+
const NAMED = ["contacts", "orient", "below", "clearance", "appear"];
|
|
6
|
+
|
|
7
|
+
/** spec -> a copy whose unnamed spec.checks entries are named "<list>[<index>]" (spec when none are). */
|
|
8
|
+
export function nameChecks(spec) {
|
|
9
|
+
const c = spec?.checks;
|
|
10
|
+
if (!c || typeof c !== "object" || !NAMED.some(k => Array.isArray(c[k]) && c[k].some(e => e && typeof e === "object" && e.name === undefined))) return spec;
|
|
11
|
+
const checks = { ...c };
|
|
12
|
+
for (const k of NAMED) if (Array.isArray(c[k])) checks[k] = c[k].map((e, i) => (e && typeof e === "object" && e.name === undefined ? { ...e, name: `${k}[${i}]` } : e));
|
|
13
|
+
return { ...spec, checks };
|
|
14
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// General motion checks (every take): scene collisions, ground, IK reach, planted-foot slide,
|
|
2
|
+
// bone length, one-frame pops (style.js), knee and elbow flips, knees bending backward, knees and
|
|
3
|
+
// elbows folded past their stop (KNEE: the shin in the thigh; ELBOW: the forearm in the upper arm,
|
|
4
|
+
// compiler/limbs.js), self contact, hands and held props entering the head (and the torso,
|
|
5
|
+
// contact-checks.js; arms into any other body part on the fitted skin, body-checks.js). Speed, acceleration, knee bend and balance are style: style.js measures them
|
|
6
|
+
// and checks them only when the spec names their limit (checks.limits).
|
|
7
|
+
import { add, sub, scale, apply, len, dist, cross, norm, dot, angle } from "../compiler/math.js";
|
|
8
|
+
import { CAPSULES, segPoints, sdBox, segDist, headDepth, solePoints } from "./geometry.js";
|
|
9
|
+
import { contactChecks } from "./contact-checks.js";
|
|
10
|
+
import { bodyChecks } from "./body-checks.js";
|
|
11
|
+
import { popChecks } from "./style.js";
|
|
12
|
+
import { groundSole } from "./ground.js";
|
|
13
|
+
import { footChecks } from "./foot-checks.js";
|
|
14
|
+
import { ELBOW_STOP, KNEE_STOP } from "../compiler/limbs.js";
|
|
15
|
+
|
|
16
|
+
export const QA_LIMITS = { allow: 0.005, groundAllow: 0.01, reach: 0.01, slide: 0.0015, plantH: 0.012,
|
|
17
|
+
speedCore: 0.04, speedLimb: 0.16, accCore: 0.015, accLimb: 0.05, flip: 60, knee: 150, headAllow: 0.003, self: 0,
|
|
18
|
+
body: 0.005, skin: 0.015, hands: 0.01, object: 0.005, grab: 0.03, balance: 0.08 };
|
|
19
|
+
/** FLIP reads a bend plane only where the limb is bent this far (deg) from straight and from folded flat. */
|
|
20
|
+
const FLIP_BEND = 20;
|
|
21
|
+
/** KNEE (folds) and ELBOW: degrees past KNEE_STOP or ELBOW_STOP that count (the IK sits on the stop to rounding). */
|
|
22
|
+
const STOP_TOL = 0.01;
|
|
23
|
+
|
|
24
|
+
export const allowance = (allow, part, obj) => {
|
|
25
|
+
for (const a of allow ?? []) if (part.startsWith(a.part) && (a.object === obj || (a.object.endsWith("*") && obj.startsWith(a.object.slice(0, -1))))) return a.depth;
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function generalChecks(sk, out, opts = {}) {
|
|
30
|
+
const L = { ...QA_LIMITS, ...opts.limits }, issues = [], I = sk.index, F = out.frames, n = F.length;
|
|
31
|
+
const push = (kind, msg, f, value, limit) => issues.push({ kind, msg, frame: f, value: +value.toFixed(4), limit });
|
|
32
|
+
for (let f = 0; f < n; f++) {
|
|
33
|
+
const { s, diag } = F[f], objs = out.objectsAt(f);
|
|
34
|
+
// bounding spheres of the boxes: a capsule farther away than its own reach cannot touch one
|
|
35
|
+
const spheres = Object.entries(objs).filter(([, box]) => box.half && !box.ignore)
|
|
36
|
+
.map(([id, box]) => [id, box, add(box.pos, apply(box.R, [0, box.half[1], 0])), len(box.half)]);
|
|
37
|
+
for (const [part, a, b, r] of CAPSULES) {
|
|
38
|
+
const A = s.P[I[a]], B = s.P[I[b]], mid = scale(add(A, B), 0.5), reach = r + dist(A, B) / 2;
|
|
39
|
+
let pts = null;
|
|
40
|
+
for (const [id, box, c, rb] of spheres) {
|
|
41
|
+
const lim = allowance(opts.allow, part, id) ?? L.allow;
|
|
42
|
+
if (reach - (dist(mid, c) - rb) <= lim) continue;
|
|
43
|
+
pts ??= segPoints(A, B);
|
|
44
|
+
const pen = r - Math.min(...pts.map(p => sdBox(p, box)));
|
|
45
|
+
if (pen > lim) push("COLLIDE", `${part} x ${id}`, f, pen, lim);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
footChecks(sk, s, f, spheres, L, (part, id) => allowance(opts.allow, part, id), push);
|
|
49
|
+
for (const side of ["Left", "Right"]) {
|
|
50
|
+
const sp = solePoints(sk, s, side), low = Math.min(sp.heel[1], sp.ball[1], sp.tip[1]);
|
|
51
|
+
if (low < -L.groundAllow) push("GROUND", `${side} sole below floor`, f, -low, L.groundAllow);
|
|
52
|
+
}
|
|
53
|
+
for (const [k, v] of Object.entries(diag)) if (/miss_[lr]$/.test(k) && v > L.reach) push("REACH", k, f, v, L.reach);
|
|
54
|
+
// hands, forearms and held props must stay out of the head
|
|
55
|
+
const probes = [];
|
|
56
|
+
for (const S of ["Left", "Right"]) probes.push(...segPoints(s.P[I[`${S}Hand`]], s.P[I[`${S}HandMiddle3`]], 5).map(p => [`hand_${S[0].toLowerCase()}`, p, 0.022]));
|
|
57
|
+
for (const [pn, pts] of Object.entries(diag.props ?? {})) for (const [k, p] of Object.entries(pts)) if (Array.isArray(p) && k !== "origin" && !(opts.mouthPoints ?? []).includes(`${pn}.${k}`)) probes.push([`prop_${pn}`, p, 0.004]);
|
|
58
|
+
for (const [nm, p, r] of probes) { const d = headDepth(sk, s, p) + r; if (d > L.headAllow) push("FACE", `${nm} into head`, f, d, L.headAllow); }
|
|
59
|
+
if (segDist(s.P[I.LeftLeg], s.P[I.LeftFoot], s.P[I.RightLeg], s.P[I.RightFoot]) < 0.1 + L.self) push("SELF", "shins touch", f, 0.1, 0.1);
|
|
60
|
+
if (segDist(s.P[I.LeftUpLeg], s.P[I.LeftLeg], s.P[I.RightUpLeg], s.P[I.RightLeg]) < 0.135 + L.self) push("SELF", "thighs overlap", f, 0.135, 0.135);
|
|
61
|
+
const tilted = norm(sub(s.P[I.Spine], s.P[I.Hips]))[1] < 0.5;
|
|
62
|
+
for (const S of ["Left", "Right"]) {
|
|
63
|
+
const th = s.P[I[`${S}UpLeg`]], kn = s.P[I[`${S}Leg`]], an = s.P[I[`${S}Foot`]];
|
|
64
|
+
const bend = angle(sub(kn, th), sub(an, kn));
|
|
65
|
+
// the foot's heading on the ground; the toe sits below the ankle, which must not count. With the pelvis turned past
|
|
66
|
+
// 60 deg from upright (a flip, a cartwheel, lying) the ground says nothing about the leg: the foot's front is the toe
|
|
67
|
+
// across the shin, which an upright leg reads the same way
|
|
68
|
+
let toeDir = sub(s.P[I[`${S}ToeBase`]], an);
|
|
69
|
+
if (tilted) { const shin = norm(sub(an, kn)); toeDir = sub(toeDir, scale(shin, dot(toeDir, shin))); } else toeDir[1] = 0;
|
|
70
|
+
const mid = sub(kn, [(th[0] + an[0]) / 2, (th[1] + an[1]) / 2, (th[2] + an[2]) / 2]);
|
|
71
|
+
if (bend > 3 && dot(mid, toeDir) < -0.002) push("KNEE", `${S} knee bends backward`, f, bend, 0);
|
|
72
|
+
// a knee folded past its stop puts the shin in the thigh (the solver stops at KNEE_STOP)
|
|
73
|
+
if (bend > KNEE_STOP + STOP_TOL) push("KNEE", `${S} knee folds ${bend.toFixed(0)}deg`, f, bend, KNEE_STOP);
|
|
74
|
+
// an elbow folded past its stop puts the forearm in the upper arm (the solver stops at ELBOW_STOP)
|
|
75
|
+
const fold = angle(sub(s.P[I[`${S}ForeArm`]], s.P[I[`${S}Arm`]]), sub(s.P[I[`${S}Hand`]], s.P[I[`${S}ForeArm`]]));
|
|
76
|
+
if (fold > ELBOW_STOP + STOP_TOL) push("ELBOW", `${S} elbow folds ${fold.toFixed(0)}deg`, f, fold, ELBOW_STOP);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const allowed = (part, into) => allowance(opts.allow, part, into);
|
|
80
|
+
contactChecks(sk, out, L, push, allowed);
|
|
81
|
+
bodyChecks(sk, out, L, push, allowed);
|
|
82
|
+
// temporal checks; a planted heel or ball is low over the ground under it, the floor or an object's top (ground.js)
|
|
83
|
+
for (const S of ["Left", "Right"]) {
|
|
84
|
+
const sole = F.map((_, f) => groundSole(out, f, S));
|
|
85
|
+
for (let f = 1; f < n; f++) {
|
|
86
|
+
const a = sole[f - 1], b = sole[f];
|
|
87
|
+
for (const key of ["heel", "ball"]) {
|
|
88
|
+
const planted = b[key][1] < L.plantH && a[key][1] < L.plantH && Math.abs(b[key][1] - a[key][1]) < 0.0005;
|
|
89
|
+
const hv = Math.hypot(b[key][0] - a[key][0], b[key][2] - a[key][2]);
|
|
90
|
+
if (planted && hv > L.slide) push("SLIDE", `${S} ${key} slides`, f, hv, L.slide);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (let j = 0; j < sk.names.length; j++) {
|
|
95
|
+
const p = sk.parent[j]; if (p < 0) continue;
|
|
96
|
+
const L0 = dist(sk.restWorldPos[j], sk.restWorldPos[p]);
|
|
97
|
+
const drift = Math.max(...F.map(fr => Math.abs(dist(fr.s.P[j], fr.s.P[p]) - L0)));
|
|
98
|
+
if (drift > 0.001) push("LENGTH", `${sk.names[p]}-${sk.names[j]}`, 0, drift, 0.001);
|
|
99
|
+
}
|
|
100
|
+
popChecks(sk, out, L, push);
|
|
101
|
+
flipChecks(sk, F, L, push);
|
|
102
|
+
return issues;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* FLIP: a knee's or elbow's bend plane turns more than L.flip degrees a frame. The plane is read
|
|
107
|
+
* only on frames the limb is bent FLIP_BEND or more from straight and from folded flat: near
|
|
108
|
+
* either end a few millimetres set it, so the hand or foot moving a few centimetres turns it any
|
|
109
|
+
* way (session df6acdcc: the right elbow folded to 171 deg as the wrist passed the shoulder, and
|
|
110
|
+
* its plane turned 71 deg in a frame while the elbow moved 3 cm). Across the frames between, the
|
|
111
|
+
* turn is shared out per frame, so a pole that flips while the limb is straight still shows.
|
|
112
|
+
*/
|
|
113
|
+
function flipChecks(sk, F, L, push) {
|
|
114
|
+
const I = sk.index, bent = Math.sin(FLIP_BEND * Math.PI / 180);
|
|
115
|
+
for (const S of ["Left", "Right"]) for (const [a, b, c] of [[`${S}UpLeg`, `${S}Leg`, `${S}Foot`], [`${S}Arm`, `${S}ForeArm`, `${S}Hand`]]) {
|
|
116
|
+
let prev = null, at = 0;
|
|
117
|
+
for (let f = 0; f < F.length; f++) {
|
|
118
|
+
const P = F[f].s.P, up = sub(P[I[b]], P[I[a]]), down = sub(P[I[c]], P[I[b]]), nv = cross(up, down);
|
|
119
|
+
if (len(nv) < bent * len(up) * len(down)) continue;
|
|
120
|
+
const u = norm(nv), turn = prev ? angle(u, prev) : 0, gap = f - at;
|
|
121
|
+
if (turn / gap > L.flip) push("FLIP", `${b} bend plane jumps ${turn.toFixed(0)}deg${gap > 1 ? ` over ${gap} frames` : ""}`, f, turn / gap, L.flip);
|
|
122
|
+
prev = u; at = f;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Contact checks on top of the general set (ported from the car-exit prototype QA): hands and
|
|
2
|
+
// held prop points entering the torso, the two hands overlapping, and hinged scene objects
|
|
3
|
+
// (spec.objects, e.g. a door) passing through the rest of the scene. Corners only for objects.
|
|
4
|
+
import { add, sub, scale, dot, len, apply, clamp } from "../compiler/math.js";
|
|
5
|
+
import { CAPSULES, sdBox, segDist } from "./geometry.js";
|
|
6
|
+
import { movesRecord } from "../carried.js";
|
|
7
|
+
|
|
8
|
+
const TORSO = CAPSULES.filter(c => c[0].startsWith("torso"));
|
|
9
|
+
const HAND_R = CAPSULES.find(c => c[0] === "hand_l")[3];
|
|
10
|
+
const PROP_R = 0.004;
|
|
11
|
+
|
|
12
|
+
function pointSegDist(p, a, b) {
|
|
13
|
+
const ab = sub(b, a), t = clamp(dot(sub(p, a), ab) / Math.max(dot(ab, ab), 1e-12), 0, 1);
|
|
14
|
+
return len(sub(p, add(a, scale(ab, t))));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function corners(o) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const x of [-1, 1]) for (const y of [0, 2]) for (const z of [-1, 1]) out.push(add(o.pos, apply(o.R, [x * o.half[0], y * o.half[1], z * o.half[2]])));
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** L: limits { body, hands, object } (m); push(kind, msg, frame, value, limit); allowed(part, into): a declared depth or null. */
|
|
24
|
+
export function contactChecks(sk, out, L, push, allowed = () => null) {
|
|
25
|
+
const I = sk.index, hinged = Object.entries(out.ctx.spec.objects ?? {}).filter(([, o]) => movesRecord(o)).map(([id]) => id);
|
|
26
|
+
for (let f = 0; f < out.frames.length; f++) {
|
|
27
|
+
const { s, diag } = out.frames[f], P = s.P;
|
|
28
|
+
// hands into the torso: body-checks.js, on the fitted skin; held prop points stay on the capsules
|
|
29
|
+
const probes = [];
|
|
30
|
+
for (const [pn, pts] of Object.entries(diag.props ?? {})) for (const [k, p] of Object.entries(pts)) if (Array.isArray(p) && k !== "origin") probes.push([`prop_${pn}`, p, PROP_R]);
|
|
31
|
+
for (const [part, a, b, r] of TORSO) {
|
|
32
|
+
const worst = new Map();
|
|
33
|
+
for (const [nm, p, pr] of probes) worst.set(nm, Math.max(worst.get(nm) ?? -Infinity, r + pr - pointSegDist(p, P[I[a]], P[I[b]])));
|
|
34
|
+
for (const [nm, d] of worst) if (d > L.body) push("BODY", `${nm} into ${part}`, f, d, L.body);
|
|
35
|
+
}
|
|
36
|
+
const overlap = 2 * HAND_R - segDist(P[I.LeftHand], P[I.LeftHandMiddle3], P[I.RightHand], P[I.RightHandMiddle3]);
|
|
37
|
+
const handsLim = Math.max(allowed("hand_l", "hand_r") ?? 0, L.hands);
|
|
38
|
+
if (overlap > handsLim) push("SELF", "hands overlap", f, overlap, handsLim);
|
|
39
|
+
if (!hinged.length) continue;
|
|
40
|
+
const objs = out.objectsAt(f);
|
|
41
|
+
for (const id of hinged) {
|
|
42
|
+
if (!objs[id]?.half) continue;
|
|
43
|
+
const cs = corners(objs[id]);
|
|
44
|
+
for (const [oid, box] of Object.entries(objs)) {
|
|
45
|
+
if (hinged.includes(oid) || !box.half || box.ignore) continue;
|
|
46
|
+
const depth = -Math.min(...cs.map(p => sdBox(p, box)));
|
|
47
|
+
if (depth > L.object) push("OBJECT", `${id} into ${oid}`, f, depth, L.object);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Actionable QA: every issue group gets a `cause` (the spec element most responsible: a key and
|
|
2
|
+
// channel, an anchor, a fit, a generator step, a skill) and a `hint` (what to change, with a
|
|
3
|
+
// direction and an amount in world metres or degrees, at a frame). Hints are derived from the
|
|
4
|
+
// geometry of the worst frame and from the keys that bracket it. A hint function returns
|
|
5
|
+
// { cause, hint, fix? }; fix rides on the group unseen (non-enumerable) for qa/patch.js.
|
|
6
|
+
import { contactHints } from "./hints-contact.js";
|
|
7
|
+
import { motionHints } from "./hints-motion.js";
|
|
8
|
+
import { objectHints } from "./hints-object.js";
|
|
9
|
+
import { goalHints } from "./hints-goal.js";
|
|
10
|
+
import { wristHints } from "./wrist-checks.js";
|
|
11
|
+
import { attachPatches } from "./patch.js";
|
|
12
|
+
|
|
13
|
+
const HINTS = { ...contactHints, ...motionHints, ...objectHints, ...goalHints, ...wristHints };
|
|
14
|
+
|
|
15
|
+
function keyTable(out) {
|
|
16
|
+
const byCh = new Map();
|
|
17
|
+
for (const k of out.keys ?? []) for (const ch of Object.keys(k.set ?? {})) {
|
|
18
|
+
if (!byCh.has(ch)) byCh.set(ch, []);
|
|
19
|
+
byCh.get(ch).push({ f: k.f, src: k.src ?? "keys", step: k.step?.includes(ch) });
|
|
20
|
+
}
|
|
21
|
+
for (const list of byCh.values()) list.sort((a, b) => a.f - b.f);
|
|
22
|
+
return byCh;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The context every hint function reads: frames by absolute index, keys, drivers. */
|
|
26
|
+
function context(sk, out, spec) {
|
|
27
|
+
const table = keyTable(out);
|
|
28
|
+
const X = {
|
|
29
|
+
sk, out, spec, I: sk.index,
|
|
30
|
+
fr: f => out.frames[Math.max(0, Math.min(out.frames.length - 1, f - (out.f0 ?? 0)))],
|
|
31
|
+
rel: f => Math.max(0, Math.min(out.frames.length - 1, f - (out.f0 ?? 0))),
|
|
32
|
+
keys: ch => table.get(ch) ?? [],
|
|
33
|
+
bracket(ch, f) {
|
|
34
|
+
const list = X.keys(ch);
|
|
35
|
+
let prev = null, next = null;
|
|
36
|
+
for (const k of list) { if (k.f <= f) prev = k; else { next = k; break; } }
|
|
37
|
+
return { prev, next };
|
|
38
|
+
},
|
|
39
|
+
/** "keys[12] f200 hand_r" when a key sits within 2 frames, else the two keys around f. */
|
|
40
|
+
cause(ch, f) {
|
|
41
|
+
const { prev, next } = X.bracket(ch, f);
|
|
42
|
+
if (!prev && !next) return `${ch} (not keyed)`;
|
|
43
|
+
const near = prev && f - prev.f <= 2 ? prev : next && next.f - f <= 2 ? next : null;
|
|
44
|
+
if (near) return `${near.src} f${Math.round(near.f)} ${ch}`;
|
|
45
|
+
if (!next) return `${ch} held since ${prev.src} f${Math.round(prev.f)}`;
|
|
46
|
+
if (!prev) return `${ch} before ${next.src} f${Math.round(next.f)}`;
|
|
47
|
+
return `${ch} between ${prev.src} f${Math.round(prev.f)} and ${next.src === prev.src ? "" : `${next.src} `}f${Math.round(next.f)}`;
|
|
48
|
+
},
|
|
49
|
+
/** What drives hand k at frame f: an anchor weight, the hand target, or the FK arm. */
|
|
50
|
+
hand(k, f) {
|
|
51
|
+
const c = X.fr(f).c;
|
|
52
|
+
if (!(c[`armw_${k}`] > 0.5)) return { label: `arm ${k} (FK: aswing_${k}/abend_${k}, or armw_${k} 1 and hand_${k})`, ch: `aswing_${k}` };
|
|
53
|
+
let best = null;
|
|
54
|
+
for (const [ch, w] of Object.entries(c)) {
|
|
55
|
+
const mm = /^w_(.+)_([lr])$/.exec(ch);
|
|
56
|
+
if (mm && mm[2] === k && w > 0.5 && (!best || w > best.w)) best = { w, anchor: mm[1], ch };
|
|
57
|
+
}
|
|
58
|
+
if (!best) return { label: `hand_${k}`, ch: `hand_${k}` };
|
|
59
|
+
const fit = (spec.fits ?? []).findIndex(ft => ft.anchor === best.anchor && ft.side === k);
|
|
60
|
+
return { label: `anchor ${best.anchor}`, ch: best.ch, anchor: best.anchor, fit: fit >= 0 ? fit : null };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return X;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Adds cause + hint (+ patch) to the first `limit` groups (mutates and returns them); userSpec is what patches address. */
|
|
67
|
+
export function explainGroups(sk, out, spec, groups, { limit = 16, userSpec } = {}) {
|
|
68
|
+
if (!groups.length) return groups;
|
|
69
|
+
const X = context(sk, out, spec);
|
|
70
|
+
for (const g of groups.slice(0, limit)) {
|
|
71
|
+
try {
|
|
72
|
+
const r = HINTS[g.kind]?.(X, g);
|
|
73
|
+
if (r) { g.cause = r.cause; g.hint = r.hint; }
|
|
74
|
+
if (r?.fix) Object.defineProperty(g, "fix", { value: r.fix, enumerable: false, configurable: true });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
g.cause = "unknown"; g.hint = `no hint (${error.message})`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return attachPatches(groups, { X, userSpec });
|
|
80
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Compact number formatting for QA hints (millimetre precision).
|
|
2
|
+
export const m3 = x => { const r = Math.round(x * 1000) / 1000; return Object.is(r, -0) ? 0 : r; };
|
|
3
|
+
|
|
4
|
+
/** Compact vector: [+0.021, 0, -0.004]. */
|
|
5
|
+
export const vec = v => `[${v.map(x => { const r = m3(x); return r > 0 ? `+${r}` : `${r}`; }).join(", ")}]`;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// COLLIDE for the feet: the foot's skin against the scene bodies, as "foot_<k> x <id>". The shin capsule ends at
|
|
2
|
+
// the ankle and GROUND reads the floor only, so a swinging toe went 9.6 cm into a stair riser with QA clean
|
|
3
|
+
// (session e7edb718). The probes are points on the fitted skin of the foot and toe bones (body.js grids: the
|
|
4
|
+
// cells within half a step of the skin, one per SPACING m cube, carried by their bone); a rig without a fitted
|
|
5
|
+
// body uses the sole points. A foot on a body's top is a step, not a collision: a probe the top face is nearest
|
|
6
|
+
// to may sit up to the ground's allowance in it (QA_LIMITS.groundAllow, as GROUND gives the floor); through a
|
|
7
|
+
// side face the usual allowance holds. A part-wise allow entry ({ part: "foot", object }) sets both.
|
|
8
|
+
import { add, apply, dist, dot, scale } from "../compiler/math.js";
|
|
9
|
+
import { bodyFor } from "../body.js";
|
|
10
|
+
import { sdBox, boxNormal, solePoints } from "./geometry.js";
|
|
11
|
+
|
|
12
|
+
const UP = [0, 1, 0], TOP = 0.7, SPACING = 0.025;
|
|
13
|
+
const skins = new Map();
|
|
14
|
+
|
|
15
|
+
/** Skin points of a foot in its bones' bind frames: [[bone index, local point], ...] (null for a rig without a fitted body). */
|
|
16
|
+
function footSkin(sk, side) {
|
|
17
|
+
const key = `${sk.model}:${side}`;
|
|
18
|
+
if (skins.has(key)) return skins.get(key);
|
|
19
|
+
const body = bodyFor(sk), out = body ? [] : null;
|
|
20
|
+
for (const g of body?.grids ?? []) {
|
|
21
|
+
if (g.b !== sk.index[`${side}Foot`] && g.b !== sk.index[`${side}ToeBase`]) continue;
|
|
22
|
+
const [nx, ny, nz] = g.dims, seen = new Set();
|
|
23
|
+
for (let i = 0; i < nx; i++) for (let j = 0; j < ny; j++) for (let k = 0; k < nz; k++) {
|
|
24
|
+
if (Math.abs(g.data[(i * ny + j) * nz + k] / 1000) > body.step / 2) continue;
|
|
25
|
+
const q = [g.min[0] + i * body.step, g.min[1] + j * body.step, g.min[2] + k * body.step], cell = q.map(v => Math.floor(v / SPACING)).join();
|
|
26
|
+
if (!seen.has(cell)) { seen.add(cell); out.push([g.b, q]); }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
skins.set(key, out);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The foot's collision probes at state s: world points on its skin (the sole points without a fitted body). */
|
|
34
|
+
export function footProbes(sk, s, side) {
|
|
35
|
+
const skin = footSkin(sk, side);
|
|
36
|
+
if (!skin?.length) {
|
|
37
|
+
const sp = solePoints(sk, s, side), mid = (a, b) => scale(add(a, b), 0.5);
|
|
38
|
+
return [sp.heel, mid(sp.heel, sp.ball), sp.ball, mid(sp.ball, sp.tip), sp.tip];
|
|
39
|
+
}
|
|
40
|
+
return skin.map(([b, q]) => add(s.P[b], apply(s.R[b], q)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How deep a foot's probes go into a box, through its top and through its sides: { top, side } in metres
|
|
45
|
+
* (-Infinity: not in), each with the probe that goes deepest that way ({ topAt, sideAt }).
|
|
46
|
+
*/
|
|
47
|
+
export function footDepths(probes, box) {
|
|
48
|
+
const out = { top: -Infinity, side: -Infinity, topAt: null, sideAt: null };
|
|
49
|
+
for (const p of probes) {
|
|
50
|
+
const d = -sdBox(p, box);
|
|
51
|
+
if (d <= 0) continue;
|
|
52
|
+
const way = dot(boxNormal(p, box), UP) > TOP ? "top" : "side";
|
|
53
|
+
if (d > out[way]) { out[way] = d; out[`${way}At`] = p; }
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Push COLLIDE "foot_<k> x <id>" for each foot in each body at frame f. spheres: [id, box, centre, radius] of
|
|
60
|
+
* the bodies (checks.js); allowed(part, id) the spec's allow depth or null.
|
|
61
|
+
*/
|
|
62
|
+
export function footChecks(sk, s, f, spheres, L, allowed, push) {
|
|
63
|
+
for (const [side, k] of [["Left", "l"], ["Right", "r"]]) {
|
|
64
|
+
const probes = footProbes(sk, s, side), part = `foot_${k}`;
|
|
65
|
+
for (const [id, box, c, rb] of spheres) {
|
|
66
|
+
if (probes.every(p => dist(p, c) > rb)) continue;
|
|
67
|
+
const d = footDepths(probes, box), own = allowed(part, id), sideLim = own ?? L.allow, topLim = own ?? L.groundAllow;
|
|
68
|
+
if (d.side > sideLim) push("COLLIDE", `${part} x ${id}`, f, d.side, sideLim);
|
|
69
|
+
else if (d.top > topLim) push("COLLIDE", `${part} x ${id}`, f, d.top, topLim);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|