cozyclay 2.0.2 → 2.1.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.
Files changed (256) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +17 -2
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/bin/agent/agent-routes.mjs +211 -36
  5. package/bin/agent/agent-runner.mjs +98 -4
  6. package/bin/agent/codex-client.mjs +2 -1
  7. package/bin/agent/fal-contract.mjs +0 -17
  8. package/bin/agent/h3-preservation.mjs +6 -8
  9. package/bin/agent/provider-keys.mjs +1 -1
  10. package/bin/agent/providers.mjs +124 -3
  11. package/bin/agent/script-tool.mjs +74 -0
  12. package/bin/agent/session-store.mjs +5 -5
  13. package/bin/agent/studio-prompt.mjs +2 -1
  14. package/bin/agent/studio-rebase.mjs +314 -0
  15. package/bin/agent/studio-tools.mjs +21 -14
  16. package/bin/agent/supervisor.mjs +311 -0
  17. package/bin/agent/video-adapters.mjs +0 -7
  18. package/bin/codex-auth.mjs +4 -4
  19. package/bin/live/cli.mjs +2 -3
  20. package/bin/live/client.mjs +1 -1
  21. package/bin/live/studio.mjs +5 -2
  22. package/bin/mcp-runtime.mjs +2 -2
  23. package/bin/telemetry-state.mjs +3 -3
  24. package/dist/app/index.html +6 -6
  25. package/dist/assets/analytics-B41FmPYI.js +1 -0
  26. package/dist/assets/app-Do-Og5Pu.css +1 -0
  27. package/dist/assets/app-DwP-tl2x.js +4920 -0
  28. package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
  29. package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
  30. package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
  31. package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
  32. package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
  33. package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
  34. package/dist/assets/workflow-Br-K7Srs.js +179 -0
  35. package/dist/cozyclay-package.json +1 -1
  36. package/dist/index.html +328 -222
  37. package/dist/sitemap.xml +1 -1
  38. package/dist/workflow/index.html +4 -4
  39. package/mcp/LIVE-PROTOCOL.md +23 -2
  40. package/mcp/README.md +37 -2
  41. package/mcp/ardy-prompts.mjs +1 -1
  42. package/mcp/image-file.mjs +132 -0
  43. package/mcp/live-hub.mjs +2 -2
  44. package/mcp/package.json +1 -0
  45. package/mcp/runtime/package-lock.json +7 -0
  46. package/mcp/runtime/package.json +1 -0
  47. package/mcp/server.mjs +0 -6
  48. package/mcp/tool-handlers.mjs +289 -87
  49. package/package.json +11 -2
  50. package/src/App.jsx +789 -263
  51. package/src/agent-image-client.js +94 -0
  52. package/src/analytics.js +47 -3
  53. package/src/app-stage.jsx +129 -2745
  54. package/src/ardy/auto-physics.js +6 -16
  55. package/src/ardy/collision-blockers.js +3 -7
  56. package/src/ardy/fix-collisions.js +5 -35
  57. package/src/ardy/ik-drag.js +86 -5
  58. package/src/ardy/ik-key-json.js +3 -1
  59. package/src/ardy/ik.js +15 -13
  60. package/src/ardy/motion-calibration.js +1 -1
  61. package/src/ardy/npz.js +0 -8
  62. package/src/ardy/physics-panel.jsx +3 -2
  63. package/src/ardy/physics-review.js +3 -2
  64. package/src/ardy/physics-support.js +1 -1
  65. package/src/ardy/physics-surface.js +1 -1
  66. package/src/ardy/platform-fit-panel.jsx +2 -1
  67. package/src/ardy/playback.js +1 -5
  68. package/src/ardy/range-pin.js +2 -2
  69. package/src/ardy/root-drop.js +0 -1
  70. package/src/ardy/timeline.css +184 -6
  71. package/src/ardy/timeline.jsx +113 -28
  72. package/src/ardy/to-cskel27.js +0 -2
  73. package/src/ardy/waypoints.js +0 -3
  74. package/src/ardy-bridge-contract.js +281 -0
  75. package/src/asset-pane.jsx +19 -9
  76. package/src/auto-color.js +1 -1
  77. package/src/burn-in.js +1 -1
  78. package/src/camera-block.js +1 -1
  79. package/src/camera-follow.js +75 -32
  80. package/src/camera-move.js +0 -6
  81. package/src/camera-tutorial.jsx +6 -7
  82. package/src/character-kind.js +79 -0
  83. package/src/command-bus.js +76 -17
  84. package/src/commands/cast.js +31 -7
  85. package/src/commands/elements/object.js +1 -1
  86. package/src/commands/elements/shot.js +1 -1
  87. package/src/commands/elements.js +1 -1
  88. package/src/commands/export.js +3 -3
  89. package/src/commands/motion.js +2 -2
  90. package/src/commands/objects.js +105 -10
  91. package/src/commands/project.js +15 -2
  92. package/src/commands/sculpt-images.js +53 -0
  93. package/src/commands/shot.js +88 -10
  94. package/src/commands/view.js +10 -4
  95. package/src/coplanar-depth.js +216 -0
  96. package/src/cuts.js +7 -6
  97. package/src/domains/cast.js +60 -43
  98. package/src/domains/motion/motion-intent-codec.js +50 -0
  99. package/src/domains/motion/motion-range-pin-rules.js +23 -0
  100. package/src/domains/motion/motion-video-capture.js +664 -0
  101. package/src/domains/motion.js +253 -742
  102. package/src/domains/objects.js +106 -5
  103. package/src/domains/scenes.js +79 -28
  104. package/src/domains/shots.js +33 -7
  105. package/src/error-boundary.jsx +1 -1
  106. package/src/execution-telemetry.js +5 -5
  107. package/src/facing-marks.js +14 -1
  108. package/src/first-success-guide.jsx +35 -4
  109. package/src/gizmo-claim.js +3 -1
  110. package/src/grid-floor.jsx +1 -4
  111. package/src/hierarchy-model.js +48 -35
  112. package/src/hierarchy-panel.css +1 -0
  113. package/src/hierarchy-panel.jsx +54 -102
  114. package/src/history.js +0 -2
  115. package/src/i2v-motion-client.js +1 -4
  116. package/src/ime.js +6 -0
  117. package/src/keyframe-pack.js +15 -6
  118. package/src/line-edit.js +4 -4
  119. package/src/live-control.js +6 -6
  120. package/src/matte-editor.js +2 -8
  121. package/src/matte.js +0 -19
  122. package/src/mesh-graph-clone.js +16 -2
  123. package/src/mesh-sniff.js +0 -1
  124. package/src/monotone-cubic.js +40 -0
  125. package/src/motion-store.js +4 -4
  126. package/src/motion-trail.js +5 -11
  127. package/src/multimodel-ingest.js +1 -3
  128. package/src/object-catalog.jsx +7 -5
  129. package/src/object-gizmo.jsx +45 -31
  130. package/src/object-path.js +310 -67
  131. package/src/object-travel.js +224 -0
  132. package/src/otio.js +8 -3
  133. package/src/panels/CharacterTransformPanel.jsx +24 -6
  134. package/src/panels/ObjectTransformPanel.jsx +4 -3
  135. package/src/panels/PoseImagePanel.jsx +205 -0
  136. package/src/panels/PromptBlocksPanel.jsx +3 -1
  137. package/src/panels/ProxyFigurePanel.jsx +56 -0
  138. package/src/panels/ReferenceImageField.jsx +1 -1
  139. package/src/panels/RigControlPanel.jsx +24 -3
  140. package/src/panels/RigPanel.jsx +2 -1
  141. package/src/panels/SculptPanel.jsx +405 -0
  142. package/src/panels/SubjectBox.jsx +1 -1
  143. package/src/panels/SubjectsPanel.jsx +0 -2
  144. package/src/panels/VideoCapturePanel.jsx +2 -1
  145. package/src/panels/pose-image.css +42 -0
  146. package/src/panels/pose.css +3 -0
  147. package/src/panels/sculpt.css +162 -0
  148. package/src/plan-diagram.js +184 -0
  149. package/src/planview.jsx +127 -80
  150. package/src/playground.js +3 -5
  151. package/src/pose-fade.js +77 -0
  152. package/src/pose-gizmo.css +23 -0
  153. package/src/pose-gizmo.js +96 -0
  154. package/src/posestudio.jsx +134 -83
  155. package/src/project-browser.css +53 -0
  156. package/src/project-browser.jsx +49 -4
  157. package/src/project.js +55 -24
  158. package/src/props.jsx +177 -36
  159. package/src/proxy-figure.jsx +69 -0
  160. package/src/range-pin-panel.jsx +0 -1
  161. package/src/receipt-findings.js +20 -0
  162. package/src/render-passes.js +0 -1
  163. package/src/resource-status.jsx +1 -1
  164. package/src/root-path.js +48 -0
  165. package/src/route-owner.js +52 -0
  166. package/src/scene-asset-cache.js +1 -2
  167. package/src/scene-assets.js +12 -4
  168. package/src/scene-fbx.js +1 -1
  169. package/src/scene-mesh-cache.js +0 -1
  170. package/src/scene-objects.js +319 -19
  171. package/src/scenes.js +58 -9
  172. package/src/sculpt-blueprint.js +65 -0
  173. package/src/sculpt-compare.js +384 -0
  174. package/src/sculpt-geometry.js +141 -0
  175. package/src/sculpt-mode.js +168 -0
  176. package/src/sculpt-recipe.js +385 -0
  177. package/src/sculpt-run.js +351 -0
  178. package/src/sculpt-stage.jsx +221 -0
  179. package/src/settings-menu.jsx +1 -1
  180. package/src/shell/BoardSlot.jsx +341 -0
  181. package/src/shell/BottomDock.jsx +58 -14
  182. package/src/shell/DetailsSlot.jsx +26 -6
  183. package/src/shell/MenuBar.jsx +78 -6
  184. package/src/shell/OutlinerSlot.jsx +3 -1
  185. package/src/shell/TopBar.jsx +17 -3
  186. package/src/shell/ViewportToolbar.jsx +18 -3
  187. package/src/shell/dock.css +239 -0
  188. package/src/shell/glass-regions.css +3 -2
  189. package/src/shell/log-store.js +4 -4
  190. package/src/shell/mode.css +20 -0
  191. package/src/shell/studio-shell-context.js +4 -1
  192. package/src/shot-authoring.js +69 -6
  193. package/src/stable-items.js +1 -1
  194. package/src/stage-attach-frames.js +169 -0
  195. package/src/stage-canvas-rigs.jsx +402 -0
  196. package/src/stage-capture.jsx +216 -0
  197. package/src/stage-characters.jsx +302 -0
  198. package/src/stage-editor-gizmos.jsx +1229 -0
  199. package/src/stage-labels-ko.js +204 -0
  200. package/src/stage-startup-defaults.js +182 -0
  201. package/src/starter-scenes.js +1 -1
  202. package/src/storyboard.js +24 -5
  203. package/src/studio-actions.js +42 -9
  204. package/src/studio-agent-commands.js +186 -23
  205. package/src/studio-agent-context.js +11 -9
  206. package/src/studio-agent-motion.js +2 -1
  207. package/src/studio-agent-protocol.js +56 -23
  208. package/src/studio-app-binding.js +52 -23
  209. package/src/studio-contact-sheet.js +3 -3
  210. package/src/studio-elements.js +11 -2
  211. package/src/studio-geometry-facts.js +114 -0
  212. package/src/styles.css +92 -9
  213. package/src/ui-scrub.js +4 -4
  214. package/src/ui.jsx +6 -4
  215. package/src/use-asset-shelf.js +60 -0
  216. package/src/use-camera-tutorial.js +50 -0
  217. package/src/use-embed-bridge.js +12 -0
  218. package/src/use-first-success-prompt.js +7 -0
  219. package/src/use-i2v-capture-modal.js +15 -0
  220. package/src/use-sculpt-mode.js +95 -0
  221. package/src/workflow/AgentPanel.jsx +106 -18
  222. package/src/workflow/WorkflowBuilder.jsx +1 -1
  223. package/src/workflow/agent-client.js +113 -45
  224. package/src/workflow/agent-panel.css +35 -0
  225. package/src/workflow/attachment-image.js +1 -1
  226. package/src/workflow/canvas-commands.js +3 -3
  227. package/src/workflow/cozy-scene-node.js +3 -3
  228. package/src/workflow/local-workflow.js +2 -2
  229. package/src/workflow/motion-input.js +3 -3
  230. package/src/workflow/node-schema.js +1 -1
  231. package/src/workflow/scene-asset-sync.js +2 -9
  232. package/src/workflow/shot-prompt-node.js +1 -1
  233. package/src/workflow/video-contract.js +1 -1
  234. package/src/workflow/workflow-resources.js +0 -8
  235. package/src/workflow-send.js +87 -0
  236. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  237. package/tools/check-eslint.mjs +57 -0
  238. package/tools/check-knip.mjs +60 -0
  239. package/tools/check-loc.mjs +54 -0
  240. package/tools/demo-worker/api-client.mjs +7 -13
  241. package/tools/demo-worker/generate.mjs +2 -4
  242. package/tools/eslint-baseline.json +23 -0
  243. package/tools/kimodo/effector-constraints.mjs +4 -4
  244. package/tools/kimodo/generate.mjs +1 -1
  245. package/tools/knip-baseline.json +331 -0
  246. package/tools/loc-baseline.json +126 -0
  247. package/tools/process-supervisor.mjs +1 -1
  248. package/tools/projflow/replay.mjs +4 -4
  249. package/tools/projflow/runner.mjs +3 -3
  250. package/tools/qa/studio-control-count.mjs +2 -0
  251. package/tools/run-tests.mjs +94 -7
  252. package/dist/assets/analytics-MVZOEW8O.js +0 -1
  253. package/dist/assets/app-BxBLC6GT.js +0 -4876
  254. package/dist/assets/app-CJdxvnrl.css +0 -1
  255. package/dist/assets/shot-prompt-hN0j_O-p.js +0 -17
  256. package/dist/assets/workflow-B5XZwVfe.js +0 -179
