cozyclay 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +211 -36
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +4 -4
- package/bin/live/cli.mjs +23 -8
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +2 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-C9pMpW7s.js +1 -0
- package/dist/assets/app-B_2XB9rw.css +1 -0
- package/dist/assets/app-DAuvKPP8.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-CvKuBgy5.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-msLI71g1.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-ChUvB_zL.js} +1 -1
- package/dist/assets/scene-asset-sync-DKkpjKBV.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-BPk2Ni95.js} +1 -1
- package/dist/assets/workflow-jJRBR94Z.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/llms.txt +14 -0
- package/dist/sitemap.xml +1 -1
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +813 -278
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +184 -6
- package/src/ardy/timeline.jsx +121 -36
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +0 -3
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +6 -7
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +15 -2
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +79 -28
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +49 -4
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +58 -9
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +60 -16
- package/src/shell/CompositionGuideButton.jsx +20 -0
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/ShotPreviewHeader.jsx +17 -0
- package/src/shell/TopBar.jsx +18 -3
- package/src/shell/ViewportToolbar.jsx +36 -31
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shell/viewport.css +12 -0
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +213 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +60 -24
- package/src/studio-app-binding.js +56 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +92 -9
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +106 -18
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/gt-render/browser.mjs +23 -0
- package/tools/headless-studio.mjs +85 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +95 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-BxBLC6GT.js +0 -4876
- package/dist/assets/app-CJdxvnrl.css +0 -1
- package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
- package/dist/assets/workflow-B5XZwVfe.js +0 -179
package/src/ardy/auto-physics.js
CHANGED
|
@@ -50,8 +50,6 @@ import { findBone, ikTouch, ikBakeKeyframe, solveHipsTranslate, solveIk } from "
|
|
|
50
50
|
* into the clip outside its keyed range by ikEvaluate's blend window.
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
/* --- body segments ---------------------------------------------------------- */
|
|
54
|
-
|
|
55
53
|
/**
|
|
56
54
|
* Segment mass fractions of total body mass, from Dempster's cadaver study
|
|
57
55
|
* (Dempster, W.T., "Space Requirements of the Seated Operator", WADC TR
|
|
@@ -113,8 +111,6 @@ export function computeCenterOfMass(rig) {
|
|
|
113
111
|
return com.divideScalar(total);
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
/* --- airborne detection ----------------------------------------------------- */
|
|
117
|
-
|
|
118
114
|
/** Shortest run of airborne frames worth correcting. Anything briefer is a
|
|
119
115
|
* one- or two-frame detection blip from a foot skimming the floor, not a
|
|
120
116
|
* flight phase, and fitting a parabola to it would amplify the noise. */
|
|
@@ -215,20 +211,18 @@ export function frameAirborne(rig, planted, lift = AIRBORNE_LIFT) {
|
|
|
215
211
|
return heightsAirborne(markerHeights(rig), planted, lift);
|
|
216
212
|
}
|
|
217
213
|
|
|
218
|
-
/* --- grounded contact ------------------------------------------------------ */
|
|
219
|
-
|
|
220
214
|
/** Contact must stay close to its clip-relative low and vertically settled.
|
|
221
215
|
* The final run gate below is stricter still: an ankle that wanders more than
|
|
222
216
|
* three centimetres was walking, not planted, and is never snapped backward. */
|
|
223
217
|
export const GROUND_CONTACT_LIFT = 0.06;
|
|
224
|
-
|
|
218
|
+
const GROUND_CONTACT_VERTICAL_SPEED = 0.6;
|
|
225
219
|
export const GROUND_CONTACT_HORIZONTAL_SPEED = 0.12;
|
|
226
|
-
|
|
220
|
+
const GROUND_CONTACT_MIN_SECONDS = 0.10;
|
|
227
221
|
export const GROUND_LOCK_MAX_PULL = 0.03;
|
|
228
222
|
export const GROUND_LOCK_EPSILON = 0.005;
|
|
229
223
|
export const GROUND_LOCK_REACH_LIFT = 0.005;
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
const GROUND_FLOOR_MAX_LIFT = 0.15;
|
|
225
|
+
const GROUND_FLOOR_EPSILON = 0.005;
|
|
232
226
|
|
|
233
227
|
const FOOT_SIDES = [
|
|
234
228
|
{ side: "left", chainId: "leftFoot", ankle: "mixamorigLeftFoot", toe: "mixamorigLeftToeBase", height: "LeftFoot" },
|
|
@@ -494,8 +488,6 @@ export function edgeTaperWeight(index, length, taper = EDGE_TAPER) {
|
|
|
494
488
|
return x * x * (3 - 2 * x);
|
|
495
489
|
}
|
|
496
490
|
|
|
497
|
-
/* --- ballistic fit ---------------------------------------------------------- */
|
|
498
|
-
|
|
499
491
|
/** Standard gravity (m/s²). */
|
|
500
492
|
export const GRAVITY = 9.81;
|
|
501
493
|
|
|
@@ -628,7 +620,7 @@ export function fitBallistic(samples, fps, g = GRAVITY, { fitFrom = 0, fitTo = n
|
|
|
628
620
|
* `residual` is the RMS distance of the samples from that curve — how
|
|
629
621
|
* un-ballistic the arc was to begin with.
|
|
630
622
|
*/
|
|
631
|
-
|
|
623
|
+
function fitBallisticArc(samples, fps, g = GRAVITY) {
|
|
632
624
|
const list = samples ?? [];
|
|
633
625
|
const rate = Number.isFinite(fps) && fps > 0 ? fps : 30;
|
|
634
626
|
const times = list.map((_, index) => index / rate);
|
|
@@ -650,8 +642,6 @@ export function fitBallisticArc(samples, fps, g = GRAVITY) {
|
|
|
650
642
|
return { positions, y0, v0, g, residual: Math.sqrt(sumSq / list.length) };
|
|
651
643
|
}
|
|
652
644
|
|
|
653
|
-
/* --- the driver ------------------------------------------------------------- */
|
|
654
|
-
|
|
655
645
|
/** Deviation from the fitted parabola a span must reach before it is worth
|
|
656
646
|
* correcting. 5 mm is well under the eye's threshold on a character-sized
|
|
657
647
|
* silhouette and comfortably above the noise in a generated root track. */
|
|
@@ -666,7 +656,7 @@ export const MAX_CORRECTION = 0.5;
|
|
|
666
656
|
/** Worst RMS a fit may have and still be believed. A real flight phase fits a
|
|
667
657
|
* parabola to a couple of centimetres; a mis-detected span (a whole walk
|
|
668
658
|
* misread as one jump) fits to a metre or more. */
|
|
669
|
-
|
|
659
|
+
const MAX_FIT_RESIDUAL = 0.25;
|
|
670
660
|
|
|
671
661
|
/** Clearance kept between the lowest foot marker and the floor when a
|
|
672
662
|
* downward correction is applied. */
|
|
@@ -34,8 +34,8 @@ const DEG = Math.PI / 180;
|
|
|
34
34
|
|
|
35
35
|
/** Namespace prefixes. Kept as exports so the App's QA surface and the tests
|
|
36
36
|
* agree on the spelling instead of both hard-coding a string. */
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const OBJECT_BLOCKER_PREFIX = "obj:";
|
|
38
|
+
const CHARACTER_BLOCKER_PREFIX = "char:";
|
|
39
39
|
|
|
40
40
|
/** `obj:<sceneObjectId>` — one box per scene object. */
|
|
41
41
|
export function objectBlockerId(sceneObjectId) {
|
|
@@ -58,8 +58,6 @@ export function blockerCharacterId(blockerId) {
|
|
|
58
58
|
return cut > 0 ? rest.slice(0, cut) : null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/* --- scene objects ---------------------------------------------------------- */
|
|
62
|
-
|
|
63
61
|
/**
|
|
64
62
|
* Resolve an object's world size. `objectSize` (scene-objects.js) is the ONE
|
|
65
63
|
* owner of footprint × scale, so it is reused rather than re-derived here — a
|
|
@@ -157,8 +155,6 @@ export function sceneObjectBlockers(sceneObjects, { library = null, frame = null
|
|
|
157
155
|
return out;
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
/* --- other cast members ----------------------------------------------------- */
|
|
161
|
-
|
|
162
158
|
/** rigs may arrive as the App's plain `{ [characterId]: rig }` map or as a Map;
|
|
163
159
|
* both are read through one iterator so a caller never has to convert. */
|
|
164
160
|
function rigEntries(rigs) {
|
|
@@ -264,7 +260,7 @@ export function characterBlockers(rigs, activeId = null, { radii = null, charact
|
|
|
264
260
|
export function collisionBlockers({ rigs = null, activeId = null, characterIds = null, sceneObjects = null, library = null, frame = null, take = null, skipIds = null, radii = null } = {}) {
|
|
265
261
|
return [
|
|
266
262
|
...characterBlockers(rigs, activeId, { radii, characterIds }),
|
|
267
|
-
...sceneObjectBlockers(sceneObjects
|
|
263
|
+
...sceneObjectBlockers(sceneObjects, { library, frame, take, skipIds, characters: characterIds }),
|
|
268
264
|
];
|
|
269
265
|
}
|
|
270
266
|
|
|
@@ -39,8 +39,6 @@ import {
|
|
|
39
39
|
* outside its keyed range by ikEvaluate's blend window.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
/* --- capsule table ---------------------------------------------------------- */
|
|
43
|
-
|
|
44
42
|
/**
|
|
45
43
|
* Body capsules. `movable.chain`/`movable.joint` name the IK chain and the
|
|
46
44
|
* joint of that chain to drive ("mid" = elbow/knee, "effector" = wrist/
|
|
@@ -77,8 +75,6 @@ const CAPSULE_DEFS = [
|
|
|
77
75
|
{ id: "rightFoot", start: "mixamorigRightFoot", end: "mixamorigRightToeBase", radiusJoint: "RightFoot", maxRadius: 0.07, movable: { chain: "rightFoot", joint: "effector" }, priority: 1 },
|
|
78
76
|
];
|
|
79
77
|
|
|
80
|
-
/* --- fingers ---------------------------------------------------------------- */
|
|
81
|
-
|
|
82
78
|
/**
|
|
83
79
|
* FINGER CAPSULES. The hand capsule spans wrist → middle-finger base, which is
|
|
84
80
|
* a palm: it still reads a pose where the FINGERS are buried in a thigh and the
|
|
@@ -302,8 +298,6 @@ export function buildCollisionCapsules(rig, radii = null) {
|
|
|
302
298
|
return capsules;
|
|
303
299
|
}
|
|
304
300
|
|
|
305
|
-
/* --- rest-relative calibration ---------------------------------------------- */
|
|
306
|
-
|
|
307
301
|
/** Stable key for an unordered capsule pair. */
|
|
308
302
|
function pairKey(idA, idB) {
|
|
309
303
|
return idA < idB ? `${idA}|${idB}` : `${idB}|${idA}`;
|
|
@@ -389,8 +383,6 @@ export function restPairOverlaps(rig, radii = null) {
|
|
|
389
383
|
return overlaps;
|
|
390
384
|
}
|
|
391
385
|
|
|
392
|
-
/* --- detection -------------------------------------------------------------- */
|
|
393
|
-
|
|
394
386
|
/** Closest points between two segments (Ericson, Real-Time Collision
|
|
395
387
|
* Detection — the textbook segment/segment distance). Writes into outA/outB
|
|
396
388
|
* and returns the squared distance. */
|
|
@@ -458,8 +450,6 @@ function capsulesAdjacent(ca, cb) {
|
|
|
458
450
|
return ca.bones.some((ba) => cb.bones.some((bb) => bonesRelated(ba, bb)));
|
|
459
451
|
}
|
|
460
452
|
|
|
461
|
-
/* --- hinged pairs ------------------------------------------------------------ */
|
|
462
|
-
|
|
463
453
|
/**
|
|
464
454
|
* HINGED PAIRS. A blanket "skip everything within HOP_LIMIT" made whole classes
|
|
465
455
|
* of real interpenetration invisible: a knee driven into the belly is
|
|
@@ -585,8 +575,6 @@ function pairGeometry(ca, cb) {
|
|
|
585
575
|
return geometry;
|
|
586
576
|
}
|
|
587
577
|
|
|
588
|
-
/* --- external blockers ------------------------------------------------------- */
|
|
589
|
-
|
|
590
578
|
/**
|
|
591
579
|
* EXTERNAL BLOCKERS. Everything above is the character against itself; a hand
|
|
592
580
|
* swinging through a chair is the same problem against the SET. Callers pass
|
|
@@ -1016,8 +1004,6 @@ function chainsInPlay(pens) {
|
|
|
1016
1004
|
return ids;
|
|
1017
1005
|
}
|
|
1018
1006
|
|
|
1019
|
-
/* --- escape direction ------------------------------------------------------- */
|
|
1020
|
-
|
|
1021
1007
|
/**
|
|
1022
1008
|
* REST-BIASED ESCAPE. The separation normal is the SHORTEST way out, which for
|
|
1023
1009
|
* a deep hit is regularly the wrong side of the blocker: a forearm driven into
|
|
@@ -1075,11 +1061,11 @@ export const MIN_ALIGN = 0.35;
|
|
|
1075
1061
|
* biases only choose the side. Weighted like HOME_BIAS, because it is the same
|
|
1076
1062
|
* kind of hint and answers the same question — where does this limb belong.
|
|
1077
1063
|
*/
|
|
1078
|
-
|
|
1064
|
+
const CONTINUITY_BIAS = 0.6;
|
|
1079
1065
|
|
|
1080
1066
|
/** The weight the STEP CAP's retry uses when the plain one produced a jump: the
|
|
1081
1067
|
* previous frame's direction outvotes the rest pose rather than tying with it. */
|
|
1082
|
-
|
|
1068
|
+
const CONTINUITY_BIAS_STRONG = 1.6;
|
|
1083
1069
|
|
|
1084
1070
|
/**
|
|
1085
1071
|
* THE STEP CAP, as a multiple of the clip's own worst adjacent step for the
|
|
@@ -1089,18 +1075,18 @@ export const CONTINUITY_BIAS_STRONG = 1.6;
|
|
|
1089
1075
|
* to lead or lag the clip's own motion by a good margin while a 20× jump — the
|
|
1090
1076
|
* flail QA measured — cannot fit inside it.
|
|
1091
1077
|
*/
|
|
1092
|
-
|
|
1078
|
+
const STEP_CAP_FACTOR = 2.5;
|
|
1093
1079
|
|
|
1094
1080
|
/** Ceiling on that cap, in metres. A clip whose hand already crosses the frame
|
|
1095
1081
|
* in one step must not thereby license a half-metre correction jump; ~24 frames
|
|
1096
1082
|
* a second, so 0.25 m is 6 m/s of hand — past any human hand speed a viewer
|
|
1097
1083
|
* would read as continuous. */
|
|
1098
|
-
|
|
1084
|
+
const STEP_CAP_ABS = 0.25;
|
|
1099
1085
|
|
|
1100
1086
|
/** Floor on that cap, in metres. A perfectly static clip has a zero worst step,
|
|
1101
1087
|
* and a cap of zero would refuse every frame that follows a corrected one. 5 mm
|
|
1102
1088
|
* is the millimetre noise of the proxies themselves. */
|
|
1103
|
-
|
|
1089
|
+
const STEP_CAP_FLOOR = 0.005;
|
|
1104
1090
|
|
|
1105
1091
|
/** The bone a capsule's push actually drives: mid joint (elbow/knee) for an
|
|
1106
1092
|
* upper segment, effector (wrist/ankle) for a lower segment or an end sphere. */
|
|
@@ -1157,8 +1143,6 @@ function escapeVector(rig, capsule, chains, normal, contactPoint, distance, weig
|
|
|
1157
1143
|
return biased.multiplyScalar(distance / Math.max(MIN_ALIGN, align));
|
|
1158
1144
|
}
|
|
1159
1145
|
|
|
1160
|
-
/* --- side preservation ------------------------------------------------------ */
|
|
1161
|
-
|
|
1162
1146
|
/** Chains whose end joint must not change sides of the body because of a fix. */
|
|
1163
1147
|
const SIDE_CHAINS = ["leftHand", "rightHand"];
|
|
1164
1148
|
|
|
@@ -1184,9 +1168,6 @@ function sagittalSide(rig, point) {
|
|
|
1184
1168
|
return point.clone().sub(origin).dot(lateral.normalize());
|
|
1185
1169
|
}
|
|
1186
1170
|
|
|
1187
|
-
/* --- resolution ------------------------------------------------------------- */
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
1171
|
const NO_PUSH = Object.freeze({ ran: false, moved: false });
|
|
1191
1172
|
|
|
1192
1173
|
/**
|
|
@@ -1339,7 +1320,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1339
1320
|
return outcome.moved;
|
|
1340
1321
|
};
|
|
1341
1322
|
|
|
1342
|
-
/* --- one pass, at a given home-bias weight ----------------------------- */
|
|
1343
1323
|
// Every push of a pass is computed from the pose the pass STARTED in, so the
|
|
1344
1324
|
// same penetration list can be replayed at a different weight (the side
|
|
1345
1325
|
// guard below) and produce a deterministic result either way.
|
|
@@ -1376,7 +1356,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1376
1356
|
return passChanged;
|
|
1377
1357
|
};
|
|
1378
1358
|
|
|
1379
|
-
/* --- side-preservation guard ------------------------------------------- */
|
|
1380
1359
|
/**
|
|
1381
1360
|
* A hand must not change sides of the body because of a fix. The heuristic,
|
|
1382
1361
|
* deliberately blunt: measure each arm chain's WRIST against the sagittal
|
|
@@ -1433,7 +1412,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1433
1412
|
* permitted chain when a filter is in force. */
|
|
1434
1413
|
const fixablePens = (capsules) => fixablePenetrations(capsules, { offset, slackFactor, blockers, onlyChains });
|
|
1435
1414
|
|
|
1436
|
-
/* --- what this frame started as, so a bad fix can be refused whole ------ */
|
|
1437
1415
|
/** Every bone a fix or a yield can write, with its full local transform. */
|
|
1438
1416
|
const frameSnapshot = () => {
|
|
1439
1417
|
const entries = [];
|
|
@@ -1540,7 +1518,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1540
1518
|
changed = changed || passChanged;
|
|
1541
1519
|
if (!passChanged) break;
|
|
1542
1520
|
}
|
|
1543
|
-
/* --- capped torso/head yield -------------------------------------------- */
|
|
1544
1521
|
/**
|
|
1545
1522
|
* The last resort, and only ever a last one: the limb passes are over and a
|
|
1546
1523
|
* movable × static pair is still penetrating, so the STATIC side leans away
|
|
@@ -1725,7 +1702,6 @@ export function fixCollisions(rig, chains, {
|
|
|
1725
1702
|
// changed the pose, and the caller must key it.
|
|
1726
1703
|
changed = changed || poseDirty;
|
|
1727
1704
|
|
|
1728
|
-
/* --- the accept gate ---------------------------------------------------- */
|
|
1729
1705
|
/**
|
|
1730
1706
|
* A frame's fix is kept only if it is BETTER than the pose it started from.
|
|
1731
1707
|
* Two ways it can fail to be, each measured against the entry pose:
|
|
@@ -1857,7 +1833,6 @@ export function fixCollisionsRange({
|
|
|
1857
1833
|
const start = Math.max(0, Math.round(startFrame));
|
|
1858
1834
|
const end = Math.max(start, Math.round(endFrame));
|
|
1859
1835
|
|
|
1860
|
-
/* --- sweep 1: record the clip as it stands, writing nothing ------------ */
|
|
1861
1836
|
const bones = layerBones(chains, fkJoints);
|
|
1862
1837
|
const chainIds = [...chains.keys()];
|
|
1863
1838
|
const readChainQuats = () => new Map(chainIds.map((id) => [id, chains.get(id).bones.map((b) => b.quaternion.clone())]));
|
|
@@ -1892,7 +1867,6 @@ export function fixCollisionsRange({
|
|
|
1892
1867
|
if (!layerActive) clipQuats.push(readChainQuats());
|
|
1893
1868
|
}
|
|
1894
1869
|
|
|
1895
|
-
/* --- what a frame-to-frame step is allowed to be ------------------------ */
|
|
1896
1870
|
/**
|
|
1897
1871
|
* THE STEP CAP, per chain, measured on the clip the pass was handed. A
|
|
1898
1872
|
* correction is allowed to move a limb; what it is not allowed to do is move
|
|
@@ -1923,7 +1897,6 @@ export function fixCollisionsRange({
|
|
|
1923
1897
|
stepCap.set(id, Math.min(STEP_CAP_ABS, Math.max(STEP_CAP_FLOOR, STEP_CAP_FACTOR * worst)));
|
|
1924
1898
|
}
|
|
1925
1899
|
|
|
1926
|
-
/* --- sweep 2: fix each frame against that recorded pose ---------------- */
|
|
1927
1900
|
const keyed = [];
|
|
1928
1901
|
// Frame → deepest penetration left standing. A map, not a list: a frame the
|
|
1929
1902
|
// blend-window round revisits reports its LAST word, and a frame that round
|
|
@@ -1937,7 +1910,6 @@ export function fixCollisionsRange({
|
|
|
1937
1910
|
if (residual > 0) residuals.set(frame, residual);
|
|
1938
1911
|
else residuals.delete(frame);
|
|
1939
1912
|
};
|
|
1940
|
-
/* --- temporal continuity ------------------------------------------------ */
|
|
1941
1913
|
/**
|
|
1942
1914
|
* WHY THIS EXISTS. Every frame above is solved on its own, from the clip,
|
|
1943
1915
|
* which is what makes each one honest — and what made a RUN of them garbage.
|
|
@@ -2192,7 +2164,6 @@ export function fixCollisionsRange({
|
|
|
2192
2164
|
};
|
|
2193
2165
|
}
|
|
2194
2166
|
|
|
2195
|
-
/* --- sweep 3: the blend window's own frames, ONCE ----------------------- */
|
|
2196
2167
|
/**
|
|
2197
2168
|
* Sweeps 1 and 2 judge every frame against the CLIP, which is what stops the
|
|
2198
2169
|
* pass chasing its own tail. The cost is that the ramp each new key runs out
|
|
@@ -2241,7 +2212,6 @@ export function fixCollisionsRange({
|
|
|
2241
2212
|
keyed.sort((a, b) => a - b);
|
|
2242
2213
|
}
|
|
2243
2214
|
|
|
2244
|
-
/* --- final sweep: the truth on the pose the viewer will actually see ----- */
|
|
2245
2215
|
/**
|
|
2246
2216
|
* Everything above judges a frame at the moment it is solved. What the viewer
|
|
2247
2217
|
* gets is the EVALUATED pose — clip plus every key's blend ramp — and the last
|
package/src/ardy/ik-drag.js
CHANGED
|
@@ -40,21 +40,23 @@ export function chainsChangedBy(chains, write) {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Read the RAW clip locals of `ids` at the current frame without disturbing the
|
|
43
|
-
* live pose: snapshot every node under `rig`, let `applyRaw(rig)` pose the clip
|
|
43
|
+
* live pose: snapshot every node under `rig`, let `applyRaw(rig, frame)` pose the clip
|
|
44
44
|
* alone (no correction layer), read chain b0..b2 quaternions and FK joint
|
|
45
45
|
* quaternion + position, then put every node back.
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
function captureRawBase(rig, chains, fkJoints, ids, applyRaw, frame = null) {
|
|
48
48
|
const saved = [];
|
|
49
49
|
rig.traverse((node) => saved.push([node, node.position.clone(), node.quaternion.clone(), node.scale.clone()]));
|
|
50
50
|
const baseQuats = new Map();
|
|
51
51
|
const basePositions = new Map();
|
|
52
52
|
try {
|
|
53
|
-
applyRaw(rig);
|
|
53
|
+
applyRaw(rig, frame);
|
|
54
54
|
for (const id of ids) {
|
|
55
55
|
const chain = chains?.get(id);
|
|
56
56
|
const joint = fkJoints?.get(id);
|
|
57
|
-
if (chain)
|
|
57
|
+
if (chain) {
|
|
58
|
+
baseQuats.set(id, chain.bones.map((bone) => bone.quaternion.clone()));
|
|
59
|
+
}
|
|
58
60
|
else if (joint) {
|
|
59
61
|
baseQuats.set(id, [joint.bone.quaternion.clone()]);
|
|
60
62
|
basePositions.set(id, joint.bone.position.clone());
|
|
@@ -83,7 +85,7 @@ export function bakeIkDragKey(chains, fkJoints, frame, ids, applyRaw = null) {
|
|
|
83
85
|
const list = [...ids];
|
|
84
86
|
if (!list.length) return null;
|
|
85
87
|
const rig = chains?.values().next().value?.rig;
|
|
86
|
-
const base = applyRaw && rig ? captureRawBase(rig, chains, fkJoints, list, applyRaw) : null;
|
|
88
|
+
const base = applyRaw && rig ? captureRawBase(rig, chains, fkJoints, list, applyRaw, frame) : null;
|
|
87
89
|
const scratch = createIkState();
|
|
88
90
|
ikBakeKeyframe(chains, scratch, frame, fkJoints, list, null, base?.baseQuats ?? null);
|
|
89
91
|
const entry = scratch.keys.get(frame);
|
|
@@ -99,3 +101,82 @@ export function bakeIkDragKey(chains, fkJoints, frame, ids, applyRaw = null) {
|
|
|
99
101
|
}
|
|
100
102
|
return entry;
|
|
101
103
|
}
|
|
104
|
+
|
|
105
|
+
/** Apply one drag's correction delta to a raw pose. The source key is captured
|
|
106
|
+
* at the drag frame, while `base` belongs to a different raw frame. */
|
|
107
|
+
function applyIkDragCorrection(chains, fkJoints, sourceEntry, base) {
|
|
108
|
+
if (!sourceEntry || !base) return;
|
|
109
|
+
for (const [id, key] of sourceEntry) {
|
|
110
|
+
const chain = chains?.get(id);
|
|
111
|
+
if (chain) {
|
|
112
|
+
const rawQuats = base.baseQuats.get(id);
|
|
113
|
+
if (rawQuats && key.q) {
|
|
114
|
+
const sourceQuats = key.baseQ;
|
|
115
|
+
chain.bones.forEach((bone, index) => {
|
|
116
|
+
if (!rawQuats[index] || !key.q[index]) return;
|
|
117
|
+
const delta = sourceQuats?.[index]
|
|
118
|
+
? sourceQuats[index].clone().invert().multiply(key.q[index])
|
|
119
|
+
: null;
|
|
120
|
+
if (!delta) return;
|
|
121
|
+
bone.quaternion.copy(rawQuats[index]).multiply(delta);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// `chainP` is an absolute legacy/explicit translation payload. A drag
|
|
125
|
+
// correction does not retain the source frame's raw chain positions, so
|
|
126
|
+
// carrying it to another clip frame would freeze that frame's segment
|
|
127
|
+
// lengths. Motion IK drags solve rotations; leave chain translations to
|
|
128
|
+
// the raw clip unless a future key format carries a translation delta.
|
|
129
|
+
chain.bones[0].updateMatrixWorld(true);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const joint = fkJoints?.get(id);
|
|
133
|
+
if (!joint) continue;
|
|
134
|
+
const rawQ = base.baseQuats.get(id)?.[0];
|
|
135
|
+
if (rawQ && key.q?.[0]) {
|
|
136
|
+
const sourceQ = key.baseQ?.[0];
|
|
137
|
+
if (sourceQ) {
|
|
138
|
+
const delta = sourceQ.clone().invert().multiply(key.q[0]);
|
|
139
|
+
joint.bone.quaternion.copy(rawQ).multiply(delta);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const rawPos = base.basePositions.get(id);
|
|
143
|
+
if (rawPos && key.p && key.basePos) {
|
|
144
|
+
joint.bone.position.copy(rawPos).add(key.p.clone().sub(key.basePos));
|
|
145
|
+
}
|
|
146
|
+
joint.bone.updateMatrixWorld(true);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Bake the captured drag correction at the inclusive range boundaries. */
|
|
151
|
+
export function bakeIkDragRange(chains, fkJoints, sourceEntry, startFrame, endFrame, ids, applyRaw = null) {
|
|
152
|
+
const frames = [...new Set([Math.round(startFrame), Math.round(endFrame)])].sort((a, b) => a - b);
|
|
153
|
+
const result = new Map();
|
|
154
|
+
const rig = chains?.values().next().value?.rig;
|
|
155
|
+
const saved = [];
|
|
156
|
+
rig?.traverse((node) => saved.push([node, node.position.clone(), node.quaternion.clone(), node.scale.clone()]));
|
|
157
|
+
const correctionRange = { start: frames[0], end: frames.at(-1) };
|
|
158
|
+
try {
|
|
159
|
+
for (const frame of frames) {
|
|
160
|
+
if (!applyRaw || !rig) {
|
|
161
|
+
result.set(frame, sourceEntry
|
|
162
|
+
? new Map([...sourceEntry].map(([id, key]) => [id, { ...key, correctionRange: { ...correctionRange } }]))
|
|
163
|
+
: null);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const applyFrame = () => applyRaw(rig, frame);
|
|
167
|
+
const base = captureRawBase(rig, chains, fkJoints, ids, applyFrame);
|
|
168
|
+
applyFrame();
|
|
169
|
+
applyIkDragCorrection(chains, fkJoints, sourceEntry, base);
|
|
170
|
+
const entry = bakeIkDragKey(chains, fkJoints, frame, ids, applyFrame);
|
|
171
|
+
result.set(frame, entry ? new Map([...entry].map(([id, key]) => [id, { ...key, correctionRange: { ...correctionRange } }])) : null);
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
for (const [node, position, quaternion, scale] of saved) {
|
|
175
|
+
node.position.copy(position);
|
|
176
|
+
node.quaternion.copy(quaternion);
|
|
177
|
+
node.scale.copy(scale);
|
|
178
|
+
}
|
|
179
|
+
rig?.updateMatrixWorld(true);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
package/src/ardy/ik-key-json.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as THREE from "three";
|
|
|
6
6
|
* positions as {x,y,z}. Optional fields are written only when the key has
|
|
7
7
|
* them, so a key without a stored `blend` stays one without it.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
function ikTrackKeyJson(key) {
|
|
10
10
|
const quaternion = (q) => ({ x: q.x, y: q.y, z: q.z, w: q.w });
|
|
11
11
|
const vector = (p) => ({ x: p.x, y: p.y, z: p.z });
|
|
12
12
|
return {
|
|
@@ -16,6 +16,7 @@ export function ikTrackKeyJson(key) {
|
|
|
16
16
|
...(key.basePos ? { basePos: vector(key.basePos) } : {}),
|
|
17
17
|
...(key.chainP ? { chainP: key.chainP.map(vector) } : {}),
|
|
18
18
|
...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
19
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
19
20
|
...(key.blend != null ? { blend: key.blend } : {}),
|
|
20
21
|
...(key.pin != null ? { pin: key.pin } : {}),
|
|
21
22
|
};
|
|
@@ -37,6 +38,7 @@ export function ikTrackKeyFromJson(key) {
|
|
|
37
38
|
...(key.basePos ? { basePos: vector(key.basePos) } : {}),
|
|
38
39
|
...(key.chainP ? { chainP: key.chainP.map(vector) } : {}),
|
|
39
40
|
...(key.keepTranslations ? { keepTranslations: true } : {}),
|
|
41
|
+
...(key.correctionRange ? { correctionRange: { start: key.correctionRange.start, end: key.correctionRange.end } } : {}),
|
|
40
42
|
...(key.blend != null ? { blend: key.blend } : {}),
|
|
41
43
|
...(key.pin != null ? { pin: key.pin } : {}),
|
|
42
44
|
};
|
package/src/ardy/ik.js
CHANGED
|
@@ -64,8 +64,7 @@ function pointSegmentDistance(point, start, end) {
|
|
|
64
64
|
return point.distanceTo(start.clone().addScaledVector(segment, t));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/*
|
|
68
|
-
*
|
|
67
|
+
/*
|
|
69
68
|
* `rig.userData.poseBind` (primed by primeBindPose at clone time) is the only
|
|
70
69
|
* record of the UNTOUCHED skeleton once a clip is playing: ARDY playback writes
|
|
71
70
|
* per-bone translations AND rotations every frame, so "where is this bone"
|
|
@@ -100,8 +99,7 @@ function bindLocalMatrix(rig, node, out, delta = null) {
|
|
|
100
99
|
return out.compose(bindPosition, bindQuaternion, node.scale);
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
/*
|
|
104
|
-
*
|
|
102
|
+
/*
|
|
105
103
|
* The bind pose is a T-pose; the pose a character actually STANDS in is
|
|
106
104
|
* DEFAULT_POSE — "arms hanging naturally at their sides" — which poses.js
|
|
107
105
|
* applies as Euler deltas PRE-multiplied onto each bind local rotation. The two
|
|
@@ -319,7 +317,7 @@ export function measureContactRadii(rig) {
|
|
|
319
317
|
* Bone points and vertices are read in the same pose, for the same reason
|
|
320
318
|
* measureContactRadii does: this is cached forever off whichever frame called
|
|
321
319
|
* first, so it must not be able to depend on that frame. */
|
|
322
|
-
|
|
320
|
+
function measureContactHeights(rig) {
|
|
323
321
|
if (!rig) return {};
|
|
324
322
|
const cached = contactHeightCache.get(rig);
|
|
325
323
|
if (cached) return cached;
|
|
@@ -792,8 +790,6 @@ export function solveHipsTranslate(joint, worldDelta, startLocalPos) {
|
|
|
792
790
|
bone.updateMatrixWorld(true);
|
|
793
791
|
}
|
|
794
792
|
|
|
795
|
-
/* --- state ----------------------------------------------------------------- */
|
|
796
|
-
|
|
797
793
|
/** Mutable per-rig IK layer state. `targets` are the live handle positions
|
|
798
794
|
* (world space) the handles render at and the solver reaches for. `keys`
|
|
799
795
|
* maps frame → Map(trackId → {q, p}) — FK LOCAL rotations of the solved
|
|
@@ -959,8 +955,6 @@ export function ikRemoveKeyframe(ikState, frame) {
|
|
|
959
955
|
ikState.keys.delete(frame);
|
|
960
956
|
}
|
|
961
957
|
|
|
962
|
-
/* --- foot planting (ground snap) -------------------------------------------- */
|
|
963
|
-
|
|
964
958
|
/** The two leg chains that get planted. */
|
|
965
959
|
const LEG_IDS = ["leftFoot", "rightFoot"];
|
|
966
960
|
|
|
@@ -1200,20 +1194,28 @@ function trackIslands(keys, trackId, blendWindow) {
|
|
|
1200
1194
|
const frames = [];
|
|
1201
1195
|
for (const [f, entry] of keys) {
|
|
1202
1196
|
const key = entry.get(trackId);
|
|
1203
|
-
if (key) frames.push([f, key.blend ?? blendWindow]);
|
|
1197
|
+
if (key) frames.push([f, key.blend ?? blendWindow, key.correctionRange ?? null]);
|
|
1204
1198
|
}
|
|
1205
1199
|
if (!frames.length) return [];
|
|
1206
1200
|
frames.sort((a, b) => a[0] - b[0]);
|
|
1201
|
+
// Paired boundary keys keep the selected interval connected even when an
|
|
1202
|
+
// older key lies inside it. Use their current frame positions so retiming
|
|
1203
|
+
// the timeline keeps the interval attached to its keys.
|
|
1204
|
+
const rangeEnd = new Map();
|
|
1205
|
+
const rangeId = range => range ? `${range.start}:${range.end}` : null;
|
|
1206
|
+
for (const [f, , range] of frames) if (range) rangeEnd.set(rangeId(range), f);
|
|
1207
1207
|
const islands = [];
|
|
1208
|
-
let [first, firstBlend] = frames[0];
|
|
1208
|
+
let [first, firstBlend, firstRange] = frames[0];
|
|
1209
|
+
let connectedUntil = rangeEnd.get(rangeId(firstRange)) ?? first;
|
|
1209
1210
|
let [prev, prevBlend] = frames[0];
|
|
1210
1211
|
for (let index = 1; index < frames.length; index += 1) {
|
|
1211
|
-
const [f, blend] = frames[index];
|
|
1212
|
-
if (f - prev > Math.max(prevBlend, blend)) {
|
|
1212
|
+
const [f, blend, range] = frames[index];
|
|
1213
|
+
if (f > connectedUntil && f - prev > Math.max(prevBlend, blend)) {
|
|
1213
1214
|
islands.push([first, prev, firstBlend, prevBlend]);
|
|
1214
1215
|
first = f;
|
|
1215
1216
|
firstBlend = blend;
|
|
1216
1217
|
}
|
|
1218
|
+
connectedUntil = Math.max(connectedUntil, rangeEnd.get(rangeId(range)) ?? f);
|
|
1217
1219
|
prev = f;
|
|
1218
1220
|
prevBlend = blend;
|
|
1219
1221
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// transform recorded by the scene (uniform scale, Y rotation and translation).
|
|
4
4
|
// It deliberately leaves local joint rotations untouched.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const MOTION_CALIBRATION_LIMITS = Object.freeze({
|
|
7
7
|
minScale: 0.1,
|
|
8
8
|
maxScale: 10,
|
|
9
9
|
maxOffset: 100,
|
package/src/ardy/npz.js
CHANGED
|
@@ -54,8 +54,6 @@ export class NpzError extends Error {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/* --- CRC-32 (IEEE 802.3, reflected) -------------------------------------- */
|
|
58
|
-
|
|
59
57
|
const CRC_TABLE = (() => {
|
|
60
58
|
const table = new Uint32Array(256);
|
|
61
59
|
for (let n = 0; n < 256; n += 1) {
|
|
@@ -72,8 +70,6 @@ function crc32(bytes) {
|
|
|
72
70
|
return (c ^ 0xffffffff) >>> 0;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
/* --- zip container -------------------------------------------------------- */
|
|
76
|
-
|
|
77
73
|
/** Locate the End of Central Directory record, scanning back from EOF. */
|
|
78
74
|
function findEocd(view) {
|
|
79
75
|
// EOCD is 22 bytes + a comment of up to 65535; the scan window also covers
|
|
@@ -219,8 +215,6 @@ async function inflateRaw(bytes, cap) {
|
|
|
219
215
|
return out;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
/* --- npy member ----------------------------------------------------------- */
|
|
223
|
-
|
|
224
218
|
const DTYPE_RE = /^([<>=|])([A-Za-z?])([0-9]{1,4})$/;
|
|
225
219
|
|
|
226
220
|
/**
|
|
@@ -403,8 +397,6 @@ function checkRotationMatrix(rows, frame, joint) {
|
|
|
403
397
|
}
|
|
404
398
|
}
|
|
405
399
|
|
|
406
|
-
/* --- archive decode ------------------------------------------------------- */
|
|
407
|
-
|
|
408
400
|
/**
|
|
409
401
|
* Decode an ARDY motion npz from raw bytes.
|
|
410
402
|
*
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { SUPPORT_SITES } from "./physics-review.js";
|
|
3
3
|
import { useMotionCommands } from '../domains/motion.js';
|
|
4
4
|
import "./physics-panel.css";
|
|
5
|
+
import { isImeComposing } from "../ime.js";
|
|
5
6
|
|
|
6
7
|
// Only the progress panel subscribes. Publishing a percentage must not
|
|
7
8
|
// rerender the full timeline/Studio hundreds of times during a solve.
|
|
@@ -31,7 +32,7 @@ export function PhysicsPanel({ ko, disabled, running, progress: progressStore, p
|
|
|
31
32
|
const onKeyDown = (event) => {
|
|
32
33
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
33
34
|
if (event.key === "Escape") { event.preventDefault(); onCancel?.(); }
|
|
34
|
-
if (event.key === "Enter") { event.preventDefault(); onApply?.(); }
|
|
35
|
+
if (event.key === "Enter" && !isImeComposing(event)) { event.preventDefault(); onApply?.(); }
|
|
35
36
|
};
|
|
36
37
|
window.addEventListener("keydown", onKeyDown);
|
|
37
38
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
@@ -31,16 +31,17 @@ export function copyPhysicsKeys(keys) {
|
|
|
31
31
|
...(k.baseQ ? { baseQ: k.baseQ.map((q) => q.clone()) } : {}),
|
|
32
32
|
...(k.basePos ? { basePos: k.basePos.clone() } : {}),
|
|
33
33
|
...(k.chainP ? { chainP: k.chainP.map((p) => p.clone()) } : {}),
|
|
34
|
+
...(k.correctionRange ? { correctionRange: { ...k.correctionRange } } : {}),
|
|
34
35
|
}]))]));
|
|
35
36
|
}
|
|
36
37
|
export function physicsKeyStamp(keys) {
|
|
37
|
-
return JSON.stringify([...keys].sort((a, b) => a[0] - b[0]).map(([f, e]) => [f, [...e].sort(([a], [b]) => a.localeCompare(b)).map(([id, k]) => [id, k.p?.toArray(), k.q?.map((q) => q.toArray()), k.basePos?.toArray(), k.baseQ?.map((q) => q.toArray()), k.keepTranslations, k.chainP?.map((p) => p.toArray())])]));
|
|
38
|
+
return JSON.stringify([...keys].sort((a, b) => a[0] - b[0]).map(([f, e]) => [f, [...e].sort(([a], [b]) => a.localeCompare(b)).map(([id, k]) => [id, k.p?.toArray(), k.q?.map((q) => q.toArray()), k.basePos?.toArray(), k.baseQ?.map((q) => q.toArray()), k.keepTranslations, k.chainP?.map((p) => p.toArray()), k.correctionRange])]));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Measured skin, shared exact transforms; never a cached bind-pose sole. */
|
|
41
42
|
export function createSupportSampler(rig) { return createSurfaceSampler(rig, SUPPORT_SITES); }
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
function sitePoint(row, site) {
|
|
44
45
|
return site.kind === "foot" ? (row.toes?.[site.id] ?? row.support[site.id]?.position) : row.support[site.id]?.position;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -5,7 +5,7 @@ import { SEGMENT_MASSES } from "./auto-physics.js";
|
|
|
5
5
|
const G = 9.81;
|
|
6
6
|
const V = () => new THREE.Vector3();
|
|
7
7
|
const smooth = (t) => { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); };
|
|
8
|
-
|
|
8
|
+
const SUPPORT_LIMITS = Object.freeze({ gap: .025, force: .22, moment: .10, maxDrop: .60, friction: .6 });
|
|
9
9
|
|
|
10
10
|
/** Cached bone lookup and an explicit approximate segmented physical body.
|
|
11
11
|
* Mass is normalized: reported forces are body weights, not measured Newtons.
|
|
@@ -3,7 +3,7 @@ import { findBone } from "./ik.js";
|
|
|
3
3
|
|
|
4
4
|
// Extra measured patches are not IK handles. They let a seated/lying body
|
|
5
5
|
// support itself without pretending its feet are planted.
|
|
6
|
-
|
|
6
|
+
const BODY_SUPPORT_SITES = [
|
|
7
7
|
["pelvis", "Hips", /Hips$/], ["abdomen", "Spine", /Spine$/],
|
|
8
8
|
["chest", "Spine1", /Spine[12]$/], ["head", "Head", /Head/],
|
|
9
9
|
["neck", "Neck", /Neck$/], ["leftShoulder", "LeftShoulder", /LeftShoulder$/], ["rightShoulder", "RightShoulder", /RightShoulder$/],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useSyncExternalStore } from "react";
|
|
2
2
|
import "../panels/pose.css";
|
|
3
|
+
import { isImeComposing } from "../ime.js";
|
|
3
4
|
|
|
4
5
|
const FOOT_NAMES = { leftFoot: ["Left foot", "왼발"], rightFoot: ["Right foot", "오른발"] };
|
|
5
6
|
function stepLabel(step, ko) {
|
|
@@ -18,7 +19,7 @@ export function PlatformFitPanel({ ko, disabled, running, progress: progressStor
|
|
|
18
19
|
const onKeyDown = (event) => {
|
|
19
20
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
20
21
|
if (event.key === "Escape") { event.preventDefault(); onCancel?.(); }
|
|
21
|
-
if (event.key === "Enter") { event.preventDefault(); onApply?.(); }
|
|
22
|
+
if (event.key === "Enter" && !isImeComposing(event)) { event.preventDefault(); onApply?.(); }
|
|
22
23
|
};
|
|
23
24
|
window.addEventListener("keydown", onKeyDown);
|
|
24
25
|
return () => window.removeEventListener("keydown", onKeyDown);
|