cozyclay 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +12 -30
- package/bin/agent/agent-routes.mjs +40 -113
- package/bin/agent/agent-runner.mjs +9 -10
- package/bin/agent/cliproxy-provider.mjs +135 -0
- package/bin/agent/credential-store.mjs +30 -7
- package/bin/agent/pi-tools.mjs +4 -1
- package/bin/agent/provider-keys.mjs +32 -0
- package/bin/agent/provider-login.mjs +110 -0
- package/bin/agent/provider-routes.mjs +99 -0
- package/bin/agent/providers.mjs +19 -97
- package/bin/agent/script-tool.mjs +17 -4
- package/bin/agent/studio-motion-reference.mjs +23 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-rebase-text.mjs +32 -0
- package/bin/agent/studio-rebase.mjs +26 -39
- package/bin/agent/studio-tools.mjs +9 -28
- package/bin/agent/tool-arguments.mjs +49 -0
- package/bin/agent/turn-frames.mjs +66 -0
- package/bin/codex-auth.mjs +9 -2
- package/bin/cozyclay.mjs +10 -116
- package/bin/live/cli.mjs +3 -3
- package/bin/mcp-runtime.mjs +2 -12
- package/bin/package-signature.mjs +0 -4
- package/dist/app/index.html +7 -6
- package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
- package/dist/assets/analytics-C9sUnndd.js +1 -0
- package/dist/assets/app-CCh--1Hc.js +5087 -0
- package/dist/assets/app-f-fkV5uP.css +1 -0
- package/dist/assets/compile-worker-CkxiD6lH.js +5 -0
- package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
- package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-CC4xe4HM.js} +1 -1
- package/dist/assets/{landing-ChUvB_zL.js → landing-DjHD8u7k.js} +1 -1
- package/dist/assets/scene-asset-sync-Hx_FzdOH.js +20 -0
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
- package/dist/assets/workflow-C4yKH1p4.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +3 -3
- package/dist/sw.js +3 -6
- package/dist/workflow/index.html +5 -4
- package/mcp/LIVE-PROTOCOL.md +7 -47
- package/mcp/README.md +16 -26
- package/mcp/live-hub.mjs +6 -10
- package/mcp/package.json +1 -3
- package/mcp/tool-handlers.mjs +4 -168
- package/package.json +3 -22
- package/src/App.jsx +150 -2624
- package/src/agent-progress.js +79 -0
- package/src/agent-turn-mark.js +30 -0
- package/src/analytics.js +1 -19
- package/src/app-context.js +5 -0
- package/src/app-stage.jsx +2 -34
- package/src/asset-pane.jsx +6 -5
- package/src/camera-block.js +21 -0
- package/src/camera-direction.js +120 -0
- package/src/character-kind.js +3 -9
- package/src/command-bus.js +8 -6
- package/src/commands/cast.js +5 -34
- package/src/commands/elements/character.js +1 -1
- package/src/commands/elements/motion.js +1 -6
- package/src/commands/elements/object.js +6 -2
- package/src/commands/elements/shot.js +4 -1
- package/src/commands/elements.js +38 -14
- package/src/commands/index.js +3 -1
- package/src/commands/kits.js +37 -0
- package/src/commands/motion-timing.js +57 -0
- package/src/commands/motion.js +9 -152
- package/src/commands/motion3-compile.js +65 -0
- package/src/commands/motion3.js +207 -0
- package/src/commands/objects.js +6 -4
- package/src/commands/shot.js +49 -11
- package/src/commands/stage.js +14 -1
- package/src/commands/view.js +4 -1
- package/src/document-mirrors.js +42 -0
- package/src/document-store.js +4 -0
- package/src/domains/cast.js +19 -194
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion/take-source.js +51 -0
- package/src/domains/motion.js +60 -1838
- package/src/domains/scenes.js +12 -55
- package/src/domains/shots.js +2 -2
- package/src/domains/stage.js +4 -2
- package/src/draft-ghost.js +46 -0
- package/src/draft-ghost.jsx +129 -0
- package/src/dualview.jsx +4 -3
- package/src/effects/EffectsLayer.jsx +112 -0
- package/src/effects/effect-meshes.js +232 -0
- package/src/effects/effect-record.js +103 -0
- package/src/effects/emitter.js +59 -0
- package/src/effects/simulate.js +261 -0
- package/src/effects/state.js +59 -0
- package/src/execution-telemetry.js +2 -3
- package/src/first-success-guide.jsx +5 -5
- package/src/ghost-set.js +257 -0
- package/src/held-object-edit.jsx +113 -0
- package/src/held-object-pose.js +175 -0
- package/src/hierarchy-model.js +1 -1
- package/src/hierarchy-panel.jsx +1 -1
- package/src/imported-mesh.jsx +103 -0
- package/src/kits/held.js +84 -0
- package/src/kits/index.js +70 -0
- package/src/kits/smoking.js +57 -0
- package/src/light-blockers.js +93 -0
- package/src/motion/beat-board-model.js +84 -0
- package/src/motion/beat-board.css +124 -0
- package/src/motion/beat-board.jsx +59 -0
- package/src/motion/center-of-mass.js +66 -0
- package/src/motion/fix-collisions.js +850 -0
- package/src/motion/physics-review.js +118 -0
- package/src/{ardy → motion}/physics-support.js +1 -76
- package/src/motion/rig-snapshot.js +22 -0
- package/src/motion/take-segments.jsx +140 -0
- package/src/{ardy → motion}/timeline-coordinates.js +0 -21
- package/src/{ardy → motion}/timeline.css +1 -2
- package/src/{ardy → motion}/timeline.jsx +42 -413
- package/src/motion/waypoints.js +76 -0
- package/src/motion3/beats.js +83 -0
- package/src/motion3/body.js +158 -0
- package/src/motion3/bones.js +47 -0
- package/src/motion3/carried.js +47 -0
- package/src/motion3/compile-job.js +75 -0
- package/src/motion3/compile-worker.js +10 -0
- package/src/motion3/compiler/anchors.js +105 -0
- package/src/motion3/compiler/arm-orient.js +128 -0
- package/src/motion3/compiler/channels.js +125 -0
- package/src/motion3/compiler/compile.js +205 -0
- package/src/motion3/compiler/gait-rhythm.js +97 -0
- package/src/motion3/compiler/grip.js +171 -0
- package/src/motion3/compiler/head-steady.js +48 -0
- package/src/motion3/compiler/hold.js +134 -0
- package/src/motion3/compiler/idle.js +113 -0
- package/src/motion3/compiler/limbs.js +170 -0
- package/src/motion3/compiler/math.js +72 -0
- package/src/motion3/compiler/pose.js +162 -0
- package/src/motion3/compiler/quat.js +49 -0
- package/src/motion3/compiler/skeleton.js +74 -0
- package/src/motion3/compiler/validate-common.js +32 -0
- package/src/motion3/compiler/validate-goals.js +118 -0
- package/src/motion3/compiler/validate-objects.js +143 -0
- package/src/motion3/compiler/validate-parts.js +116 -0
- package/src/motion3/compiler/validate.js +122 -0
- package/src/motion3/compiler/vocab.js +73 -0
- package/src/motion3/compiler/walk-pelvis.js +275 -0
- package/src/motion3/compiler/walk.js +207 -0
- package/src/motion3/compiler/wrist-clamp.js +32 -0
- package/src/motion3/delta.js +47 -0
- package/src/motion3/describe.js +174 -0
- package/src/motion3/docs.js +27 -0
- package/src/motion3/edit-carry.js +21 -0
- package/src/motion3/fix/ease.js +16 -0
- package/src/motion3/fix/ground.js +37 -0
- package/src/motion3/fix/index.js +52 -0
- package/src/motion3/fix/push.js +71 -0
- package/src/motion3/fix/reach.js +46 -0
- package/src/motion3/fix/retime.js +79 -0
- package/src/motion3/fix/self.js +211 -0
- package/src/motion3/fix/slide.js +70 -0
- package/src/motion3/fix/turn.js +51 -0
- package/src/motion3/follow.js +39 -0
- package/src/motion3/free-motion.js +173 -0
- package/src/motion3/grip-choice.js +130 -0
- package/src/motion3/hold-plan.js +157 -0
- package/src/motion3/json-patch.js +77 -0
- package/src/motion3/key-aim.js +105 -0
- package/src/motion3/key-edit.js +82 -0
- package/src/motion3/lenient.js +127 -0
- package/src/motion3/object-keys.js +236 -0
- package/src/motion3/object-pose.js +182 -0
- package/src/motion3/object-tracks.js +12 -0
- package/src/motion3/pipeline.js +102 -0
- package/src/motion3/playback.js +51 -0
- package/src/motion3/probes.js +91 -0
- package/src/motion3/qa/body-checks.js +34 -0
- package/src/motion3/qa/check-names.js +14 -0
- package/src/motion3/qa/checks.js +121 -0
- package/src/motion3/qa/contact-checks.js +51 -0
- package/src/motion3/qa/explain.js +80 -0
- package/src/motion3/qa/fmt.js +5 -0
- package/src/motion3/qa/foot-checks.js +72 -0
- package/src/motion3/qa/geometry.js +77 -0
- package/src/motion3/qa/goals.js +191 -0
- package/src/motion3/qa/ground.js +60 -0
- package/src/motion3/qa/hints-contact.js +179 -0
- package/src/motion3/qa/hints-goal.js +160 -0
- package/src/motion3/qa/hints-knee.js +24 -0
- package/src/motion3/qa/hints-motion.js +209 -0
- package/src/motion3/qa/hints-object.js +91 -0
- package/src/motion3/qa/index.js +42 -0
- package/src/motion3/qa/key-checks.js +82 -0
- package/src/motion3/qa/near.js +96 -0
- package/src/motion3/qa/object-checks.js +135 -0
- package/src/motion3/qa/patch.js +161 -0
- package/src/motion3/qa/severity.js +38 -0
- package/src/motion3/qa/spec-checks.js +65 -0
- package/src/motion3/qa/style.js +218 -0
- package/src/motion3/qa/wrist-checks.js +69 -0
- package/src/motion3/qa/wrist-obs.js +60 -0
- package/src/motion3/refs.js +120 -0
- package/src/motion3/repair-tune.js +121 -0
- package/src/motion3/repair.js +83 -0
- package/src/motion3/report.js +67 -0
- package/src/motion3/rigs/index.js +14 -0
- package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/x-bot-tpose.json +1 -0
- package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
- package/src/motion3/rigs/y-bot-tpose.json +1 -0
- package/src/motion3/scene.js +60 -0
- package/src/motion3/schema/brief.js +89 -0
- package/src/motion3/schema/fields.js +44 -0
- package/src/motion3/schema/spec-examples.js +46 -0
- package/src/motion3/schema/topics.js +111 -0
- package/src/motion3/schema/types.js +147 -0
- package/src/motion3/search.js +117 -0
- package/src/motion3/second-hand.js +112 -0
- package/src/motion3/skills/basic.js +82 -0
- package/src/motion3/skills/book.js +76 -0
- package/src/motion3/skills/car.js +145 -0
- package/src/motion3/skills/frames-auto.js +29 -0
- package/src/motion3/skills/index.js +83 -0
- package/src/motion3/skills/info.js +65 -0
- package/src/motion3/skills/smoke-kit.js +109 -0
- package/src/motion3/skills/smoke.js +160 -0
- package/src/motion3/spec-store.js +59 -0
- package/src/motion3/take-edit.js +155 -0
- package/src/motion3/take.js +63 -0
- package/src/motion3/timeline.js +127 -0
- package/src/motion3/views.js +121 -0
- package/src/motion3/walk-plan.js +119 -0
- package/src/object-catalog.jsx +7 -1
- package/src/object-colors.js +98 -0
- package/src/object-gizmo-parts.js +115 -0
- package/src/object-gizmo.jsx +37 -122
- package/src/object-glow.js +139 -0
- package/src/object-hidden.js +33 -0
- package/src/object-look.js +196 -0
- package/src/object-size.js +10 -0
- package/src/object-travel.js +71 -6
- package/src/panels/CameraPanel.jsx +7 -7
- package/src/panels/HdrEnvironmentControls.jsx +59 -0
- package/src/panels/LightPanel.jsx +25 -4
- package/src/panels/ObjectLookPanel.jsx +88 -0
- package/src/panels/PosePanel.jsx +2 -17
- package/src/panels/RigControlPanel.jsx +5 -53
- package/src/panels/SubjectBox.jsx +4 -1
- package/src/panels/details.css +14 -0
- package/src/posestudio.jsx +4 -27
- package/src/project-browser.jsx +3 -4
- package/src/project-resources.js +18 -31
- package/src/project.js +13 -104
- package/src/props.jsx +25 -280
- package/src/rail-dot-drag.js +23 -0
- package/src/render-passes.js +10 -0
- package/src/resource-status.jsx +1 -0
- package/src/result-modal.jsx +4 -19
- package/src/room.jsx +20 -12
- package/src/scene-assets.js +8 -6
- package/src/scene-light-place.js +72 -0
- package/src/scene-lights.jsx +148 -0
- package/src/scene-objects.js +31 -139
- package/src/scenes.js +15 -105
- package/src/semantic-edit.js +2 -5
- package/src/set-pieces.jsx +183 -0
- package/src/settings-menu.jsx +3 -11
- package/src/shell/BottomDock.jsx +9 -51
- package/src/shell/DetailsSlot.jsx +16 -200
- package/src/shell/GhostsMenuGroup.jsx +74 -0
- package/src/shell/LibrarySlot.jsx +1 -3
- package/src/shell/MenuBar.jsx +3 -3
- package/src/shell/PreferencesDialog.jsx +15 -264
- package/src/shell/PreferencesSlot.jsx +2 -8
- package/src/shell/TopBar.jsx +5 -84
- package/src/shell/ViewportToolbar.jsx +10 -70
- package/src/shell/agent-glass.css +19 -6
- package/src/shell/details-selection.js +42 -0
- package/src/shell/ghosts-menu.css +22 -0
- package/src/shell/glass-regions.css +29 -3
- package/src/shell/preferences.css +6 -1
- package/src/shot-authoring.js +4 -7
- package/src/soft-shadows.js +37 -0
- package/src/stage-attach-frames.js +12 -12
- package/src/stage-canvas-rigs.jsx +3 -3
- package/src/stage-capture.jsx +42 -8
- package/src/stage-characters.jsx +22 -216
- package/src/stage-editor-gizmos.jsx +6 -6
- package/src/stage-environment.js +93 -0
- package/src/stage-hdr-builtins.js +107 -0
- package/src/stage-hdr-image.js +116 -0
- package/src/stage-hdr-library.js +70 -0
- package/src/stage-hdr-map.js +94 -0
- package/src/stage-hdr.js +130 -0
- package/src/stage-lighting.js +149 -0
- package/src/stage-occlusion.js +210 -0
- package/src/stage-sky.jsx +159 -0
- package/src/stage-startup-defaults.js +5 -9
- package/src/studio-actions.js +12 -14
- package/src/studio-agent-commands.js +19 -16
- package/src/studio-agent-motion.js +9 -9
- package/src/studio-agent-protocol.js +41 -71
- package/src/studio-app-binding.js +25 -45
- package/src/studio-asset-list.js +17 -0
- package/src/studio-contact-sheet.js +37 -5
- package/src/studio-elements.js +27 -7
- package/src/studio-entity-bounds.js +18 -0
- package/src/studio-error-codes.js +8 -0
- package/src/studio-export-framing.js +152 -0
- package/src/studio-lighting-check.js +103 -0
- package/src/studio-motion-visual.js +150 -0
- package/src/studio-schema-errors.js +80 -0
- package/src/studio-support-gap.js +74 -0
- package/src/styles.css +1 -19
- package/src/timeline-defaults.js +5 -0
- package/src/timeline-extent.js +8 -20
- package/src/tutorial-walk.js +17 -0
- package/src/ui-scrub.js +1 -1
- package/src/use-shot-camera-tracks.js +12 -0
- package/src/view-ghosts.js +97 -0
- package/src/view-ghosts.jsx +49 -0
- package/src/workflow/AgentPanel.jsx +47 -166
- package/src/workflow/AgentProgressCard.jsx +107 -0
- package/src/workflow/AgentReplyText.jsx +37 -0
- package/src/workflow/AgentTurnSteps.jsx +84 -0
- package/src/workflow/LoginPanel.jsx +140 -0
- package/src/workflow/WorkflowBuilder.jsx +7 -23
- package/src/workflow/agent-client.js +54 -51
- package/src/workflow/agent-panel.css +71 -1
- package/src/workflow/agent-progress-card.css +167 -0
- package/src/workflow/agent-progress-view.js +47 -0
- package/src/workflow/agent-turn-steps.css +113 -0
- package/src/workflow/agent-turn.js +191 -0
- package/src/workflow/login-client.js +34 -0
- package/src/workflow/motion-input.js +18 -31
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/reply-markdown.js +69 -0
- package/src/workflow/scene-asset-sync.js +8 -8
- package/tools/agent-session-stats.mjs +61 -0
- package/tools/dev-full.mjs +5 -56
- package/tools/eslint-baseline.json +12 -13
- package/tools/knip-baseline.json +0 -179
- package/tools/loc-baseline.json +31 -81
- package/tools/motion3/extract-body.mjs +103 -0
- package/tools/motion3/extract-rig.mjs +85 -0
- package/tools/process-supervisor.mjs +0 -88
- package/tools/run-tests.mjs +8 -124
- package/bin/agent/motion-runtime.mjs +0 -76
- package/dist/ardy/cskel27-rest.json +0 -557
- package/dist/assets/analytics-C9pMpW7s.js +0 -1
- package/dist/assets/app-B_2XB9rw.css +0 -1
- package/dist/assets/app-DAuvKPP8.js +0 -4920
- package/dist/assets/config-Bx7pYsD9.js +0 -1
- package/dist/assets/config-DeoFTYzb.css +0 -1
- package/dist/assets/demo-CvKuBgy5.js +0 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
- package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
- package/dist/assets/ticket-BPk2Ni95.js +0 -1
- package/dist/assets/workflow-jJRBR94Z.js +0 -179
- package/dist/demo/walk-then-stop.npz +0 -0
- package/mcp/ardy-prompts.mjs +0 -218
- package/src/ardy/auto-fix-panel.css +0 -108
- package/src/ardy/auto-physics.js +0 -1194
- package/src/ardy/client.js +0 -153
- package/src/ardy/convert.js +0 -313
- package/src/ardy/cskel27-neutral.js +0 -39
- package/src/ardy/cskel27.js +0 -77
- package/src/ardy/export.js +0 -165
- package/src/ardy/fix-collisions.js +0 -2257
- package/src/ardy/foot-lock.js +0 -230
- package/src/ardy/motion-calibration.js +0 -118
- package/src/ardy/motion-edit.js +0 -213
- package/src/ardy/motion-url.js +0 -55
- package/src/ardy/mp4-duration.js +0 -102
- package/src/ardy/npz.js +0 -595
- package/src/ardy/physics-panel.css +0 -21
- package/src/ardy/physics-panel.jsx +0 -89
- package/src/ardy/physics-refine.js +0 -111
- package/src/ardy/physics-review.js +0 -567
- package/src/ardy/physics-temporal.js +0 -54
- package/src/ardy/platform-fit-panel.css +0 -64
- package/src/ardy/platform-fit-panel.jsx +0 -41
- package/src/ardy/platform-fit.js +0 -326
- package/src/ardy/playback.js +0 -711
- package/src/ardy/pose-pin.js +0 -97
- package/src/ardy/prompt-clips.js +0 -30
- package/src/ardy/retime.js +0 -211
- package/src/ardy/root-drop.js +0 -369
- package/src/ardy/timeline-resize.js +0 -13
- package/src/ardy/to-cskel27.js +0 -225
- package/src/ardy/trim.js +0 -33
- package/src/ardy/waypoints.js +0 -369
- package/src/ardy-bridge-contract.js +0 -281
- package/src/domains/motion/motion-video-capture.js +0 -664
- package/src/i2v-motion-client.js +0 -77
- package/src/i2v-motion-studio.jsx +0 -180
- package/src/line-edit.js +0 -1348
- package/src/motion/generation.js +0 -105
- package/src/motion-readiness-ui.jsx +0 -67
- package/src/motion-readiness.js +0 -40
- package/src/motion-resources.js +0 -177
- package/src/motion-store.js +0 -65
- package/src/motion-trail.js +0 -601
- package/src/multimodel-ingest.js +0 -394
- package/src/panels/PromptBlocksPanel.jsx +0 -444
- package/src/panels/VideoCapturePanel.jsx +0 -181
- package/src/pose-extract/browser.js +0 -42
- package/src/pose-extract/detector.js +0 -100
- package/src/pose-extract/fit.js +0 -227
- package/src/pose-extract/image-frame.js +0 -51
- package/src/pose-extract/index.js +0 -58
- package/src/pose-extract/keyframes.js +0 -74
- package/src/pose-extract/landmarks.js +0 -162
- package/src/pose-extract/mirror.js +0 -153
- package/src/pose-extract/take.js +0 -131
- package/src/pose-extract/video-frames.js +0 -91
- package/src/take-recipe.js +0 -165
- package/src/trail-key-conflicts.js +0 -39
- package/src/trail-pick.js +0 -59
- package/src/use-i2v-capture-modal.js +0 -15
- package/tools/ardy/BRIDGE.md +0 -37
- package/tools/ardy/GVHMR-NPZ.md +0 -68
- package/tools/ardy/README.md +0 -124
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/artifacts.mjs +0 -34
- package/tools/ardy/bridge.mjs +0 -1732
- package/tools/ardy/bvh-cskel27.mjs +0 -1265
- package/tools/ardy/cclay_constrained_generate.py +0 -561
- package/tools/ardy/cclay_gvhmr_worker.py +0 -264
- package/tools/ardy/cclay_motion_edit.py +0 -510
- package/tools/ardy/cclay_sequence_generate.py +0 -705
- package/tools/ardy/cclay_worker.py +0 -304
- package/tools/ardy/dump-npz.py +0 -205
- package/tools/ardy/extract-rest.mjs +0 -299
- package/tools/ardy/extract.mjs +0 -507
- package/tools/ardy/footage.mjs +0 -467
- package/tools/ardy/gvhmr-floor.mjs +0 -102
- package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
- package/tools/ardy/gvhmr_fastpath.py +0 -168
- package/tools/ardy/gvhmr_trajectory.py +0 -240
- package/tools/ardy/merge-text-encoder.py +0 -160
- package/tools/ardy/mocap-metrics.mjs +0 -200
- package/tools/ardy/mocap-quality-gate.mjs +0 -110
- package/tools/ardy/motion-stabilize.mjs +0 -348
- package/tools/ardy/npz.mjs +0 -414
- package/tools/ardy/pose-to-npz.mjs +0 -106
- package/tools/ardy/prompt-limits.mjs +0 -5
- package/tools/ardy/run-edit-on-box.sh +0 -73
- package/tools/ardy/run-local.mjs +0 -233
- package/tools/ardy/run-on-box.sh +0 -593
- package/tools/ardy/run-sequence-on-box.sh +0 -177
- package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
- package/tools/ardy/runners/index.mjs +0 -18
- package/tools/ardy/runners/local.mjs +0 -455
- package/tools/ardy/runners/proc.mjs +0 -125
- package/tools/ardy/runners/remote.mjs +0 -252
- package/tools/ardy/setup-local.mjs +0 -189
- package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
- package/tools/ardy/setup-text-encoder.py +0 -306
- package/tools/ardy/smpl-cskel27.mjs +0 -178
- package/tools/ardy/visual-qa.mjs +0 -196
- package/tools/ardy/vq-car.mjs +0 -52
- package/tools/bench/EXP3.md +0 -61
- package/tools/bench/cclay_bench_extract_incam.py +0 -125
- package/tools/bench/cclay_bench_extract_obs.py +0 -204
- package/tools/bench/cclay_bench_runner.py +0 -42
- package/tools/bench/cube-contact.mjs +0 -162
- package/tools/bench/exp3.mjs +0 -152
- package/tools/bench/extract-bench-lib.mjs +0 -216
- package/tools/bench/extract-bench.mjs +0 -302
- package/tools/bench/fal-generate.mjs +0 -88
- package/tools/bench/fit/README.md +0 -189
- package/tools/bench/fit/camera.mjs +0 -59
- package/tools/bench/fit/contact.mjs +0 -419
- package/tools/bench/fit/footlock.mjs +0 -234
- package/tools/bench/fit/motion.mjs +0 -85
- package/tools/bench/fit/pin.mjs +0 -27
- package/tools/bench/fit/remote.mjs +0 -105
- package/tools/bench/fit-bench.mjs +0 -126
- package/tools/bench/fit-sanity.mjs +0 -45
- package/tools/bench/metrics.mjs +0 -313
- package/tools/bench/obs/depth.mjs +0 -258
- package/tools/bench/obs/extrinsics.mjs +0 -175
- package/tools/bench/obs/fit_mannequin_betas.py +0 -340
- package/tools/bench/obs/ground.mjs +0 -294
- package/tools/bench/obs/heading.mjs +0 -151
- package/tools/bench/obs/ladder.mjs +0 -555
- package/tools/bench/obs/mannequin-betas.json +0 -124
- package/tools/bench/obs/remote.mjs +0 -83
- package/tools/bench/obs/rest_joints.py +0 -52
- package/tools/bench/obs/ybot-targets.mjs +0 -49
- package/tools/bench/obs-bench.mjs +0 -520
- package/tools/bench/score.mjs +0 -343
- package/tools/bench/summarize.mjs +0 -86
- package/tools/demo-worker/README.md +0 -137
- package/tools/demo-worker/api-client.mjs +0 -233
- package/tools/demo-worker/generate.mjs +0 -301
- package/tools/demo-worker/index.mjs +0 -466
- package/tools/demo-worker/sign-core.mjs +0 -112
- package/tools/demo-worker/sign.mjs +0 -161
- package/tools/gt-render/browser.mjs +0 -182
- package/tools/gt-render/camera-math.mjs +0 -276
- package/tools/gt-render/page.mjs +0 -267
- package/tools/gt-render/render.mjs +0 -514
- package/tools/gt-render/scene-box.mjs +0 -31
- package/tools/gt-render/take-transform.mjs +0 -87
- package/tools/kimodo/compare-seam.mjs +0 -108
- package/tools/kimodo/constraints.mjs +0 -244
- package/tools/kimodo/edit.mjs +0 -259
- package/tools/kimodo/effector-constraints.mjs +0 -200
- package/tools/kimodo/generate.mjs +0 -646
- package/tools/kimodo/local-output.mjs +0 -265
- package/tools/kimodo/measure-pose.mjs +0 -65
- package/tools/kimodo/measure-preserve.mjs +0 -899
- package/tools/kimodo/measure-waypoints.mjs +0 -55
- package/tools/kimodo/mlx-generate.py +0 -111
- package/tools/kimodo/pose-constraints.mjs +0 -248
- package/tools/kimodo/preserve-mask.mjs +0 -543
- package/tools/kimodo/read-npz.mjs +0 -189
- package/tools/kimodo/run-edit-on-box.mjs +0 -155
- package/tools/kimodo/run-sequence-on-box.mjs +0 -178
- package/tools/kimodo/runner.mjs +0 -284
- package/tools/kimodo/seam-batch.mjs +0 -77
- package/tools/kimodo/setup-local.mjs +0 -30
- package/tools/kimodo/setup-on-box.sh +0 -339
- package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
- package/tools/morphgs/assets/gen_truth.py +0 -39
- package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
- package/tools/morphgs/assets/playback-check.mjs +0 -30
- package/tools/morphgs/demo-gate.sh +0 -26
- package/tools/morphgs/fbx2morphgs.mjs +0 -68
- package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
- package/tools/morphgs/setup-on-cluster.sh +0 -126
- package/tools/projflow/driver.py +0 -1069
- package/tools/projflow/generate.mjs +0 -805
- package/tools/projflow/hml22-to-cskel27.mjs +0 -735
- package/tools/projflow/line-edit-job.mjs +0 -547
- package/tools/projflow/replay.mjs +0 -452
- package/tools/projflow/runner.mjs +0 -269
- package/tools/projflow/service.mjs +0 -742
- package/tools/track/DESIGN.md +0 -513
- package/tools/track/backfill-provenance.mjs +0 -116
- package/tools/track/budget.mjs +0 -8
- package/tools/track/check-rig.mjs +0 -209
- package/tools/track/diagnostics.schema.json +0 -60
- package/tools/track/export-rig.mjs +0 -256
- package/tools/track/fallback.mjs +0 -7
- package/tools/track/fk-parity-fixture.mjs +0 -161
- package/tools/track/gate.mjs +0 -349
- package/tools/track/masks.mjs +0 -318
- package/tools/track/metrics.mjs +0 -201
- package/tools/track/publish-obs.mjs +0 -108
- package/tools/track/py/check_env.py +0 -61
- package/tools/track/py/eval_lr.py +0 -229
- package/tools/track/py/lr_viterbi.py +0 -294
- package/tools/track/py/masks.py +0 -405
- package/tools/track/py/objective.py +0 -382
- package/tools/track/py/rig.py +0 -508
- package/tools/track/py/scene.py +0 -326
- package/tools/track/py/test_joint_indices.py +0 -123
- package/tools/track/py/test_lr_viterbi.py +0 -195
- package/tools/track/py/test_masks.py +0 -229
- package/tools/track/py/test_rig.py +0 -288
- package/tools/track/py/test_scene.py +0 -354
- package/tools/track/py/test_track.py +0 -328
- package/tools/track/py/track.py +0 -411
- package/tools/track/remote.mjs +0 -140
- package/tools/track/rig-dump.mjs +0 -227
- package/tools/track/run-box-tests.mjs +0 -14
- package/tools/track/run-box.mjs +0 -168
- package/tools/track/setup-box.sh +0 -59
- package/tools/track/study-2d.mjs +0 -728
- /package/src/{ardy → motion}/collision-blockers.js +0 -0
- /package/src/{ardy → motion}/ground.js +0 -0
- /package/src/{ardy → motion}/ik-drag.js +0 -0
- /package/src/{ardy → motion}/ik-key-json.js +0 -0
- /package/src/{ardy → motion}/ik.js +0 -0
- /package/src/{ardy → motion}/physics-surface.js +0 -0
- /package/src/{ardy → motion}/playback-clock.js +0 -0
- /package/src/{ardy → motion}/range-pin.js +0 -0
package/src/line-edit.js
DELETED
|
@@ -1,1348 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Line editing (ProjFlow, contract C6) — the pure half.
|
|
3
|
-
*
|
|
4
|
-
* The joint's EXISTING screen trajectory is drawn over the viewport and the
|
|
5
|
-
* user grabs a point on it and pulls; the curve deforms under a Gaussian
|
|
6
|
-
* falloff and the deformed curve is what the box is made to follow EXACTLY.
|
|
7
|
-
* (This is the Disney scheduled-inpainting figure-1 interaction: drag the dots
|
|
8
|
-
* on the motion curve.) Everything in this file is the part of that feature
|
|
9
|
-
* that has no DOM, no React and no three.js in it: a projected curve in, a wire
|
|
10
|
-
* payload out. It is kept separate from App.jsx for two reasons —
|
|
11
|
-
*
|
|
12
|
-
* 1. the camera maths below is the one place where a sign error produces a
|
|
13
|
-
* silently mirrored edit rather than a crash, so it has to be testable
|
|
14
|
-
* without a browser;
|
|
15
|
-
* 2. App.jsx is 11k lines and the feature's diff there should be wiring,
|
|
16
|
-
* not arithmetic.
|
|
17
|
-
*
|
|
18
|
-
* NOTHING here imports three.js. Matrices arrive as plain 16-element arrays in
|
|
19
|
-
* three's own column-major `elements` order, which is what `Matrix4.elements`
|
|
20
|
-
* already is — the caller passes `[...m.elements]` and no adapter is needed.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The 15 track ids a line edit may name. Deliberately spelled out rather than
|
|
25
|
-
* imported from ./ardy/ik.js: that module pulls in three.js for its contact
|
|
26
|
-
* -radius measurement, and this one must stay loadable in a bare node test.
|
|
27
|
-
*
|
|
28
|
-
* The list IS the union of ik.js's three tables — IK_TRACKS (hands/feet),
|
|
29
|
-
* MID_TRACKS (elbows/knees) and FK_TRACKS (hips/spine/chest/neck/head/
|
|
30
|
-
* shoulders) — and it is also exactly the key set of TRACK_GROUPS in
|
|
31
|
-
* tools/kimodo/preserve-mask.mjs, which is the bridge's own validation table.
|
|
32
|
-
* App.jsx builds the picker's LABELS by looking each id up in those ik.js
|
|
33
|
-
* tables, so a track added there without being added here shows up as a
|
|
34
|
-
* missing option rather than as a silently wrong wire value.
|
|
35
|
-
*/
|
|
36
|
-
export const LINE_EDIT_TRACK_IDS = Object.freeze([
|
|
37
|
-
// IK_TRACKS — effectors
|
|
38
|
-
"leftHand",
|
|
39
|
-
"rightHand",
|
|
40
|
-
"leftFoot",
|
|
41
|
-
"rightFoot",
|
|
42
|
-
// MID_TRACKS — mid-chain handles
|
|
43
|
-
"leftElbow",
|
|
44
|
-
"rightElbow",
|
|
45
|
-
"leftKnee",
|
|
46
|
-
"rightKnee",
|
|
47
|
-
// FK_TRACKS — torso and head
|
|
48
|
-
"hips",
|
|
49
|
-
"spine",
|
|
50
|
-
"chest",
|
|
51
|
-
"neck",
|
|
52
|
-
"head",
|
|
53
|
-
"leftShoulder",
|
|
54
|
-
"rightShoulder",
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
/** Upper bound on the polyline the app sends.
|
|
58
|
-
*
|
|
59
|
-
* The box builds TWO affine rows per constrained sample and then factorises an
|
|
60
|
-
* m x m system (Cholesky, per the C7 amendment). 64 points is 128 rows, which
|
|
61
|
-
* is comfortably inside what the sampler solves per ODE step, and it is far
|
|
62
|
-
* more resolution than the deformed trajectory carries. A 200-frame range is
|
|
63
|
-
* 200 curve points; sending them all would cost solve time for detail the
|
|
64
|
-
* Gaussian falloff made smooth on purpose. */
|
|
65
|
-
export const MAX_LINE_POINTS = 64;
|
|
66
|
-
|
|
67
|
-
/** A polyline needs at least a start and an end (C6: `>= 2 points`). */
|
|
68
|
-
export const MIN_LINE_POINTS = 2;
|
|
69
|
-
|
|
70
|
-
/** How many curve points at EACH end are hard-pinned to the original
|
|
71
|
-
* trajectory — never moved by a drag, never grabbable.
|
|
72
|
-
*
|
|
73
|
-
* This is the seam fix, and it is the reason this interaction replaced
|
|
74
|
-
* freehand drawing. Gate GP2 measured it: an arbitrary drawn line pops the
|
|
75
|
-
* take 3.9x/8.0x its own median frame delta at the range edges (the joint
|
|
76
|
-
* teleports to wherever the stroke started), while a line whose ENDPOINTS sit
|
|
77
|
-
* on the joint's own trajectory collapses that to 1.67x/1.09x. A Gaussian
|
|
78
|
-
* never actually reaches zero — at 6 sigma it is still ~1e-8, and at the more
|
|
79
|
-
* realistic "grab three frames from the end with radius 40" it is a very
|
|
80
|
-
* visible fraction — so relying on the falloff alone would leave the endpoint
|
|
81
|
-
* OFF the original trajectory by an amount that depends on where the user
|
|
82
|
-
* happened to grab. Pinning makes the seam property structural instead of
|
|
83
|
-
* probabilistic: points [0, 1] and [n-2, n-1] are returned byte-identical by
|
|
84
|
-
* dragCurve, so the edit's first and last constrained frames are always
|
|
85
|
-
* exactly where the take already put the joint. Two rather than one because a
|
|
86
|
-
* single pinned frame still lets the SLOPE jump at the seam. */
|
|
87
|
-
export const PINNED_CURVE_ENDS = 2;
|
|
88
|
-
|
|
89
|
-
/** Influence-radius bounds, in FRAMES, for the panel's slider. 2 is a local
|
|
90
|
-
* nudge (one gesture beat); 40 at 20 fps is two seconds, past which a drag
|
|
91
|
-
* stops being an edit and becomes a different motion. */
|
|
92
|
-
export const DRAG_RADIUS_MIN = 2;
|
|
93
|
-
export const DRAG_RADIUS_MAX = 40;
|
|
94
|
-
export const DRAG_RADIUS_DEFAULT = 8;
|
|
95
|
-
|
|
96
|
-
/** Below this weight a point is not being dragged AT ALL: dragWeight returns
|
|
97
|
-
* exactly 0 there, so dragCurve hands the point back by reference and the
|
|
98
|
-
* overlay's influenced-span highlight, the identity-based curvesEqual test and
|
|
99
|
-
* changedFrameRange (which names the frames a pull actually touched) all agree
|
|
100
|
-
* on one boundary. Before the cutoff the Gaussian tail brushed every frame of
|
|
101
|
-
* the clip with sub-5% weights, which made "the frames the pull touched" the
|
|
102
|
-
* whole clip — and a whole-clip edit range leaves the box zero preserve rows. */
|
|
103
|
-
export const DRAG_WEIGHT_EPSILON = 0.05;
|
|
104
|
-
|
|
105
|
-
/** Grab tolerance for the hit test, in CSS pixels. */
|
|
106
|
-
export const CURVE_GRAB_RADIUS_PX = 14;
|
|
107
|
-
|
|
108
|
-
/** How far the camera may drift under a pulled curve before the curve stops
|
|
109
|
-
* being DRAWABLE in the live view. The captured extrinsics and the authored uv
|
|
110
|
-
* are a MATCHED PAIR: once the user orbits, the same uv names a different ray,
|
|
111
|
-
* so the line can no longer be painted where it belongs and a second gesture
|
|
112
|
-
* would author points through a lens the first ones never saw.
|
|
113
|
-
*
|
|
114
|
-
* What it does NOT invalidate is the edit: the committed curve carries its own
|
|
115
|
-
* camera and that snapshot is what goes on the wire, so a pending edit survives
|
|
116
|
-
* any amount of drift (App.jsx paints it detached and refuses new gestures
|
|
117
|
-
* until the view comes back). 1e-4 is tighter than any deliberate nudge and
|
|
118
|
-
* looser than float noise from re-deriving the same matrix. */
|
|
119
|
-
const CAMERA_DRIFT_EPSILON = 1e-4;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* The rendering camera -> the C6 `camera` block: { fx, fy, cx, cy, R, t }.
|
|
123
|
-
*
|
|
124
|
-
* ============================ CONVENTION (read this) ======================
|
|
125
|
-
* This is the contract the box-side driver builds its affine rows against
|
|
126
|
-
* [fx*R0 - (u - cx)*R2] X = -[fx*t0 - (u - cx)*t2]
|
|
127
|
-
* [fy*R1 - (v - cy)*R2] X = -[fy*t1 - (v - cy)*t2]
|
|
128
|
-
* so every symbol below has to mean exactly one thing.
|
|
129
|
-
*
|
|
130
|
-
* UNITS. points2d are normalized 0..1 across the render viewport, so fx, fy,
|
|
131
|
-
* cx, cy are in THOSE SAME uv units, not pixels. That is forced, not stylistic:
|
|
132
|
-
* C6's camera block carries no width/height, so the only way (u - cx) can be
|
|
133
|
-
* dimensionally consistent with fx is for both to live in uv. Multiply fx and
|
|
134
|
-
* cx by the viewport width (fy, cy by its height) to recover pixel intrinsics.
|
|
135
|
-
* cx and cy are therefore always exactly 0.5 for a three.js PerspectiveCamera,
|
|
136
|
-
* which has no principal-point offset; they are still emitted so the box never
|
|
137
|
-
* has to assume a centred principal point.
|
|
138
|
-
*
|
|
139
|
-
* AXES. u runs left->right and v runs TOP->BOTTOM, matching both the DOM
|
|
140
|
-
* pointer coordinates the curve is grabbed in and the OpenCV image
|
|
141
|
-
* convention. three.js NDC has y running bottom->top, so v is the flipped one:
|
|
142
|
-
* u = (x_ndc + 1) / 2
|
|
143
|
-
* v = (1 - y_ndc) / 2
|
|
144
|
-
*
|
|
145
|
-
* FRAME. R and t map WORLD -> an OPENCV camera frame: x right, y DOWN, z
|
|
146
|
-
* FORWARD (into the screen, positive depth in front of the lens). three.js
|
|
147
|
-
* cameras look down their own -Z with +Y up, so this function applies the
|
|
148
|
-
* standard diag(1, -1, -1) flip to `matrixWorldInverse` — negating rows 1 and
|
|
149
|
-
* 2 of the rotation and components 1 and 2 of the translation. With that flip
|
|
150
|
-
* both fx and fy come out POSITIVE, which is why the flip is done here rather
|
|
151
|
-
* than pushed onto the box as a sign convention it would have to remember.
|
|
152
|
-
* The projection is then plain pinhole:
|
|
153
|
-
* Xc = R X + t ; u = cx + fx * Xc0 / Xc2 ; v = cy + fy * Xc1 / Xc2
|
|
154
|
-
* with Xc2 > 0 for anything in front of the camera.
|
|
155
|
-
*
|
|
156
|
-
* R is 3x3 ROW-MAJOR as a flat 9-array of numbers (R[0..2] is the first row).
|
|
157
|
-
* ==========================================================================
|
|
158
|
-
*
|
|
159
|
-
* @param {object} view
|
|
160
|
-
* @param {number} view.fovDeg three's VERTICAL field of view, in degrees.
|
|
161
|
-
* @param {number} view.aspect the camera's own `aspect` — the one baked into
|
|
162
|
-
* its projection matrix, which is not necessarily width/height (the shot
|
|
163
|
-
* camera is locked to the export aspect and letterboxed into its pane).
|
|
164
|
-
* @param {number[]} view.matrixWorldInverse 16 numbers, three's column-major
|
|
165
|
-
* `elements` order, world -> three-camera.
|
|
166
|
-
* @param {number} view.width render viewport width in CSS px — the exact
|
|
167
|
-
* @param {number} view.height rectangle the curve's uv are normalized by.
|
|
168
|
-
* Only used to catch a caller that measured the curve against a different
|
|
169
|
-
* rectangle than the one the camera actually drew into.
|
|
170
|
-
*/
|
|
171
|
-
export function cameraToC6({ fovDeg, aspect, matrixWorldInverse, width, height }) {
|
|
172
|
-
if (!Number.isFinite(fovDeg) || fovDeg <= 0 || fovDeg >= 180) {
|
|
173
|
-
throw new Error(`cameraToC6: fovDeg must be a vertical FOV in (0, 180), got ${JSON.stringify(fovDeg)}`);
|
|
174
|
-
}
|
|
175
|
-
if (!Number.isFinite(aspect) || aspect <= 0) {
|
|
176
|
-
throw new Error(`cameraToC6: aspect must be a positive number, got ${JSON.stringify(aspect)}`);
|
|
177
|
-
}
|
|
178
|
-
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
179
|
-
throw new Error(`cameraToC6: width/height must be positive, got ${JSON.stringify([width, height])}`);
|
|
180
|
-
}
|
|
181
|
-
const e = matrixWorldInverse;
|
|
182
|
-
if (!e || e.length !== 16 || !Array.prototype.every.call(e, Number.isFinite)) {
|
|
183
|
-
throw new Error("cameraToC6: matrixWorldInverse must be 16 finite numbers (three's column-major elements)");
|
|
184
|
-
}
|
|
185
|
-
// The image rectangle the caller normalized against must match the aspect
|
|
186
|
-
// the projection matrix was built with, or u and v are measured in a
|
|
187
|
-
// different space than fx and fy describe and the whole edit lands skewed.
|
|
188
|
-
// The callers fit the pane to the camera's aspect (dualview's fitAspect),
|
|
189
|
-
// so a mismatch here is a wiring bug, not a user action.
|
|
190
|
-
if (Math.abs(width / height - aspect) > 1e-3 * aspect) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`cameraToC6: viewport aspect ${(width / height).toFixed(4)} does not match camera aspect ${aspect.toFixed(4)} — ` +
|
|
193
|
-
"the curve was measured against a rectangle the camera did not draw into",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Vertical FOV -> uv intrinsics. Derivation, from three's own perspective
|
|
198
|
-
// projection: x_ndc = Xc_x / (aspect * tan(fov/2) * -Zc) and
|
|
199
|
-
// y_ndc = Yc_y / (tan(fov/2) * -Zc). Substituting u = (x_ndc + 1)/2 and
|
|
200
|
-
// v = (1 - y_ndc)/2 gives the halves below.
|
|
201
|
-
const tanHalfFov = Math.tan((fovDeg * Math.PI) / 360);
|
|
202
|
-
const fx = 0.5 / (aspect * tanHalfFov);
|
|
203
|
-
const fy = 0.5 / tanHalfFov;
|
|
204
|
-
|
|
205
|
-
// three's Matrix4.elements is COLUMN-major: element(row, col) = e[col*4+row].
|
|
206
|
-
const m = (row, col) => e[col * 4 + row];
|
|
207
|
-
// diag(1, -1, -1) * [R | t] — the three-camera (y up, -z forward) to
|
|
208
|
-
// OpenCV-camera (y down, +z forward) change of basis described above.
|
|
209
|
-
const R = [
|
|
210
|
-
m(0, 0), m(0, 1), m(0, 2),
|
|
211
|
-
-m(1, 0), -m(1, 1), -m(1, 2),
|
|
212
|
-
-m(2, 0), -m(2, 1), -m(2, 2),
|
|
213
|
-
];
|
|
214
|
-
const t = [m(0, 3), -m(1, 3), -m(2, 3)];
|
|
215
|
-
return { fx, fy, cx: 0.5, cy: 0.5, R, t };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Project a world point through a C6 camera block into the uv the curve lives
|
|
220
|
-
* in. It is what builds the editable trajectory below, and it is the direct
|
|
221
|
-
* inverse-check of cameraToC6: if a projected joint does not land on the joint
|
|
222
|
-
* the user sees, the convention above is wrong and the curve makes that visible
|
|
223
|
-
* immediately.
|
|
224
|
-
*
|
|
225
|
-
* Returns null for points at or behind the lens (depth <= 0), which have no
|
|
226
|
-
* image.
|
|
227
|
-
*/
|
|
228
|
-
export function projectPointC6(camera, x, y, z) {
|
|
229
|
-
const { fx, fy, cx, cy, R, t } = camera;
|
|
230
|
-
const xc = R[0] * x + R[1] * y + R[2] * z + t[0];
|
|
231
|
-
const yc = R[3] * x + R[4] * y + R[5] * z + t[1];
|
|
232
|
-
const zc = R[6] * x + R[7] * y + R[8] * z + t[2];
|
|
233
|
-
if (!(zc > 1e-6)) return null;
|
|
234
|
-
return [cx + (fx * xc) / zc, cy + (fy * yc) / zc];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* A uv drag -> a WORLD-space delta in the plane through `point` that faces the
|
|
239
|
-
* camera. The pin gesture's whole 2D-to-3D step.
|
|
240
|
-
*
|
|
241
|
-
* A screen drag names two of three degrees of freedom and the third has to come
|
|
242
|
-
* from somewhere. Every DCC answers the same way for an unconstrained effector
|
|
243
|
-
* drag: keep the depth the point already had and move it in the image plane. So
|
|
244
|
-
* the point's camera-space depth zc is measured once, a uv delta becomes a
|
|
245
|
-
* camera-space delta (du*zc/fx, dv*zc/fy, 0) — the exact inverse of the pinhole
|
|
246
|
-
* projection at that depth — and R transposed carries it back to world. The
|
|
247
|
-
* consequence is worth saying out loud: the pin lands exactly under the pointer
|
|
248
|
-
* in the view it was authored in, and its depth is the take's own. Nothing here
|
|
249
|
-
* guesses at depth, which is precisely the guess that would make a pin
|
|
250
|
-
* unpredictable.
|
|
251
|
-
*
|
|
252
|
-
* R is world->camera and orthonormal, so its transpose is its inverse; that is
|
|
253
|
-
* the one property this function leans on and it is guaranteed by cameraToC6.
|
|
254
|
-
*
|
|
255
|
-
* Returns null for a point at or behind the lens, which has no image and
|
|
256
|
-
* therefore no image-plane to slide in.
|
|
257
|
-
*/
|
|
258
|
-
export function unprojectDeltaC6(camera, point, du, dv) {
|
|
259
|
-
if (!camera || !Array.isArray(point) || point.length !== 3) return null;
|
|
260
|
-
if (!Number.isFinite(du) || !Number.isFinite(dv)) return null;
|
|
261
|
-
const { fx, fy, R } = camera;
|
|
262
|
-
const t = camera.t;
|
|
263
|
-
const zc = R[6] * point[0] + R[7] * point[1] + R[8] * point[2] + t[2];
|
|
264
|
-
if (!(zc > 1e-6)) return null;
|
|
265
|
-
const dxc = (du * zc) / fx;
|
|
266
|
-
const dyc = (dv * zc) / fy;
|
|
267
|
-
// R^T @ [dxc, dyc, 0] — column j of R^T is row j of R.
|
|
268
|
-
return [
|
|
269
|
-
R[0] * dxc + R[3] * dyc,
|
|
270
|
-
R[1] * dxc + R[4] * dyc,
|
|
271
|
-
R[2] * dxc + R[5] * dyc,
|
|
272
|
-
];
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/* ===================== the editable trajectory curve =======================
|
|
276
|
-
* A "curve" throughout this file is a DENSE, FRAME-INDEXED array: one entry per
|
|
277
|
-
* frame of the edit range, in order, either `{ frame, u, v }` or `null` for a
|
|
278
|
-
* frame whose joint is behind the lens and therefore has no image.
|
|
279
|
-
*
|
|
280
|
-
* Dense-and-frame-indexed is the load-bearing property. C6 spreads points2d
|
|
281
|
-
* across frameRange, so index i of the curve IS frame startFrame + i; that is
|
|
282
|
-
* what lets a drag mean "move the joint at THIS moment" instead of "move this
|
|
283
|
-
* point of a shape", and it is why the falloff below is measured in frames.
|
|
284
|
-
* Nothing in here compacts the array — nulls keep their slot.
|
|
285
|
-
* ========================================================================== */
|
|
286
|
-
|
|
287
|
-
/** Is this curve point inside the drawable viewport?
|
|
288
|
-
*
|
|
289
|
-
* A point can leave 0..1 two ways: it was already offscreen when the trail was
|
|
290
|
-
* projected (the joint is out of frame at that moment), or a drag pushed it
|
|
291
|
-
* out. Both are the same fact for the UI — it cannot be grabbed and it cannot
|
|
292
|
-
* be sent, because the bridge refuses points2d outside 0..1. */
|
|
293
|
-
export function isCurvePointOnScreen(point) {
|
|
294
|
-
return !!point && point.u >= 0 && point.u <= 1 && point.v >= 0 && point.v <= 1;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Is this index one of the hard-pinned ends? See PINNED_CURVE_ENDS. */
|
|
298
|
-
export function isCurveEndPinned(index, length) {
|
|
299
|
-
return index < PINNED_CURVE_ENDS || index >= length - PINNED_CURVE_ENDS;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Gaussian falloff weight for a point `distanceFrames` away from the grab.
|
|
304
|
-
*
|
|
305
|
-
* sigma = radius/2, so `radiusFrames` reads as "the span I am pulling": the
|
|
306
|
-
* weight is 1 at the grab, 0.61 at radius/2, 0.135 at the radius itself and
|
|
307
|
-
* ~1e-4 at twice it. Exported (rather than inlined in dragCurve) because the
|
|
308
|
-
* overlay paints the influenced span with the SAME formula — two independent
|
|
309
|
-
* spellings of one falloff is how the drawn highlight drifts from the applied
|
|
310
|
-
* deformation.
|
|
311
|
-
*
|
|
312
|
-
* NOT the smoothstep in motion-trail.js: that one is compact (exactly 0 at the
|
|
313
|
-
* radius) because it edits 3D world positions where a hard boundary is fine.
|
|
314
|
-
* Here a C1 tail is what keeps the deformed trajectory from developing a crease
|
|
315
|
-
* the sampler would then have to follow exactly.
|
|
316
|
-
*/
|
|
317
|
-
export function dragWeight(distanceFrames, radiusFrames) {
|
|
318
|
-
const radius = Number(radiusFrames);
|
|
319
|
-
const distance = Number(distanceFrames);
|
|
320
|
-
if (!Number.isFinite(distance)) return 0;
|
|
321
|
-
if (!(radius > 0)) return distance === 0 ? 1 : 0;
|
|
322
|
-
const sigma = radius / 2;
|
|
323
|
-
const weight = Math.exp(-(distance * distance) / (2 * sigma * sigma));
|
|
324
|
-
// Hard zero past the tail (see DRAG_WEIGHT_EPSILON): a pull must touch a
|
|
325
|
-
// bounded window of frames, not brush the whole clip with 1% ghosts.
|
|
326
|
-
return weight < DRAG_WEIGHT_EPSILON ? 0 : weight;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* The joint's world-space trail -> the on-screen curve the user grabs.
|
|
331
|
-
*
|
|
332
|
-
* `trail` is the flat Float32Array from motion-trail's jointTrailPoints (3
|
|
333
|
-
* floats per frame of the WHOLE take); `frameRange` is C6's half-open app-clip
|
|
334
|
-
* range; `camera` is the C6 block the range will be sent with. Frames past the
|
|
335
|
-
* end of the trail and frames whose joint is behind the lens come back as
|
|
336
|
-
* null. Points OUTSIDE 0..1 are kept as they are — the joint really is over
|
|
337
|
-
* there, just not in shot, and the caller wants to say so rather than pretend
|
|
338
|
-
* the trajectory stops at the frame edge.
|
|
339
|
-
*/
|
|
340
|
-
export function projectTrailCurve({ trail, frameRange, camera }) {
|
|
341
|
-
if (!trail || !camera || !frameRange) return null;
|
|
342
|
-
const start = Math.max(0, Math.trunc(frameRange.startFrame));
|
|
343
|
-
const end = Math.trunc(frameRange.endFrame);
|
|
344
|
-
if (!(end > start)) return null;
|
|
345
|
-
const trailFrames = Math.floor(trail.length / 3);
|
|
346
|
-
const curve = [];
|
|
347
|
-
for (let frame = start; frame < end; frame += 1) {
|
|
348
|
-
if (frame >= trailFrames) {
|
|
349
|
-
curve.push(null);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const uv = projectPointC6(camera, trail[frame * 3], trail[frame * 3 + 1], trail[frame * 3 + 2]);
|
|
353
|
-
curve.push(uv ? { frame, u: uv[0], v: uv[1] } : null);
|
|
354
|
-
}
|
|
355
|
-
return curve;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Re-anchor an authored curve in WORLD space and see it through another lens.
|
|
360
|
-
*
|
|
361
|
-
* A committed edit is uv through the camera it was authored in; painted
|
|
362
|
-
* verbatim after the view moves, those uv land wherever the new lens happens
|
|
363
|
-
* to point (the "line floats across the floor" bug). But the depth a naive
|
|
364
|
-
* reprojection lacks is sitting in the take: each curve point names a frame,
|
|
365
|
-
* and the joint's ORIGINAL world position at that frame is in `trail`. So per
|
|
366
|
-
* point: measure the authored uv against the original point's image under
|
|
367
|
-
* `fromCamera`, lift that uv offset into world with unprojectDeltaC6 (image-
|
|
368
|
-
* plane at the joint's own depth — the same rule the pin gesture uses), and
|
|
369
|
-
* project the resulting WORLD point through `toCamera`. An unedited point
|
|
370
|
-
* therefore lands exactly on the trajectory under the new lens, and an edited
|
|
371
|
-
* one shows its true world displacement from wherever the user now stands.
|
|
372
|
-
*
|
|
373
|
-
* Display only: the wire payload keeps the authored camera + uv untouched,
|
|
374
|
-
* because a 2D constraint is a ray through ITS lens — rewriting it through
|
|
375
|
-
* another lens would change what the sampler is asked to satisfy.
|
|
376
|
-
*
|
|
377
|
-
* `trail` is jointTrailPoints' flat Float32Array for the whole take. Points
|
|
378
|
-
* that cannot make the trip — null in, frame off the trail's end, original or
|
|
379
|
-
* edited point behind either lens — come back null, which the painter already
|
|
380
|
-
* treats as "no image at this frame". Returns null (not a partial curve) when
|
|
381
|
-
* the inputs themselves are unusable.
|
|
382
|
-
*/
|
|
383
|
-
export function reprojectCurveWorld(curve, trail, fromCamera, toCamera) {
|
|
384
|
-
if (!Array.isArray(curve) || !trail || !fromCamera || !toCamera) return null;
|
|
385
|
-
const trailFrames = Math.floor(trail.length / 3);
|
|
386
|
-
const out = [];
|
|
387
|
-
for (const point of curve) {
|
|
388
|
-
if (!point || !Number.isInteger(point.frame) || point.frame < 0 || point.frame >= trailFrames) {
|
|
389
|
-
out.push(null);
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
const x = trail[point.frame * 3];
|
|
393
|
-
const y = trail[point.frame * 3 + 1];
|
|
394
|
-
const z = trail[point.frame * 3 + 2];
|
|
395
|
-
const authored = projectPointC6(fromCamera, x, y, z);
|
|
396
|
-
if (!authored) {
|
|
397
|
-
out.push(null);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const delta = unprojectDeltaC6(fromCamera, [x, y, z], point.u - authored[0], point.v - authored[1]);
|
|
401
|
-
if (!delta) {
|
|
402
|
-
out.push(null);
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
const uv = projectPointC6(toCamera, x + delta[0], y + delta[1], z + delta[2]);
|
|
406
|
-
out.push(uv ? { frame: point.frame, u: uv[0], v: uv[1] } : null);
|
|
407
|
-
}
|
|
408
|
-
return out;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Pull one point of the curve and let the neighbours follow — a new curve out,
|
|
413
|
-
* the input untouched.
|
|
414
|
-
*
|
|
415
|
-
* `du`/`dv` are the pointer's total travel since the grab, in normalized uv,
|
|
416
|
-
* and they are applied to the curve AS IT WAS AT GRAB TIME (the caller keeps
|
|
417
|
-
* that snapshot). That makes a single drag idempotent: moving the pointer back
|
|
418
|
-
* to where it started restores the curve exactly, and re-running the function
|
|
419
|
-
* with a different radius mid-drag re-derives the whole deformation instead of
|
|
420
|
-
* compounding it. Successive drags stack because each new grab snapshots the
|
|
421
|
-
* curve the previous one produced.
|
|
422
|
-
*
|
|
423
|
-
* Null points stay null (no image, nothing to move) and the outermost
|
|
424
|
-
* PINNED_CURVE_ENDS points on each side are returned by reference, unmoved —
|
|
425
|
-
* the seam guarantee, argued at that constant.
|
|
426
|
-
*/
|
|
427
|
-
export function dragCurve(curve, grabIndex, du, dv, radiusFrames) {
|
|
428
|
-
if (!Array.isArray(curve)) return curve;
|
|
429
|
-
const deltaU = Number(du);
|
|
430
|
-
const deltaV = Number(dv);
|
|
431
|
-
if (!Number.isFinite(deltaU) || !Number.isFinite(deltaV)) return curve;
|
|
432
|
-
const length = curve.length;
|
|
433
|
-
return curve.map((point, index) => {
|
|
434
|
-
if (!point) return null;
|
|
435
|
-
if (isCurveEndPinned(index, length)) return point;
|
|
436
|
-
const weight = dragWeight(index - grabIndex, radiusFrames);
|
|
437
|
-
// Not an approximation of zero — an untouched point must come back as
|
|
438
|
-
// the SAME object so an unedited stretch of curve compares equal to the
|
|
439
|
-
// original without a float epsilon.
|
|
440
|
-
if (weight === 0) return point;
|
|
441
|
-
return { frame: point.frame, u: point.u + deltaU * weight, v: point.v + deltaV * weight };
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* The half-open app-frame window a pull actually touched.
|
|
447
|
-
*
|
|
448
|
-
* dragCurve returns UNTOUCHED points by reference (weight 0 and the pinned
|
|
449
|
-
* ends), so "touched" is object identity — no float epsilon, no threshold on a
|
|
450
|
-
* Gaussian tail. This exists because a pull used to inherit the panel's range,
|
|
451
|
-
* which defaults to the WHOLE CLIP: with no frames left outside the edit range
|
|
452
|
-
* there were no preserve rows, and the sampler re-rolled the entire body of the
|
|
453
|
-
* entire clip — a 25 px nudge moved frames four metres, eight seconds away. The
|
|
454
|
-
* frames the falloff touched are the only frames the artist asked about; the
|
|
455
|
-
* splice guards everything else.
|
|
456
|
-
*
|
|
457
|
-
* `pad` breathes a couple of frames so the seam ease has room; `minFrames`
|
|
458
|
-
* keeps a one-frame flick from asking the sampler for a sub-perceptual window.
|
|
459
|
-
* Returns null when nothing changed or the curve carries no frame numbers.
|
|
460
|
-
*/
|
|
461
|
-
export function changedFrameRange(before, after, { pad = 2, minFrames = 8, clipFrames } = {}) {
|
|
462
|
-
if (!Array.isArray(before) || !Array.isArray(after) || before.length !== after.length) return null;
|
|
463
|
-
let first = -1;
|
|
464
|
-
let last = -1;
|
|
465
|
-
for (let index = 0; index < after.length; index += 1) {
|
|
466
|
-
if (after[index] !== before[index]) {
|
|
467
|
-
if (first < 0) first = index;
|
|
468
|
-
last = index;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (first < 0) return null;
|
|
472
|
-
const firstFrame = after[first]?.frame ?? before[first]?.frame;
|
|
473
|
-
const lastFrame = after[last]?.frame ?? before[last]?.frame;
|
|
474
|
-
if (!Number.isFinite(firstFrame) || !Number.isFinite(lastFrame)) return null;
|
|
475
|
-
let start = Math.trunc(firstFrame) - pad;
|
|
476
|
-
let end = Math.trunc(lastFrame) + 1 + pad;
|
|
477
|
-
const grow = minFrames - (end - start);
|
|
478
|
-
if (grow > 0) {
|
|
479
|
-
start -= Math.ceil(grow / 2);
|
|
480
|
-
end += Math.floor(grow / 2);
|
|
481
|
-
}
|
|
482
|
-
start = Math.max(0, start);
|
|
483
|
-
if (Number.isFinite(clipFrames)) {
|
|
484
|
-
end = Math.min(end, clipFrames);
|
|
485
|
-
start = Math.max(0, Math.min(start, end - minFrames));
|
|
486
|
-
}
|
|
487
|
-
if (end - start < 2) return null;
|
|
488
|
-
return { startFrame: start, endFrame: end };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* The curve's points whose frame lies inside the half-open range.
|
|
493
|
-
*
|
|
494
|
-
* The wire pairing is positional: driver.py spreads points2d across frameRange
|
|
495
|
-
* by time, so the points MUST be exactly the range's own frames. A dragged
|
|
496
|
-
* curve spans whatever window it was projected over (the whole clip by
|
|
497
|
-
* default); once the committed range shrinks to the touched frames, sending the
|
|
498
|
-
* unsliced curve would compress eight seconds of trail into a half-second
|
|
499
|
-
* window. A curve that already spans the range comes back unchanged.
|
|
500
|
-
*/
|
|
501
|
-
export function sliceCurveToRange(curve, frameRange) {
|
|
502
|
-
if (!Array.isArray(curve) || !frameRange) return curve;
|
|
503
|
-
const { startFrame, endFrame } = frameRange;
|
|
504
|
-
if (!Number.isFinite(startFrame) || !Number.isFinite(endFrame)) return curve;
|
|
505
|
-
// Nulls (frames behind the lens) carry no frame number, but the curve is
|
|
506
|
-
// dense and index-aligned, so any named point anchors the index -> frame map
|
|
507
|
-
// and the nulls inherit their frame from position.
|
|
508
|
-
const anchor = curve.findIndex((point) => point && Number.isFinite(point.frame));
|
|
509
|
-
if (anchor < 0) return curve;
|
|
510
|
-
const base = curve[anchor].frame - anchor;
|
|
511
|
-
const sliced = curve.filter((point, index) => {
|
|
512
|
-
const frame = point && Number.isFinite(point.frame) ? point.frame : base + index;
|
|
513
|
-
return frame >= startFrame && frame < endFrame;
|
|
514
|
-
});
|
|
515
|
-
return sliced.length === curve.length ? curve : sliced;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/** How long a freehand stroke has to be before it is a STROKE rather than a
|
|
519
|
-
* click, in CSS pixels. Measured by the caller, which is the only side that
|
|
520
|
-
* knows how big the pane is; a press-and-release with a few pixels of hand
|
|
521
|
-
* tremor is a click, and a click must never wipe an edit that is already there
|
|
522
|
-
* (that exact regression is why the drag commit compares curvesEqual). */
|
|
523
|
-
export const DRAW_MIN_STROKE_PX = 8;
|
|
524
|
-
|
|
525
|
-
/** Fewest samples a stroke needs before it names a shape at all. One point is a
|
|
526
|
-
* dot: it has no direction, no length and nothing to parameterize. */
|
|
527
|
-
export const DRAW_MIN_STROKE_POINTS = 2;
|
|
528
|
-
|
|
529
|
-
/* ==================== the drawn stroke's frame association ==================
|
|
530
|
-
* THE PROBLEM THE PAPER DOES NOT HAVE. ProjFlow's 2D-to-3D lifting (paper 4.3.2)
|
|
531
|
-
* constrains frame n of the motion to the 2D point y_{n,j}, and it "follows the
|
|
532
|
-
* 2D heart trajectory exactly at every frame" — but that per-frame association
|
|
533
|
-
* is FREE in their setup, because their 2D condition is the reprojection of a
|
|
534
|
-
* real motion under a known camera: point n exists because frame n exists. A
|
|
535
|
-
* human drawing on a viewport supplies a SHAPE and nothing else. Every one of
|
|
536
|
-
* the three ways v1 was unusable comes from inventing that association badly:
|
|
537
|
-
*
|
|
538
|
-
* 1. the stroke was spread over the panel's frameRange, which defaults to the
|
|
539
|
-
* WHOLE CLIP, so a two-inch hook became an eight-second crawl;
|
|
540
|
-
* 2. the ends were pinned to the trail 2 frames in, so a stroke drawn away
|
|
541
|
-
* from the trail teleported the joint there inside 2 frames;
|
|
542
|
-
* 3. arc length was spread UNIFORMLY over those frames, erasing the take's own
|
|
543
|
-
* velocity profile (a fast fall + slow recovery became one constant glide).
|
|
544
|
-
*
|
|
545
|
-
* The fix for all three is to RECOVER the association the paper gets for free:
|
|
546
|
-
* match the stroke back onto the joint's own projected trail. The trail is the
|
|
547
|
-
* reprojection of the take under the live camera — exactly the object ProjFlow's
|
|
548
|
-
* y is built from — so "where on the trail did this stroke start and end" is a
|
|
549
|
-
* well-posed question with a screen-space answer, and answering it fixes the
|
|
550
|
-
* range (matchStrokeWindow), the timing (baseArcFractions) and the seam
|
|
551
|
-
* (seamEaseFrames) at once.
|
|
552
|
-
* ========================================================================== */
|
|
553
|
-
|
|
554
|
-
/** Shortest window, in frames, a matched stroke may name.
|
|
555
|
-
*
|
|
556
|
-
* Below this the two matched frames are not naming a stretch of path at all —
|
|
557
|
-
* they are the two ends of a stroke drawn ACROSS a bunched trail, where the
|
|
558
|
-
* whole visible path is a few pixels wide and both endpoints land on nearly the
|
|
559
|
-
* same frame. Rerouting 3 frames of a 24 fps take is a twitch, not an edit. */
|
|
560
|
-
export const DRAW_MIN_WINDOW_FRAMES = 8;
|
|
561
|
-
|
|
562
|
-
/** The window a bunched match falls back to: one second at the timeline's 24
|
|
563
|
-
* fps, centred on the frame both endpoints matched. "I drew across the path
|
|
564
|
-
* here" can only sanely mean "around this moment", and a second is the smallest
|
|
565
|
-
* span an artist reads as a gesture rather than a pop. */
|
|
566
|
-
export const DRAW_BUNCHED_WINDOW_FRAMES = 24;
|
|
567
|
-
|
|
568
|
-
/** How far apart, in CSS pixels, the two matched trail points must be before
|
|
569
|
-
* the match is believed.
|
|
570
|
-
*
|
|
571
|
-
* A frame-count test alone is not enough, and the take that motivated drawing
|
|
572
|
-
* proves it: a hand that travels two pixels across the image over 120 frames
|
|
573
|
-
* still has a strictly increasing trail, so a stroke drawn ACROSS it matches
|
|
574
|
-
* its ends to frames 0 and 119 and "reroute this part" silently becomes
|
|
575
|
-
* "reroute the whole clip" — the original complaint, reintroduced through the
|
|
576
|
-
* back door. The honest question is whether the stroke's two ends picked out
|
|
577
|
-
* two distinguishable PLACES on the path, and that is a screen-distance
|
|
578
|
-
* question. Twice the grab radius is the app's own scale for "the same spot":
|
|
579
|
-
* inside it a user could not have aimed at one end rather than the other. */
|
|
580
|
-
const DRAW_MATCH_MIN_SEPARATION_PX = 2 * CURVE_GRAB_RADIUS_PX;
|
|
581
|
-
|
|
582
|
-
/** Seam easing, replacing the hard 2-frame pin on the DRAW path only.
|
|
583
|
-
*
|
|
584
|
-
* PINNED_CURVE_ENDS argues why a drag pins: a Gaussian never reaches zero, so
|
|
585
|
-
* without a pin the drag's endpoint sits off the original trail by an amount
|
|
586
|
-
* that depends on where the user grabbed. A DRAWN stroke has the opposite
|
|
587
|
-
* problem — its endpoint is wherever the hand stopped, so a hard pin does not
|
|
588
|
-
* remove the gap, it CONCENTRATES it into the two frames next to the pin. GP2
|
|
589
|
-
* measured that: an arbitrary line pops the take 3.9x/8.0x its own median frame
|
|
590
|
-
* delta at the range edges.
|
|
591
|
-
*
|
|
592
|
-
* So the draw path ramps instead. Over the outer `fraction` of the window the
|
|
593
|
-
* curve is lerp(base, stroke, w) with w a smoothstep from 0 at the very edge to
|
|
594
|
-
* 1 in the interior. Two properties matter and both are structural:
|
|
595
|
-
* - w(edge) = 0 EXACTLY, so the first and last constrained frames are still
|
|
596
|
-
* byte-identically on the original trail (the pin's guarantee, kept);
|
|
597
|
-
* - w'(edge) = 0 too (smoothstep is flat at both ends), so the curve leaves
|
|
598
|
-
* the seam along the ORIGINAL trail's own slope. That is strictly stronger
|
|
599
|
-
* than the 2-frame pin, whose second point was there precisely because one
|
|
600
|
-
* pinned frame still let the slope jump.
|
|
601
|
-
* Because auto-ranging puts the stroke's ends near the trail to begin with, the
|
|
602
|
-
* ramp usually has almost nothing to absorb; when it does, it converts a
|
|
603
|
-
* teleport into a glide.
|
|
604
|
-
*
|
|
605
|
-
* 15% with a 12-frame cap is the documented GP2 follow-up ("free the first/last
|
|
606
|
-
* ~15% of line frames") clipped to the paper's own temporal neighbourhood:
|
|
607
|
-
* ProjFlow's dynamic masking uses l_max = 10 frames around a hard observation
|
|
608
|
-
* (Table 5), so a dozen frames is the scale at which the sampler already thinks
|
|
609
|
-
* about "near a constraint". The 2-frame floor keeps the shortest legal window
|
|
610
|
-
* behaving exactly like the pin it replaces. */
|
|
611
|
-
const SEAM_EASE_FRACTION = 0.15;
|
|
612
|
-
export const SEAM_EASE_MIN = 2;
|
|
613
|
-
export const SEAM_EASE_MAX = 12;
|
|
614
|
-
|
|
615
|
-
/** How many frames at EACH end of a window of `length` frames are eased.
|
|
616
|
-
* Capped at a third of the window so the two ramps can never meet and leave the
|
|
617
|
-
* stroke with no interior of its own. */
|
|
618
|
-
export function seamEaseFrames(length, { fraction = SEAM_EASE_FRACTION, min = SEAM_EASE_MIN, max = SEAM_EASE_MAX } = {}) {
|
|
619
|
-
const n = Math.trunc(length);
|
|
620
|
-
// A 2-frame window is nothing but its two edges: there is no interior for a
|
|
621
|
-
// ramp to reach, and 0 hands the caller back to the pin path, which says the
|
|
622
|
-
// same thing without pretending to ease anything.
|
|
623
|
-
if (!(n >= 3)) return 0;
|
|
624
|
-
const wanted = Math.round(n * fraction);
|
|
625
|
-
const capped = Math.min(max, Math.max(min, wanted));
|
|
626
|
-
return Math.max(1, Math.min(capped, Math.floor((n - 1) / 2)));
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** The ramp itself: 0 at either edge of the window, 1 once `easeFrames` in.
|
|
630
|
-
*
|
|
631
|
-
* Exported so the overlay and the tests read the SAME curve the blend applies —
|
|
632
|
-
* a second spelling of a falloff is how a drawn highlight drifts from the
|
|
633
|
-
* deformation it claims to describe (the lesson dragWeight already carries). */
|
|
634
|
-
export function seamEaseWeight(index, length, easeFrames) {
|
|
635
|
-
const n = Math.trunc(length);
|
|
636
|
-
const ease = Math.trunc(easeFrames);
|
|
637
|
-
if (!(n >= 2)) return 1;
|
|
638
|
-
if (!(ease > 0)) return index <= 0 || index >= n - 1 ? 0 : 1;
|
|
639
|
-
const distance = Math.min(index, n - 1 - index);
|
|
640
|
-
if (distance <= 0) return 0;
|
|
641
|
-
if (distance >= ease) return 1;
|
|
642
|
-
const t = distance / ease;
|
|
643
|
-
// smoothstep: flat at both ends, which is the slope-continuity claim above.
|
|
644
|
-
return t * t * (3 - 2 * t);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* The BASE trail's cumulative screen arc-length fraction at each frame of a
|
|
649
|
-
* window — the timing a drawn stroke is replayed with.
|
|
650
|
-
*
|
|
651
|
-
* This is redesign point (B), and it is what keeps a re-routed motion looking
|
|
652
|
-
* like the same performance. Uniform spacing along the stroke says "the joint
|
|
653
|
-
* crosses the new path at a constant speed", which is true of no motion anybody
|
|
654
|
-
* animates: the take's hand falls fast and recovers slowly, and spreading the
|
|
655
|
-
* stroke evenly turns both into one glide. Sampling the stroke at the fraction
|
|
656
|
-
* of arc length the ORIGINAL had already covered by frame f keeps the fast part
|
|
657
|
-
* fast and the slow part slow — the path changes, the phrasing does not.
|
|
658
|
-
*
|
|
659
|
-
* Screen-space arc length, not world: the stroke is a screen object, the trail
|
|
660
|
-
* it is replacing was measured on the same screen, and the two are only
|
|
661
|
-
* comparable there. Null frames (joint behind the lens) contribute zero length
|
|
662
|
-
* and inherit their neighbour's fraction, which keeps the sequence monotone
|
|
663
|
-
* without inventing a position for them.
|
|
664
|
-
*
|
|
665
|
-
* Returns an array of `curve.length` fractions rising from 0 to 1, or null when
|
|
666
|
-
* the base is degenerate (a stationary joint, total length ~0) — the caller
|
|
667
|
-
* falls back to uniform, which is the only honest reading of "no velocity
|
|
668
|
-
* profile to preserve".
|
|
669
|
-
*/
|
|
670
|
-
export function baseArcFractions(curve, { epsilon = 1e-9 } = {}) {
|
|
671
|
-
if (!Array.isArray(curve) || curve.length < 2) return null;
|
|
672
|
-
const cumulative = new Array(curve.length).fill(0);
|
|
673
|
-
let previous = null;
|
|
674
|
-
let total = 0;
|
|
675
|
-
for (let i = 0; i < curve.length; i += 1) {
|
|
676
|
-
const point = curve[i];
|
|
677
|
-
if (point && previous) total += Math.hypot(point.u - previous.u, point.v - previous.v);
|
|
678
|
-
if (point) previous = point;
|
|
679
|
-
cumulative[i] = total;
|
|
680
|
-
}
|
|
681
|
-
if (!(total > epsilon)) return null;
|
|
682
|
-
return cumulative.map((value) => value / total);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Which stretch of the joint's own path did this stroke draw over?
|
|
687
|
-
*
|
|
688
|
-
* `fullCurve` is projectTrailCurve run over the WHOLE clip — the original trail,
|
|
689
|
-
* never a previously drawn edit, so redrawing re-matches against the same
|
|
690
|
-
* reference instead of chasing its own last answer. The stroke's first and last
|
|
691
|
-
* samples are matched to their nearest trail frames in PIXELS (uv distance is
|
|
692
|
-
* anisotropic; the same argument nearestCurvePoint makes), and the two matched
|
|
693
|
-
* frames become the edit's range.
|
|
694
|
-
*
|
|
695
|
-
* A stroke drawn end-to-start is not an error and not a reversal of the take:
|
|
696
|
-
* the artist traced the same stretch of path the other way round. The window is
|
|
697
|
-
* ordered start < end regardless and `reversed` is reported, so the caller can
|
|
698
|
-
* walk the stroke backwards and land a curve that runs forwards in time.
|
|
699
|
-
*
|
|
700
|
-
* Returns `{ startFrame, endFrame, reversed }` (endFrame EXCLUSIVE, like every
|
|
701
|
-
* range on this wire) or null when nothing on the trail is on screen to match
|
|
702
|
-
* against — the caller then keeps whatever range the panel already shows, which
|
|
703
|
-
* is the only remaining meaning available.
|
|
704
|
-
*/
|
|
705
|
-
export function matchStrokeWindow(stroke, fullCurve, {
|
|
706
|
-
paneW,
|
|
707
|
-
paneH,
|
|
708
|
-
clipFrames,
|
|
709
|
-
minWindowFrames = DRAW_MIN_WINDOW_FRAMES,
|
|
710
|
-
bunchedWindowFrames = DRAW_BUNCHED_WINDOW_FRAMES,
|
|
711
|
-
minSeparationPx = DRAW_MATCH_MIN_SEPARATION_PX,
|
|
712
|
-
} = {}) {
|
|
713
|
-
if (!Array.isArray(stroke) || !Array.isArray(fullCurve) || fullCurve.length < MIN_LINE_POINTS) return null;
|
|
714
|
-
if (!(paneW > 0) || !(paneH > 0)) return null;
|
|
715
|
-
const samples = stroke.filter((s) => Array.isArray(s) && s.length >= 2 && Number.isFinite(Number(s[0])) && Number.isFinite(Number(s[1])));
|
|
716
|
-
if (samples.length < DRAW_MIN_STROKE_POINTS) return null;
|
|
717
|
-
|
|
718
|
-
// Unlike nearestCurvePoint this accepts EVERY on-screen point, ends included:
|
|
719
|
-
// matching is not grabbing, and the frame a stroke starts over is a perfectly
|
|
720
|
-
// good answer even when that frame would be undraggable.
|
|
721
|
-
const nearestFrame = (u, v) => {
|
|
722
|
-
let best = null;
|
|
723
|
-
for (const point of fullCurve) {
|
|
724
|
-
if (!isCurvePointOnScreen(point)) continue;
|
|
725
|
-
const dist = Math.hypot((point.u - u) * paneW, (point.v - v) * paneH);
|
|
726
|
-
if (!best || dist < best.dist) best = { frame: point.frame, u: point.u, v: point.v, dist };
|
|
727
|
-
}
|
|
728
|
-
return best;
|
|
729
|
-
};
|
|
730
|
-
const head = samples[0];
|
|
731
|
-
const tail = samples[samples.length - 1];
|
|
732
|
-
const a = nearestFrame(Number(head[0]), Number(head[1]));
|
|
733
|
-
const b = nearestFrame(Number(tail[0]), Number(tail[1]));
|
|
734
|
-
if (!a || !b) return null;
|
|
735
|
-
|
|
736
|
-
const reversed = b.frame < a.frame;
|
|
737
|
-
let start = Math.min(a.frame, b.frame);
|
|
738
|
-
let end = Math.max(a.frame, b.frame) + 1;
|
|
739
|
-
|
|
740
|
-
// The clip is the only hard wall; without one, the trail's own extent is.
|
|
741
|
-
const firstFrame = fullCurve[0]?.frame ?? 0;
|
|
742
|
-
const limit = Number.isFinite(clipFrames)
|
|
743
|
-
? Math.trunc(clipFrames)
|
|
744
|
-
: firstFrame + fullCurve.length;
|
|
745
|
-
const floor = Math.max(0, firstFrame);
|
|
746
|
-
|
|
747
|
-
const separationPx = Math.hypot((a.u - b.u) * paneW, (a.v - b.v) * paneH);
|
|
748
|
-
const bunched = end - start < Math.max(MIN_LINE_POINTS, Math.trunc(minWindowFrames))
|
|
749
|
-
|| separationPx < minSeparationPx;
|
|
750
|
-
if (bunched) {
|
|
751
|
-
// BUNCHED TRAIL. The two ends landed on the same PLACE on the path — one
|
|
752
|
-
// frame apart, or a couple of pixels apart on a trail that projects into
|
|
753
|
-
// a dot — so the stroke crosses the path rather than running along it and
|
|
754
|
-
// the frames it named are noise. Take a second around the moment it
|
|
755
|
-
// crossed: sliding the window to keep its length when it hits a clip
|
|
756
|
-
// edge, then clipping if the clip is shorter than a second.
|
|
757
|
-
const want = Math.max(MIN_LINE_POINTS, Math.trunc(bunchedWindowFrames));
|
|
758
|
-
const centre = (start + end) / 2;
|
|
759
|
-
start = Math.round(centre - want / 2);
|
|
760
|
-
end = start + want;
|
|
761
|
-
if (start < floor) { start = floor; end = start + want; }
|
|
762
|
-
if (end > limit) { end = limit; start = Math.max(floor, end - want); }
|
|
763
|
-
}
|
|
764
|
-
if (!(end - start >= MIN_LINE_POINTS)) return null;
|
|
765
|
-
return { startFrame: start, endFrame: end, reversed };
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/** The slice of a whole-clip curve a frame range names, with the frame numbers
|
|
769
|
-
* respected rather than assumed equal to the index. Returns null when the range
|
|
770
|
-
* is not fully covered — a half-covered window would silently shorten the edit
|
|
771
|
-
* and the caller would never know which frames it actually sent. */
|
|
772
|
-
export function curveWindow(curve, { startFrame, endFrame } = {}) {
|
|
773
|
-
if (!Array.isArray(curve) || curve.length === 0) return null;
|
|
774
|
-
const first = curve[0]?.frame;
|
|
775
|
-
// Nulls carry no frame, so anchor on the first point that has one and count
|
|
776
|
-
// from there — the array is dense in FRAMES by construction.
|
|
777
|
-
let offset = null;
|
|
778
|
-
for (let i = 0; i < curve.length; i += 1) {
|
|
779
|
-
if (curve[i]) { offset = curve[i].frame - i; break; }
|
|
780
|
-
}
|
|
781
|
-
if (offset === null) offset = Number.isFinite(first) ? first : 0;
|
|
782
|
-
const from = Math.trunc(startFrame) - offset;
|
|
783
|
-
const to = Math.trunc(endFrame) - offset;
|
|
784
|
-
if (!(from >= 0 && to <= curve.length && to - from >= MIN_LINE_POINTS)) return null;
|
|
785
|
-
return curve.slice(from, to);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
* A freehand stroke -> the same dense frame-indexed curve a drag produces.
|
|
790
|
-
*
|
|
791
|
-
* This is the OTHER half of the interaction: on takes whose projected trail
|
|
792
|
-
* bunches into a handful of screen pixels there is nothing to grab, so a press
|
|
793
|
-
* on empty space draws a new path instead of doing nothing. The result has to
|
|
794
|
-
* be indistinguishable from a dragged curve — same shape, same nulls, same
|
|
795
|
-
* pinned ends — because everything downstream (preview, undo, curveToPoints2d,
|
|
796
|
-
* the drift watcher) is written against that one type.
|
|
797
|
-
*
|
|
798
|
-
* ARC LENGTH, NOT SAMPLE INDEX. Frame at position `index` of the range samples
|
|
799
|
-
* the stroke at the point `index / (length - 1)` of the way along its TOTAL
|
|
800
|
-
* LENGTH, walking the polyline and interpolating inside the segment it lands
|
|
801
|
-
* in. That is what makes drawing SPEED irrelevant: a stroke drawn slowly at the
|
|
802
|
-
* start and flicked at the end carries a crowd of samples near its beginning,
|
|
803
|
-
* and sampling by sample index would hand the crowded part most of the frames
|
|
804
|
-
* and re-time the take. Arc length reads the stroke as a SHAPE, which is the
|
|
805
|
-
* only thing a hand-drawn line honestly carries. (Note the deliberate contrast
|
|
806
|
-
* with curveToPoints2d, which downsamples by index precisely because the curve
|
|
807
|
-
* it is given DOES carry timing — index i is frame startFrame + i.)
|
|
808
|
-
*
|
|
809
|
-
* The parameterization is endpoint-inclusive: index 0 sits at the stroke's
|
|
810
|
-
* start and index length-1 at its end, so the whole drawn shape is used.
|
|
811
|
-
*
|
|
812
|
-
* PINNED ENDS ARE THE SEAM GUARANTEE — or `easeFrames` is, which is the same
|
|
813
|
-
* guarantee said in a way a drawn line can keep. With `pinnedEnds` (the default,
|
|
814
|
-
* and what a DRAG uses) the first and last entries come back BY REFERENCE from
|
|
815
|
-
* `baseCurve`, exactly as dragCurve returns them. With `easeFrames` the outer
|
|
816
|
-
* frames are BLENDED toward the stroke instead, w = 0 at the very edge rising by
|
|
817
|
-
* smoothstep to 1 in the interior; see SEAM_EASE_FRACTION for why a drawn stroke
|
|
818
|
-
* needs the ramp and a drag does not. The two are mutually exclusive: an eased
|
|
819
|
-
* blend that also pinned would just move the discontinuity inward.
|
|
820
|
-
*
|
|
821
|
-
* TIMING. `arcFractions` (from baseArcFractions) replaces the uniform
|
|
822
|
-
* index/(n-1) parameterization with the base trail's own cumulative arc-length
|
|
823
|
-
* fraction, so the take's velocity profile survives the reroute. Absent, the
|
|
824
|
-
* mapping is uniform — the honest reading when there is no profile to preserve.
|
|
825
|
-
*
|
|
826
|
-
* `reversed` walks the stroke from its last sample to its first, for a stroke
|
|
827
|
-
* the artist traced end-to-start over the path.
|
|
828
|
-
*
|
|
829
|
-
* Nulls in `baseCurve` stay null, again like dragCurve: a frame whose joint is
|
|
830
|
-
* behind the lens has no image, and a stroke cannot invent one.
|
|
831
|
-
*
|
|
832
|
-
* @param {Array<[number, number]>} stroke pointer samples in the SAME
|
|
833
|
-
* viewport-normalized uv the curve lives in, in the order they were drawn.
|
|
834
|
-
* @param {Array<{frame:number,u:number,v:number}|null>} baseCurve the dense
|
|
835
|
-
* frame-indexed curve whose interior this stroke replaces (the curve as it is
|
|
836
|
-
* right now, so drawing twice means the second drawing wins and a drag after
|
|
837
|
-
* a draw refines the drawn curve).
|
|
838
|
-
* @param {object} [options]
|
|
839
|
-
* @param {number} [options.pinnedEnds] how many entries at each end stay put.
|
|
840
|
-
* @param {number} [options.easeFrames] ramp width in frames at each end;
|
|
841
|
-
* non-zero REPLACES pinnedEnds with a blend.
|
|
842
|
-
* @param {number[]} [options.arcFractions] per-index position along the stroke
|
|
843
|
-
* in 0..1; defaults to uniform.
|
|
844
|
-
* @param {boolean} [options.reversed] walk the stroke backwards.
|
|
845
|
-
* @param {number} [options.minLength] refuse strokes shorter than this in uv
|
|
846
|
-
* units. 0 (the default) still refuses a stroke of zero length — the callers
|
|
847
|
-
* measure their own threshold in PIXELS, where the user drew it.
|
|
848
|
-
* @returns a new curve, or null when the stroke is a no-op click.
|
|
849
|
-
*/
|
|
850
|
-
export function strokeToCurve(stroke, baseCurve, {
|
|
851
|
-
pinnedEnds = PINNED_CURVE_ENDS,
|
|
852
|
-
minLength = 0,
|
|
853
|
-
easeFrames = 0,
|
|
854
|
-
arcFractions = null,
|
|
855
|
-
reversed = false,
|
|
856
|
-
} = {}) {
|
|
857
|
-
if (!Array.isArray(baseCurve) || baseCurve.length < MIN_LINE_POINTS) return null;
|
|
858
|
-
if (!Array.isArray(stroke)) return null;
|
|
859
|
-
// Non-finite samples are dropped rather than poisoning the arc length: a
|
|
860
|
-
// pointer event outside a settled pane can produce one, and a single NaN
|
|
861
|
-
// would otherwise turn the whole curve into NaN and be refused much later
|
|
862
|
-
// with a message about the wire format.
|
|
863
|
-
const points = [];
|
|
864
|
-
for (const sample of stroke) {
|
|
865
|
-
if (!Array.isArray(sample) || sample.length < 2) continue;
|
|
866
|
-
const x = Number(sample[0]);
|
|
867
|
-
const y = Number(sample[1]);
|
|
868
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
869
|
-
points.push([x, y]);
|
|
870
|
-
}
|
|
871
|
-
if (points.length < DRAW_MIN_STROKE_POINTS) return null;
|
|
872
|
-
// Reversing here rather than at the sampler keeps ONE monotone walk below:
|
|
873
|
-
// a backwards stroke is just a forwards stroke over the flipped polyline,
|
|
874
|
-
// and arc length is direction-agnostic so the cumulative table is unchanged
|
|
875
|
-
// in meaning.
|
|
876
|
-
if (reversed) points.reverse();
|
|
877
|
-
const cumulative = [0];
|
|
878
|
-
for (let i = 1; i < points.length; i += 1) {
|
|
879
|
-
cumulative.push(cumulative[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));
|
|
880
|
-
}
|
|
881
|
-
const total = cumulative[cumulative.length - 1];
|
|
882
|
-
// A stroke that never moved is a click, and a click changes nothing.
|
|
883
|
-
if (!(total > 0) || total < minLength) return null;
|
|
884
|
-
|
|
885
|
-
const length = baseCurve.length;
|
|
886
|
-
// Never pin away the whole curve: a range shorter than 2 x pinnedEnds would
|
|
887
|
-
// otherwise come back byte-identical and read as "the user drew nothing".
|
|
888
|
-
const ease = Math.max(0, Math.trunc(easeFrames) || 0);
|
|
889
|
-
const pinned = ease > 0 ? 0 : Math.max(0, Math.min(Math.trunc(pinnedEnds) || 0, Math.floor(length / 2)));
|
|
890
|
-
const timing = Array.isArray(arcFractions) && arcFractions.length === length ? arcFractions : null;
|
|
891
|
-
// One monotone walk along the stroke for the whole curve: the targets only
|
|
892
|
-
// ever increase with index, so the cursor never rewinds and a 200-frame
|
|
893
|
-
// range costs one pass over the samples rather than 200 searches. Velocity
|
|
894
|
-
// timing keeps that property — arc-length fractions are non-decreasing.
|
|
895
|
-
let cursor = 1;
|
|
896
|
-
const out = new Array(length);
|
|
897
|
-
for (let index = 0; index < length; index += 1) {
|
|
898
|
-
const base = baseCurve[index];
|
|
899
|
-
if (pinned > 0 && (index < pinned || index >= length - pinned)) {
|
|
900
|
-
// BY REFERENCE, like dragCurve — an untouched point must compare
|
|
901
|
-
// equal with no epsilon to choose.
|
|
902
|
-
out[index] = base;
|
|
903
|
-
continue;
|
|
904
|
-
}
|
|
905
|
-
if (!base) {
|
|
906
|
-
out[index] = null;
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
const along = timing ? timing[index] : index / (length - 1);
|
|
910
|
-
const target = along * total;
|
|
911
|
-
while (cursor < cumulative.length - 1 && cumulative[cursor] < target) cursor += 1;
|
|
912
|
-
const span = cumulative[cursor] - cumulative[cursor - 1];
|
|
913
|
-
const fraction = span > 0 ? Math.min(1, Math.max(0, (target - cumulative[cursor - 1]) / span)) : 0;
|
|
914
|
-
const a = points[cursor - 1];
|
|
915
|
-
const b = points[cursor];
|
|
916
|
-
const u = a[0] + (b[0] - a[0]) * fraction;
|
|
917
|
-
const v = a[1] + (b[1] - a[1]) * fraction;
|
|
918
|
-
if (ease > 0) {
|
|
919
|
-
const weight = seamEaseWeight(index, length, ease);
|
|
920
|
-
// w === 0 returns the base point BY REFERENCE, so the outermost frame
|
|
921
|
-
// of an eased window keeps the pin's exact-equality property and
|
|
922
|
-
// curvesEqual still reads an untouched edge as untouched.
|
|
923
|
-
if (weight === 0) { out[index] = base; continue; }
|
|
924
|
-
out[index] = weight === 1
|
|
925
|
-
? { frame: base.frame, u, v }
|
|
926
|
-
: { frame: base.frame, u: base.u + (u - base.u) * weight, v: base.v + (v - base.v) * weight };
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
out[index] = { frame: base.frame, u, v };
|
|
930
|
-
}
|
|
931
|
-
return out;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* THE DRAW GESTURE, end to end: a raw stroke in, a committed edit out.
|
|
936
|
-
*
|
|
937
|
-
* One function because the three redesign decisions are one decision — the
|
|
938
|
-
* matched window IS the range, the range's base trail IS the timing, and the
|
|
939
|
-
* window's edges ARE the seam. Splitting them across the caller is how they
|
|
940
|
-
* drift apart. App.jsx therefore hands over the stroke and the geometry it was
|
|
941
|
-
* drawn on, and gets back everything a commit needs:
|
|
942
|
-
*
|
|
943
|
-
* { frameRange, base, curve, reversed, matched, easeFrames }
|
|
944
|
-
*
|
|
945
|
-
* `frameRange` is the panel's new range (redesign A) — drawing over a stretch of
|
|
946
|
-
* the visible path means "reroute THIS part", with no numbers typed. `base` is
|
|
947
|
-
* that window's slice of the original projected trail, i.e. the `original` half
|
|
948
|
-
* of the committed `{ camera, original, edited }`; `curve` is the `edited` half.
|
|
949
|
-
*
|
|
950
|
-
* FALLBACKS, in the order the brief fixes them:
|
|
951
|
-
* - nothing on the trail is on screen to match against -> keep the panel's
|
|
952
|
-
* range (`fallbackCurve` / `fallbackRange`) and draw into it, so the gesture
|
|
953
|
-
* still does something on a take framed out of shot;
|
|
954
|
-
* - the matched window is not fully covered by `fullCurve` (a trail shorter
|
|
955
|
-
* than the clip) -> same fallback, for the same reason;
|
|
956
|
-
* - the base window has no measurable arc length (a stationary joint) ->
|
|
957
|
-
* uniform timing, which is what baseArcFractions returning null means.
|
|
958
|
-
*
|
|
959
|
-
* Returns null for a no-op click, exactly as strokeToCurve does, so the caller's
|
|
960
|
-
* "a stray click must never wipe an existing edit" rule is unchanged.
|
|
961
|
-
*/
|
|
962
|
-
export function drawStrokeEdit(stroke, {
|
|
963
|
-
fullCurve = null,
|
|
964
|
-
fallbackCurve = null,
|
|
965
|
-
fallbackRange = null,
|
|
966
|
-
paneW,
|
|
967
|
-
paneH,
|
|
968
|
-
clipFrames,
|
|
969
|
-
minLength = 0,
|
|
970
|
-
minWindowFrames = DRAW_MIN_WINDOW_FRAMES,
|
|
971
|
-
bunchedWindowFrames = DRAW_BUNCHED_WINDOW_FRAMES,
|
|
972
|
-
minSeparationPx = DRAW_MATCH_MIN_SEPARATION_PX,
|
|
973
|
-
} = {}) {
|
|
974
|
-
const match = matchStrokeWindow(stroke, fullCurve, { paneW, paneH, clipFrames, minWindowFrames, bunchedWindowFrames, minSeparationPx });
|
|
975
|
-
const window = match ? curveWindow(fullCurve, match) : null;
|
|
976
|
-
const base = window ?? fallbackCurve;
|
|
977
|
-
if (!Array.isArray(base) || base.length < MIN_LINE_POINTS) return null;
|
|
978
|
-
const matched = !!window;
|
|
979
|
-
const frameRange = matched
|
|
980
|
-
? { startFrame: match.startFrame, endFrame: match.endFrame }
|
|
981
|
-
: (fallbackRange ?? null);
|
|
982
|
-
const easeFrames = seamEaseFrames(base.length);
|
|
983
|
-
const curve = strokeToCurve(stroke, base, {
|
|
984
|
-
minLength,
|
|
985
|
-
easeFrames,
|
|
986
|
-
arcFractions: baseArcFractions(base),
|
|
987
|
-
reversed: matched ? match.reversed : false,
|
|
988
|
-
});
|
|
989
|
-
if (!curve) return null;
|
|
990
|
-
return { frameRange, base, curve, reversed: matched ? match.reversed : false, matched, easeFrames };
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/* ========================= the third gesture: 3D pins =======================
|
|
994
|
-
* "순간 찍기" — scrub to a moment, grab the joint, put it where it should be.
|
|
995
|
-
*
|
|
996
|
-
* WHY IT BELONGS BESIDE THE OTHER TWO, in the paper's own terms. ProjFlow's
|
|
997
|
-
* entire spatial-control evaluation is SPARSE KEYFRAMES — 1, 2, 5, 49 and 196 of
|
|
998
|
-
* them — with 0.0000 trajectory/location/average error at every density, and the
|
|
999
|
-
* flow prior filling everything between. A drawn line is the dense end of that
|
|
1000
|
-
* axis (one constraint per frame); a pin is the sparse end, and the sparse end
|
|
1001
|
-
* is the one the method was measured on. Everything a stroke needs and a pin
|
|
1002
|
-
* does not — a camera, a frame association, a timing rule, a seam ease — exists
|
|
1003
|
-
* because a 2D stroke is an impoverished signal. A pin carries its own frame, is
|
|
1004
|
-
* already in 3D, and needs no lens — which is why a camera move leaves a pin
|
|
1005
|
-
* fully drawable (it is re-projected through the new lens and lands where the
|
|
1006
|
-
* artist put it) while a drawn curve can only be shown detached until the view
|
|
1007
|
-
* comes back. Both edits survive the move; only one of them can still be drawn.
|
|
1008
|
-
*
|
|
1009
|
-
* The exchange: a pin says nothing about the frames between pins, and the model
|
|
1010
|
-
* answers for them. That is the deal the paper's numbers are about.
|
|
1011
|
-
* ========================================================================== */
|
|
1012
|
-
|
|
1013
|
-
/** How many pins one edit may carry. ONE is legal (a line needs two points to
|
|
1014
|
-
* have a direction; a pin needs none); EIGHT is where keyframing by hand stops
|
|
1015
|
-
* being a refinement and starts being the pose studio's job. Mirrored in
|
|
1016
|
-
* tools/projflow/replay.mjs, which is the wire's own gate. */
|
|
1017
|
-
export const LINE_EDIT_PINS_MIN = 1;
|
|
1018
|
-
export const LINE_EDIT_PINS_MAX = 8;
|
|
1019
|
-
|
|
1020
|
-
/** Frames of CONTEXT either side of the pinned span, at the app's 24 fps.
|
|
1021
|
-
*
|
|
1022
|
-
* A pin is a point, but an edit is a passage: the model needs room to get the
|
|
1023
|
-
* limb into the pinned pose and back out of it, and pinning frame 100 with a
|
|
1024
|
-
* range of exactly [100, 101) would ask for an instantaneous arrival. Half a
|
|
1025
|
-
* second each way is the smallest span that reads as a movement rather than a
|
|
1026
|
-
* snap, and it is also comfortably wider than the driver's seam pin (2
|
|
1027
|
-
* generation frames), so the two never fight. */
|
|
1028
|
-
export const PIN_CONTEXT_FRAMES = 12;
|
|
1029
|
-
|
|
1030
|
-
/** A SINGLE pin has no span of its own, so it gets a window instead: 1.5 s
|
|
1031
|
-
* centred on it. Wider than 2 x PIN_CONTEXT_FRAMES on purpose — with two pins
|
|
1032
|
-
* the span between them already carries the edit, and with one there is nothing
|
|
1033
|
-
* but context. */
|
|
1034
|
-
export const PIN_SOLO_WINDOW_FRAMES = 36;
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* The frames a pins-only edit owns, from the pins themselves.
|
|
1038
|
-
*
|
|
1039
|
-
* The pins ARE the edit, so the range is derived, never typed — the same
|
|
1040
|
-
* "nobody sets the numeric range first" lesson matchStrokeWindow learned for
|
|
1041
|
-
* strokes. Half-open and clamped to the clip, like every range on this wire.
|
|
1042
|
-
*
|
|
1043
|
-
* Returns null when there are no pins or no clip to place them in.
|
|
1044
|
-
*/
|
|
1045
|
-
export function pinsFrameRange(pins, clipFrames, {
|
|
1046
|
-
context = PIN_CONTEXT_FRAMES,
|
|
1047
|
-
soloWindow = PIN_SOLO_WINDOW_FRAMES,
|
|
1048
|
-
} = {}) {
|
|
1049
|
-
if (!Array.isArray(pins) || pins.length === 0) return null;
|
|
1050
|
-
const frames = pins.map((pin) => Math.trunc(pin?.frame)).filter(Number.isFinite);
|
|
1051
|
-
if (frames.length === 0) return null;
|
|
1052
|
-
const limit = Number.isFinite(clipFrames) ? Math.trunc(clipFrames) : 0;
|
|
1053
|
-
if (!(limit >= MIN_LINE_POINTS)) return null;
|
|
1054
|
-
const first = Math.min(...frames);
|
|
1055
|
-
const last = Math.max(...frames);
|
|
1056
|
-
let start;
|
|
1057
|
-
let end;
|
|
1058
|
-
if (first === last) {
|
|
1059
|
-
const want = Math.max(MIN_LINE_POINTS, Math.trunc(soloWindow));
|
|
1060
|
-
start = Math.round(first - want / 2);
|
|
1061
|
-
end = start + want;
|
|
1062
|
-
} else {
|
|
1063
|
-
start = first - Math.trunc(context);
|
|
1064
|
-
end = last + 1 + Math.trunc(context);
|
|
1065
|
-
}
|
|
1066
|
-
// Slide to keep the length when one edge hits the clip, then clip.
|
|
1067
|
-
const span = end - start;
|
|
1068
|
-
if (start < 0) { start = 0; end = Math.min(limit, span); }
|
|
1069
|
-
if (end > limit) { end = limit; start = Math.max(0, end - span); }
|
|
1070
|
-
// Every pin must stay INSIDE the half-open range the wire validates against.
|
|
1071
|
-
start = Math.min(start, first);
|
|
1072
|
-
end = Math.max(end, last + 1);
|
|
1073
|
-
start = Math.max(0, start);
|
|
1074
|
-
end = Math.min(limit, end);
|
|
1075
|
-
if (end - start < MIN_LINE_POINTS) return null;
|
|
1076
|
-
return { startFrame: start, endFrame: end };
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
/**
|
|
1080
|
-
* Add or replace a pin, keeping the list ascending and capped.
|
|
1081
|
-
*
|
|
1082
|
-
* PINNING THE SAME FRAME TWICE REPLACES, it does not stack — a frame has one
|
|
1083
|
-
* answer to "where is the hand", and the second gesture is the artist changing
|
|
1084
|
-
* their mind, exactly as drawing twice replaces a stroke. Over the cap the
|
|
1085
|
-
* OLDEST pin is dropped rather than the new one refused: a gesture the artist
|
|
1086
|
-
* just made must do something visible, and silently ignoring the ninth pin is
|
|
1087
|
-
* the kind of nothing that made drawing feel broken.
|
|
1088
|
-
*
|
|
1089
|
-
* Returns a NEW array; the input is untouched.
|
|
1090
|
-
*/
|
|
1091
|
-
export function upsertPin(pins, pin, { max = LINE_EDIT_PINS_MAX } = {}) {
|
|
1092
|
-
if (!pin || !Number.isInteger(pin.frame) || !Array.isArray(pin.position) || pin.position.length !== 3) return pins;
|
|
1093
|
-
if (!pin.position.every(Number.isFinite)) return pins;
|
|
1094
|
-
const entry = { frame: pin.frame, position: [pin.position[0], pin.position[1], pin.position[2]] };
|
|
1095
|
-
const kept = (Array.isArray(pins) ? pins : []).filter((existing) => existing.frame !== entry.frame);
|
|
1096
|
-
kept.push(entry);
|
|
1097
|
-
kept.sort((a, b) => a.frame - b.frame);
|
|
1098
|
-
// Dropping from the FRONT keeps the pin just placed and the ones nearest it
|
|
1099
|
-
// in time, which is what "I am working on this passage" looks like.
|
|
1100
|
-
return kept.length > max ? kept.slice(kept.length - max) : kept;
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* The edited curve -> the `points2d` C6 sends, or a refusal.
|
|
1105
|
-
*
|
|
1106
|
-
* Returns `{ points2d }` or `{ error }` where error is one of:
|
|
1107
|
-
* "empty" — fewer than 2 points have an image at all
|
|
1108
|
-
* "offscreen" — some point sits outside 0..1, which the bridge refuses
|
|
1109
|
-
*
|
|
1110
|
-
* DOWNSAMPLING IS EVEN BY INDEX, i.e. even IN TIME, and that is the opposite
|
|
1111
|
-
* of what the old freehand path did (even by arc length). The reason is that
|
|
1112
|
-
* the two inputs mean different things. A freehand stroke carries no timing —
|
|
1113
|
-
* the only sane reading is "constant speed along the shape", so arc-length
|
|
1114
|
-
* spacing was right there. This curve is the joint's own trajectory: index i is
|
|
1115
|
-
* frame startFrame + i, and the box spreads the points evenly across
|
|
1116
|
-
* frameRange. Arc-length resampling would therefore RE-TIME the take — it
|
|
1117
|
-
* would hand the slow parts of the motion fewer samples and the box would
|
|
1118
|
-
* stretch them back out to equal duration, turning a pause into a glide. Even
|
|
1119
|
-
* index sampling keeps every sent point on the frame it was projected from.
|
|
1120
|
-
*
|
|
1121
|
-
* The first and last kept points are always included, so the pinned endpoints
|
|
1122
|
-
* from dragCurve survive into the payload rather than being resampled away.
|
|
1123
|
-
*/
|
|
1124
|
-
export function curveToPoints2d(curve) {
|
|
1125
|
-
const visible = Array.isArray(curve) ? curve.filter(Boolean) : [];
|
|
1126
|
-
if (visible.length < MIN_LINE_POINTS) return { error: "empty" };
|
|
1127
|
-
for (const point of visible) {
|
|
1128
|
-
if (!isCurvePointOnScreen(point)) return { error: "offscreen" };
|
|
1129
|
-
}
|
|
1130
|
-
const count = Math.min(MAX_LINE_POINTS, visible.length);
|
|
1131
|
-
const last = visible.length - 1;
|
|
1132
|
-
const points2d = [];
|
|
1133
|
-
for (let i = 0; i < count; i += 1) {
|
|
1134
|
-
// Rounded rather than floored so the sampling is symmetric about the
|
|
1135
|
-
// middle; i = count-1 lands exactly on `last`, i = 0 exactly on 0.
|
|
1136
|
-
const source = Math.round((i * last) / (count - 1));
|
|
1137
|
-
const point = visible[source];
|
|
1138
|
-
points2d.push([point.u, point.v]);
|
|
1139
|
-
}
|
|
1140
|
-
return { points2d };
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* Which curve point is under the pointer? `{ index, dist }` or null.
|
|
1145
|
-
*
|
|
1146
|
-
* The hit test is in PIXELS, not uv, because uv distance is anisotropic: on a
|
|
1147
|
-
* 16:9 pane one unit of u is nearly twice one unit of v, so a uv-radius grab
|
|
1148
|
-
* zone is a visible ellipse and the curve is hardest to grab exactly where it
|
|
1149
|
-
* runs vertically. paneW/paneH convert once, here.
|
|
1150
|
-
*
|
|
1151
|
-
* Points with no image, points outside the viewport and the pinned ends are
|
|
1152
|
-
* all skipped — a marker the user can grab but that cannot move is worse than
|
|
1153
|
-
* no marker, so the overlay does not draw those as draggable either.
|
|
1154
|
-
*/
|
|
1155
|
-
export function nearestCurvePoint(curve, u, v, maxDistPx, paneW, paneH) {
|
|
1156
|
-
if (!Array.isArray(curve) || !(paneW > 0) || !(paneH > 0)) return null;
|
|
1157
|
-
let best = null;
|
|
1158
|
-
for (let index = 0; index < curve.length; index += 1) {
|
|
1159
|
-
const point = curve[index];
|
|
1160
|
-
if (!isCurvePointOnScreen(point)) continue;
|
|
1161
|
-
if (isCurveEndPinned(index, curve.length)) continue;
|
|
1162
|
-
const dx = (point.u - u) * paneW;
|
|
1163
|
-
const dy = (point.v - v) * paneH;
|
|
1164
|
-
const dist = Math.hypot(dx, dy);
|
|
1165
|
-
if (dist > maxDistPx) continue;
|
|
1166
|
-
if (!best || dist < best.dist) best = { index, dist };
|
|
1167
|
-
}
|
|
1168
|
-
return best;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/** Are these two curves the same curve? The "has the user actually pulled
|
|
1172
|
-
* anything yet?" test, and the reason dragCurve returns untouched points by
|
|
1173
|
-
* reference: an unmoved curve compares equal exactly, with no tolerance to
|
|
1174
|
-
* choose. Compared on value anyway so a rebuilt-but-identical curve (a camera
|
|
1175
|
-
* re-projection that landed in the same place) also reads as unedited. */
|
|
1176
|
-
export function curvesEqual(a, b) {
|
|
1177
|
-
if (a === b) return true;
|
|
1178
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1179
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
1180
|
-
const pa = a[i];
|
|
1181
|
-
const pb = b[i];
|
|
1182
|
-
if (pa === pb) continue;
|
|
1183
|
-
if (!pa || !pb) return false;
|
|
1184
|
-
if (pa.frame !== pb.frame || pa.u !== pb.u || pa.v !== pb.v) return false;
|
|
1185
|
-
}
|
|
1186
|
-
return true;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
/**
|
|
1190
|
-
* Has the camera moved out from under the curve?
|
|
1191
|
-
*
|
|
1192
|
-
* Compared on the C6 block itself rather than on three's camera object, so the
|
|
1193
|
-
* test is on exactly the numbers that were sent — a re-derived but identical
|
|
1194
|
-
* pose reads as "not moved" even after a matrix rebuild.
|
|
1195
|
-
*
|
|
1196
|
-
* A true means the 2D offsets on this curve CANNOT BE RE-INTERPRETED through the
|
|
1197
|
-
* new camera: there is no correct way to read offsets authored through one lens
|
|
1198
|
-
* as offsets through another, and no depth to reproject them with. It does not
|
|
1199
|
-
* mean the edit is void. An UNEDITED curve is simply re-projected from the new
|
|
1200
|
-
* camera and follows the view; a COMMITTED one keeps the lens it was authored
|
|
1201
|
-
* with (the caller stores `{ camera, original, edited }` and sends that camera),
|
|
1202
|
-
* so drift makes it undrawable and un-extendable in the live view, not invalid.
|
|
1203
|
-
*/
|
|
1204
|
-
export function cameraDrifted(a, b, epsilon = CAMERA_DRIFT_EPSILON) {
|
|
1205
|
-
if (!a || !b) return true;
|
|
1206
|
-
if (Math.abs(a.fx - b.fx) > epsilon || Math.abs(a.fy - b.fy) > epsilon) return true;
|
|
1207
|
-
if (Math.abs(a.cx - b.cx) > epsilon || Math.abs(a.cy - b.cy) > epsilon) return true;
|
|
1208
|
-
for (let i = 0; i < 9; i += 1) if (Math.abs(a.R[i] - b.R[i]) > epsilon) return true;
|
|
1209
|
-
for (let i = 0; i < 3; i += 1) if (Math.abs(a.t[i] - b.t[i]) > epsilon) return true;
|
|
1210
|
-
return false;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
/**
|
|
1214
|
-
* Last gate before a line edit goes on the wire (contract C6).
|
|
1215
|
-
*
|
|
1216
|
-
* Returns `null` when the payload is good, otherwise
|
|
1217
|
-
* `{ code, message }` — `code` is a stable token the UI maps to localized copy
|
|
1218
|
-
* and `message` is the English detail worth putting in a log or a test name.
|
|
1219
|
-
* Structured the same way as the bridge's own validateGenerate: one specific
|
|
1220
|
-
* reason, named field first, and the first failure wins.
|
|
1221
|
-
*
|
|
1222
|
-
* `clipFrames` is the loaded take's frame count on the APP timeline clock —
|
|
1223
|
-
* C6's frameRange is in app clip frames, and unlike waypoints/motionEdit it is
|
|
1224
|
-
* NOT converted to the bridge clock here.
|
|
1225
|
-
*/
|
|
1226
|
-
export function validateLineEdit(lineEdit, { clipFrames } = {}) {
|
|
1227
|
-
const fail = (code, message) => ({ code, message });
|
|
1228
|
-
if (!lineEdit || typeof lineEdit !== "object" || Array.isArray(lineEdit)) {
|
|
1229
|
-
return fail("shape", "lineEdit must be an object");
|
|
1230
|
-
}
|
|
1231
|
-
if (typeof lineEdit.sourceMotion !== "string" || !lineEdit.sourceMotion) {
|
|
1232
|
-
// A line edit REWRITES a take; without the bridge-side source npz there
|
|
1233
|
-
// is nothing to rewrite and the box would silently generate from scratch.
|
|
1234
|
-
return fail("sourceMotion", "field 'lineEdit.sourceMotion' must be a non-empty motion url");
|
|
1235
|
-
}
|
|
1236
|
-
if (!LINE_EDIT_TRACK_IDS.includes(lineEdit.track)) {
|
|
1237
|
-
return fail("track", `field 'lineEdit.track' must be one of the ${LINE_EDIT_TRACK_IDS.length} ik track ids, got ${JSON.stringify(lineEdit.track)}`);
|
|
1238
|
-
}
|
|
1239
|
-
const range = lineEdit.frameRange;
|
|
1240
|
-
if (!range || typeof range !== "object" || !Number.isInteger(range.startFrame) || !Number.isInteger(range.endFrame)) {
|
|
1241
|
-
return fail("frameRange", "field 'lineEdit.frameRange' must be { startFrame, endFrame } integers");
|
|
1242
|
-
}
|
|
1243
|
-
if (range.startFrame < 0 || range.endFrame <= range.startFrame) {
|
|
1244
|
-
return fail("frameRange", `field 'lineEdit.frameRange' must satisfy 0 <= startFrame < endFrame, got ${range.startFrame}..${range.endFrame}`);
|
|
1245
|
-
}
|
|
1246
|
-
// endFrame is EXCLUSIVE, like every other half-open range crossing this
|
|
1247
|
-
// boundary (preserve.editRanges, the prompt schedule), so it may equal the
|
|
1248
|
-
// clip length but never exceed it.
|
|
1249
|
-
if (Number.isFinite(clipFrames) && range.endFrame > clipFrames) {
|
|
1250
|
-
return fail("frameRange", `field 'lineEdit.frameRange.endFrame' must be <= the clip's ${clipFrames} frames, got ${range.endFrame}`);
|
|
1251
|
-
}
|
|
1252
|
-
// One constrained frame cannot follow a LINE; two points need two frames.
|
|
1253
|
-
if (Number.isFinite(clipFrames) && range.endFrame - range.startFrame < MIN_LINE_POINTS) {
|
|
1254
|
-
return fail("frameRange", "field 'lineEdit.frameRange' must span at least 2 frames");
|
|
1255
|
-
}
|
|
1256
|
-
// PINS — the camera-free gesture. Checked before points2d so a pins payload
|
|
1257
|
-
// is never told it needs two points of a field it deliberately omitted; the
|
|
1258
|
-
// wire's own gate (tools/projflow/replay.mjs validateLineEditFields) applies
|
|
1259
|
-
// the identical rules, and this one exists so the panel can refuse in the
|
|
1260
|
-
// artist's language before anything is sent.
|
|
1261
|
-
if (lineEdit.pins3d !== undefined) {
|
|
1262
|
-
if (lineEdit.points2d !== undefined) {
|
|
1263
|
-
return fail("pins", "a line edit is one gesture: send either points2d or pins3d, not both");
|
|
1264
|
-
}
|
|
1265
|
-
if (lineEdit.camera !== undefined) {
|
|
1266
|
-
return fail("pins", "field 'lineEdit.camera' must be omitted for a pins edit — pins are world-space");
|
|
1267
|
-
}
|
|
1268
|
-
const pins = lineEdit.pins3d;
|
|
1269
|
-
if (!Array.isArray(pins) || pins.length < LINE_EDIT_PINS_MIN) {
|
|
1270
|
-
return fail("pins", `field 'lineEdit.pins3d' needs at least ${LINE_EDIT_PINS_MIN} pin`);
|
|
1271
|
-
}
|
|
1272
|
-
if (pins.length > LINE_EDIT_PINS_MAX) {
|
|
1273
|
-
return fail("pins", `field 'lineEdit.pins3d' is capped at ${LINE_EDIT_PINS_MAX} pins, got ${pins.length}`);
|
|
1274
|
-
}
|
|
1275
|
-
let previous = -1;
|
|
1276
|
-
for (let i = 0; i < pins.length; i += 1) {
|
|
1277
|
-
const pin = pins[i];
|
|
1278
|
-
if (!pin || typeof pin !== "object" || Array.isArray(pin) || !Number.isInteger(pin.frame)) {
|
|
1279
|
-
return fail("pins", `field 'lineEdit.pins3d[${i}]' must be { frame: integer, position: [x, y, z] }`);
|
|
1280
|
-
}
|
|
1281
|
-
if (pin.frame < range.startFrame || pin.frame >= range.endFrame) {
|
|
1282
|
-
return fail("pins", `field 'lineEdit.pins3d[${i}].frame' ${pin.frame} is outside frameRange ${range.startFrame}..${range.endFrame}`);
|
|
1283
|
-
}
|
|
1284
|
-
if (pin.frame <= previous) {
|
|
1285
|
-
return fail("pins", `field 'lineEdit.pins3d' frames must be strictly ascending, got ${pin.frame} after ${previous}`);
|
|
1286
|
-
}
|
|
1287
|
-
previous = pin.frame;
|
|
1288
|
-
if (!Array.isArray(pin.position) || pin.position.length !== 3 || !pin.position.every(Number.isFinite)) {
|
|
1289
|
-
return fail("pins", `field 'lineEdit.pins3d[${i}].position' must be [x, y, z] finite numbers`);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
if (lineEdit.prompt !== undefined && typeof lineEdit.prompt !== "string") {
|
|
1293
|
-
return fail("prompt", "field 'lineEdit.prompt' must be a string when present");
|
|
1294
|
-
}
|
|
1295
|
-
return null;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
const points = lineEdit.points2d;
|
|
1299
|
-
if (!Array.isArray(points) || points.length < MIN_LINE_POINTS) {
|
|
1300
|
-
return fail("points", `field 'lineEdit.points2d' needs at least ${MIN_LINE_POINTS} points`);
|
|
1301
|
-
}
|
|
1302
|
-
if (points.length > MAX_LINE_POINTS) {
|
|
1303
|
-
return fail("points", `field 'lineEdit.points2d' is capped at ${MAX_LINE_POINTS} points, got ${points.length}`);
|
|
1304
|
-
}
|
|
1305
|
-
for (let i = 0; i < points.length; i += 1) {
|
|
1306
|
-
const point = points[i];
|
|
1307
|
-
if (!Array.isArray(point) || point.length !== 2 || !point.every(Number.isFinite)) {
|
|
1308
|
-
return fail("points", `field 'lineEdit.points2d[${i}]' must be [u, v] finite numbers`);
|
|
1309
|
-
}
|
|
1310
|
-
if (point[0] < 0 || point[0] > 1 || point[1] < 0 || point[1] > 1) {
|
|
1311
|
-
return fail("points", `field 'lineEdit.points2d[${i}]' must be viewport-normalized into 0..1, got ${JSON.stringify(point)}`);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const camera = lineEdit.camera;
|
|
1315
|
-
if (!camera || typeof camera !== "object") return fail("camera", "field 'lineEdit.camera' is required");
|
|
1316
|
-
for (const key of ["fx", "fy", "cx", "cy"]) {
|
|
1317
|
-
if (!Number.isFinite(camera[key])) return fail("camera", `field 'lineEdit.camera.${key}' must be a finite number`);
|
|
1318
|
-
}
|
|
1319
|
-
// A non-positive focal length means the uv/NDC flip was applied twice (or
|
|
1320
|
-
// not at all) and every solved position would be mirrored.
|
|
1321
|
-
if (camera.fx <= 0 || camera.fy <= 0) {
|
|
1322
|
-
return fail("camera", "field 'lineEdit.camera' has a non-positive focal length — the uv convention is inverted");
|
|
1323
|
-
}
|
|
1324
|
-
if (!Array.isArray(camera.R) || camera.R.length !== 9 || !camera.R.every(Number.isFinite)) {
|
|
1325
|
-
return fail("camera", "field 'lineEdit.camera.R' must be 9 finite numbers (3x3 row-major)");
|
|
1326
|
-
}
|
|
1327
|
-
if (!Array.isArray(camera.t) || camera.t.length !== 3 || !camera.t.every(Number.isFinite)) {
|
|
1328
|
-
return fail("camera", "field 'lineEdit.camera.t' must be 3 finite numbers");
|
|
1329
|
-
}
|
|
1330
|
-
if (lineEdit.prompt !== undefined && typeof lineEdit.prompt !== "string") {
|
|
1331
|
-
return fail("prompt", "field 'lineEdit.prompt' must be a string when present");
|
|
1332
|
-
}
|
|
1333
|
-
return null;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
/**
|
|
1337
|
-
* Does this generate failure mean "the bridge does not know about lineEdit
|
|
1338
|
-
* yet"? Wave 2 lands the routing; until then a request either 400s on the
|
|
1339
|
-
* unknown field or is refused by the capability preflight. Both deserve the
|
|
1340
|
-
* same calm "not connected yet" answer rather than a red error card, so the
|
|
1341
|
-
* classification lives here where it can be tested against real reason
|
|
1342
|
-
* strings.
|
|
1343
|
-
*/
|
|
1344
|
-
export function isLineEditUnsupported(message) {
|
|
1345
|
-
if (typeof message !== "string" || !message) return false;
|
|
1346
|
-
if (/HTTP\s*400/i.test(message)) return true;
|
|
1347
|
-
return /line\s*edit/i.test(message) && /unknown|unsupported|not supported|unrecognis|unrecogniz|must not|invalid field/i.test(message);
|
|
1348
|
-
}
|