@@ -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
- export const GROUND_CONTACT_VERTICAL_SPEED = 0.6;
218
+ const GROUND_CONTACT_VERTICAL_SPEED = 0.6;
225
219
  export const GROUND_CONTACT_HORIZONTAL_SPEED = 0.12;
226
- export const GROUND_CONTACT_MIN_SECONDS = 0.10;
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
- export const GROUND_FLOOR_MAX_LIFT = 0.15;
231
- export const GROUND_FLOOR_EPSILON = 0.005;
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
- export function fitBallisticArc(samples, fps, g = GRAVITY) {
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
- export const MAX_FIT_RESIDUAL = 0.25;
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
- export const OBJECT_BLOCKER_PREFIX = "obj:";
38
- export const CHARACTER_BLOCKER_PREFIX = "char:";
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 ?? [], { library, frame, take, skipIds, characters: characterIds }),
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
- export const CONTINUITY_BIAS = 0.6;
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
- export const CONTINUITY_BIAS_STRONG = 1.6;
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
- export const STEP_CAP_FACTOR = 2.5;
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
- export const STEP_CAP_ABS = 0.25;
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
- export const STEP_CAP_FLOOR = 0.005;
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
@@ -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
- export function captureRawBase(rig, chains, fkJoints, ids, applyRaw) {
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) baseQuats.set(id, chain.bones.map((bone) => bone.quaternion.clone()));
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
+ }
@@ -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
- export function ikTrackKeyJson(key) {
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
- /* --- bind-pose geometry ------------------------------------------------------
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
- /* --- the REST pose -----------------------------------------------------------
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
- export function measureContactHeights(rig) {
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
- export const MOTION_CALIBRATION_LIMITS = Object.freeze({
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 React, { useEffect, useState, useSyncExternalStore } from "react";
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
- export function sitePoint(row, site) {
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
- export const SUPPORT_LIMITS = Object.freeze({ gap: .025, force: .22, moment: .10, maxDrop: .60, friction: .6 });
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
- export const BODY_SUPPORT_SITES = [
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);