lecodes-assets 0.1.2 → 0.2.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.
Files changed (65) hide show
  1. package/README.md +240 -5
  2. package/dist/cli.js +7757 -683
  3. package/dist/index.js +7866 -675
  4. package/package.json +13 -3
  5. package/src/bvh/fromDocument.ts +136 -0
  6. package/src/bvh/toDocument.ts +202 -0
  7. package/src/cli.ts +1039 -0
  8. package/src/doctor/doctor.ts +241 -0
  9. package/src/doctor/lightmapUv.ts +452 -0
  10. package/src/doctor/lod.ts +177 -0
  11. package/src/doctor/normalizeScale.ts +220 -0
  12. package/src/doctor/reduceGlbBones.ts +218 -0
  13. package/src/doctor/shading.ts +95 -0
  14. package/src/doctor/transcodeKtx2.ts +124 -0
  15. package/src/doctor/transcodeTextures.ts +101 -0
  16. package/src/doctor/validateGlb.ts +264 -0
  17. package/src/fbx/convert.ts +327 -0
  18. package/src/fbx/read.ts +109 -0
  19. package/src/fbx/toDocument.ts +209 -0
  20. package/src/filmstrip.ts +263 -0
  21. package/src/index.ts +17 -0
  22. package/src/mergeSkins.ts +117 -0
  23. package/src/probe.ts +167 -0
  24. package/src/props.ts +137 -0
  25. package/src/retarget/automap.ts +276 -0
  26. package/src/retarget/ghost.ts +23 -0
  27. package/src/retarget/heading.ts +171 -0
  28. package/src/retarget/humanoid.ts +561 -0
  29. package/src/retarget/inPlace.ts +130 -0
  30. package/src/retarget/marks.ts +581 -0
  31. package/src/retarget/mirror.ts +0 -0
  32. package/src/retarget/retarget.ts +2272 -0
  33. package/src/retarget/rig.ts +542 -0
  34. package/src/retarget/unityAnim.ts +171 -0
  35. package/src/retarget/writeMarks.ts +132 -0
  36. package/src/sky/analyze.ts +136 -0
  37. package/src/sky/cmgen.ts +71 -0
  38. package/src/sky/index.ts +151 -0
  39. package/src/terrainPack.ts +169 -0
  40. package/src/textures/resolveImage.ts +202 -0
  41. package/src/unity/materials.ts +593 -0
  42. package/src/unity/png.ts +75 -0
  43. package/src/unity/prefabs.ts +80 -0
  44. package/src/unity/scene.ts +363 -0
  45. package/src/unity/unpack.ts +130 -0
  46. package/src/unreal/anim.ts +284 -0
  47. package/src/unreal/clipSource.ts +25 -0
  48. package/src/unreal/convert.ts +67 -0
  49. package/src/unreal/convertAsset.ts +81 -0
  50. package/src/unreal/index.ts +114 -0
  51. package/src/unreal/list.ts +113 -0
  52. package/src/unreal/materials.ts +291 -0
  53. package/src/unreal/mesh.ts +205 -0
  54. package/src/unreal/oodle.ts +153 -0
  55. package/src/unreal/package.ts +659 -0
  56. package/src/unreal/probe.ts +144 -0
  57. package/src/unreal/resolve.ts +51 -0
  58. package/src/unreal/rig.ts +149 -0
  59. package/src/unreal/sequencer.ts +193 -0
  60. package/src/unreal/skeleton.ts +74 -0
  61. package/src/unreal/texture.ts +202 -0
  62. package/src/unreal/toDocument.ts +360 -0
  63. package/src/unreal/versions.ts +100 -0
  64. package/src/wasm/fbxReader.js +14 -0
  65. package/src/wasm/lightmapUv.js +14 -0
@@ -0,0 +1,2272 @@
1
+ // Cross-rig animation retargeting: take clips authored on one skeleton and drive a different one.
2
+ //
3
+ // The `--clips` merge in `convert` binds tracks by BONE NAME and copies them verbatim. That only
4
+ // works when both files came from the same rig (the Mixamo case). Between two unrelated rigs it is
5
+ // hopeless — the bone names differ, the chains have different lengths, and, worst of all, the local
6
+ // axis conventions differ (this repo's fox runs its bones down local +Y, a Maya/AdvancedSkeleton
7
+ // wolf runs them down local +X), so the same local rotation means two different things.
8
+ //
9
+ // So work in WORLD space instead, where a rotation means the same thing in both rigs:
10
+ //
11
+ // ΔR(t) = Rsrc_world(t) · Rsrc_bind_world⁻¹ how far the donor bone turned from ITS bind
12
+ // Rdst_world(t) = ΔR(t) · Rdst_ref_world apply that turn to the target's REFERENCE
13
+ // Rdst_local(t) = Rdst_parent_world(t)⁻¹ · Rdst_world(t)
14
+ //
15
+ // Because the target's reference pose is the anchor, nothing about the target's proportions, chain
16
+ // lengths or axis conventions has to match the donor's — only the two reference poses have to
17
+ // describe the same thing (both animals standing, facing the same way, same up axis).
18
+ // `analyzeFacing` checks that precondition instead of leaving it to be discovered as a mangled result.
19
+ //
20
+ // The transfer itself is exact: with matching references, every mapped bone's world DIRECTION on
21
+ // the target equals the donor's at every frame. Everything below is about the four things that
22
+ // formula alone gets wrong on real assets, in the order they matter:
23
+ //
24
+ // 1. THE TWO REFERENCE POSES. Deltas are measured from the donor's reference and applied to the
25
+ // target's; both default to the BIND pose (inverse of the skin's inverse-bind matrices — never
26
+ // the TRS on the nodes, which an exporter routinely leaves at some junk frame), but two bind
27
+ // poses rarely describe the same posture, and the whole retarget hinges on them doing so.
28
+ // - Donor side (`donorReference`): a bind is often a posture the clips never visit — the
29
+ // polyperfect wolf binds upright on vertical thighs and walks 10% lower with its thighs 30°
30
+ // forward. Measured from such a bind every clip reads "crouch and lean", and a target keeping
31
+ // its own stance inherits the crouch. So when the two binds put the LEGS differently (>12°,
32
+ // i.e. different conventions), the donor is referenced to the MEAN pose of its idle/walk — its
33
+ // actual neutral posture — and the deltas are pure motion. When the binds agree, bind-to-bind
34
+ // is the faithful transfer (an identity retarget reproduces the donor exactly).
35
+ // - Target side (`alignReference`), per mapped bone: `keep` its own bind orientation, or
36
+ // `direction` — turn it so the bone POINTS the way the donor's does, measured to the nearest
37
+ // mapped descendant on each side (an auto-T-pose step per bone). Core bones — the root and
38
+ // every branch point (pelvis, chest, head) — keep: artists put "Hips" and "chest" wherever the
39
+ // weights want them, so the pelvis→chest joint line is +3° on one wolf and −12° on one fox
40
+ // while both stand level, and forcing them equal pitched the whole fox nose-up. Legs keep when
41
+ // the target binds in a STANCE (feet level, legs hanging) — that is its natural posture and the
42
+ // donor's deltas ride on it — and align when it binds posed (T-pose, folded). Everything else
43
+ // (tail, neck, ears, spine members) aligns. `align` in the map overrides per bone.
44
+ // Straightening a crouched rig lifts it off the ground, so the root is put back down
45
+ // (`groundOffset`) and every foot is planted in the reference pose.
46
+ //
47
+ // 2. PROPORTIONS (`placeLeg`). Directions transfer, positions do not: when the target's front and
48
+ // hind legs have a different length ratio than the donor's, the feet float or sink by a few
49
+ // percent of body height — exactly what the eye catches first. Leg chains are detected from the
50
+ // map (`detectLegs`), each foot gets a goal built as THE TARGET'S OWN STANCE plus the donor's
51
+ // departure from ITS stance, scaled by that leg's length ratio, on all three axes — height above
52
+ // each rig's own stance level, and the horizontal offset each rig plants its foot at under its
53
+ // own hip — and a FABRIK pass from the FK pose bends the chain onto it. Using the donor's
54
+ // absolute foot-under-hip offset instead overwrites the target's build with the donor's: this
55
+ // fox tucks each foot ~8% of body height inboard while every polyperfect animal stands on
56
+ // vertical legs, and copying that planted the fox bow-legged in every clip. Because the goal IS
57
+ // the FK result whenever the two legs are scaled copies, IK only ever spends the proportion
58
+ // residual. Root motion is scaled by the mean leg ratio (a stride is leg length, not body
59
+ // height), so stance feet that were still on the donor stay still on the target.
60
+ //
61
+ // 3. CHAIN LENGTH (`findBridges`). A donor chain LONGER than the target's collapses for free (the
62
+ // target's `chest` mapped to `Chest_M` picks up Spine1+Spine2, because a world rotation already
63
+ // contains everything above it). A target chain longer than the donor's — unmapped bones between
64
+ // two mapped ones — interpolates the world delta along the chain instead of bending at one joint.
65
+ //
66
+ // 4. SPACES. A donor facing another way is turned first (`donorYawDeg`, from `analyzeFacing`); a
67
+ // skin whose bind space is scaled against node space is rescaled in `rig.ts` (`bind.scale`).
68
+ //
69
+ // 5. HUMANOIDS (`opts.humanoid`, the two rigs' avatars from humanoid.ts). The same transfer, with
70
+ // the reference policy decided by canonical ROLE instead of by tree shape: torso, head, legs and
71
+ // feet keep their bind (a standing bind is build — ankle height, spine joint placement — not
72
+ // posture), arms and fingers align to the donor's bind directions with explicit references
73
+ // (hand → the finger bases, not "the heaviest subtree") and a THRESHOLD: a mismatch under 8° is
74
+ // build noise and stays, above it is posture (an A-pose against a T-pose, a bent elbow) and turns.
75
+ // Direction fixes two of a bone's three degrees of freedom; the third, the ROLL about the arm, is
76
+ // what decides where the palm faces, and no direction can see it. It is read off the hand's
77
+ // KNUCKLE LINE (index base → little base): after alignment the whole forearm chain — forearm,
78
+ // hand, fingers — is turned about the forearm axis until the target's knuckle line lies where the
79
+ // donor's does. A Mixamo T-pose (palms down) against an A-pose rig whose palms face back is a
80
+ // ~90° roll that would otherwise ride every clip as a twisted wrist. An identity retarget is
81
+ // still exact: equal directions and equal knuckle lines leave every correction at zero.
82
+ // The donor is referenced to its bind (its clips are authored on it); a target root bone that
83
+ // shares the donor's hips gets the hips' TRAVEL only, never their rotation, so helpers parented
84
+ // to the root stay put.
85
+ //
86
+ // 6. PROPORTIONS, SECOND PASS (`opts.rootMotion`, `opts.plant`, `ik: 'legs+hands'`, `opts.twist`).
87
+ // - Where the travel goes: a rig with a root ABOVE the hips gets the donor's horizontal travel on
88
+ // the root and the vertical bob on the hips (`'root'`, the default when such a root exists — a
89
+ // character controller reads the root's travel and must not bob with it); `'hips'` puts all of
90
+ // it on the hips as the donor had it; `'none'` strips the travel for an in-place clip (feet
91
+ // still planted in the travelling frame, so they slide back under the body like a treadmill).
92
+ // A donor whose hips bob RELATIVE to their own root (MoCap Central: `root` travels, `pelvis`
93
+ // bobs) has that bob re-applied on the target's hips — the one donor translation besides the
94
+ // root's that carries motion.
95
+ // - Feet as CONTACT + ORIENTATION (humanoid): the leg's effector is the ankle, but its goal is
96
+ // derived from the BALL of the foot (the canonical `Toes` bone) — the ball goes where the
97
+ // donor's ball goes (scaled, stance-corrected), the foot itself keeps the donor's rotation on
98
+ // the target's own flat bind, and the ankle follows from the two. Rigs disagree about where the
99
+ // ankle sits over the ball (a boot's foot bone dives 56° to the ball, X-bot's 27°, both soles
100
+ // flat): transferring the ankle's position AND the foot's rotation lifts such a boot 10 cm off
101
+ // the ground at toe-off, while driving the ball with the foot as an IK link (the old way) lets
102
+ // the solver pick the foot's pitch — 50° off the donor in the swing. Planting holds the ball.
103
+ // - Foot planting: the donor's foot MARKS (marks.ts) drive two constraints on the target's foot.
104
+ // Through a STILL window the ball is LOCKED to one world point (the mean of where the FK put
105
+ // it, at the height the foot's clearance rule below gives) and the foot keeps one rotation,
106
+ // fading in and out over a few frames inside the touch. On every frame of a flat-ground clip
107
+ // (every TOUCH frame of a stair / platform clip) the foot's CLEARANCE — the lowest of its heel
108
+ // (virtual: the ankle's bind height hung under the ankle), its ball and its toe (real, or a
109
+ // virtual one off the ball's rotation) over its level — is matched to the donor's, scaled by
110
+ // the leg ratio; the whole foot shifts vertically, x/z and rotation stay the FK's. So a heel
111
+ // strike lands on the ground, a toe-off lifts with the donor's, a hang dangles, and a foot the
112
+ // transfer left 7 cm up in a lunge comes down. The BODY follows: while a foot touches, the
113
+ // hips drop until the leg reaches its anchor at the donor's extension (up to 10 cm; a swing
114
+ // foot asks nothing — its knee bends), so the knees never straighten past the donor's.
115
+ // - Platform takes (`stairs` → 'platform'): a HANG — both hands still and overhead, no foot on
116
+ // the floor — names the top when the feet never stand on it (the grip's height + what a hand
117
+ // holds under a lip), and every such take gets a `platform` block beside the contacts
118
+ // (marks.ts `fitPlatform`, fitted again on the OUTPUT): the top's height and the ledge's EDGE
119
+ // — a point on it relative to the root at frame 0 and the face's outward normal — from the
120
+ // plants on the top, the hands resting on it, the wrists gripping under it and the body's
121
+ // envelope, so a runtime can stand the character against a real ledge at clip start.
122
+ // - Hands as effectors (`'legs+hands'`): the same ratio-scaled goal as a foot, measured from the
123
+ // upper arm, chain capped at the upper + lower arm, for clips where the hands meet or hold.
124
+ // - Twist helpers (the avatar's `twist` map): a helper at fraction `t` along its limb bone takes
125
+ // `f·(t·twistOut − (1 − t)·twistIn)` about the bone's axis on top of riding it — near the joint
126
+ // it counter-rotates the bone's own axial turn (the shoulder skin stays with the clavicle), near
127
+ // the far end it follows the child's (the wrist skin follows the hand's pronation). `f` = 0.5
128
+ // like Unity's arm/leg twist defaults.
129
+ //
130
+ // Bone lengths always come from the TARGET — donor translation tracks are ignored except the root's
131
+ // (and the hips' bob relative to it, see 6).
132
+ import type { Document } from '@gltf-transform/core'
133
+ import { qmul, qinv, qnorm, qslerp, qrot, worldOf, bindVsRest, rotateRig, type Quat, type Vec3, type Rig, type DocRig, type Pose, type Clip } from './rig'
134
+ import type { Avatar, HumanBone } from './humanoid'
135
+ import { describeMarks, detectMarks, fitPlatform, fitStaircase, type FootTrack, type LevelKind, type MarkSpan, type MarksInput, type PlatformMark, type Stairs } from './marks'
136
+ import { headingCurve } from './heading'
137
+
138
+ export interface BoneMap { [targetBone: string]: string } // target bone name -> donor bone name
139
+
140
+ export type Align = 'none' | 'bone-direction'
141
+ /** Per-bone reference policy: keep the bind orientation, align the bone direction to the donor's
142
+ * (measured to the heuristic descendant), or align it measured to these named descendants. */
143
+ export type AlignPolicy = 'keep' | 'direction' | string[]
144
+
145
+ export interface RetargetOptions {
146
+ align?: Align // global switch (default 'bone-direction'; 'none' = keep everything)
147
+ alignBones?: Record<string, AlignPolicy> // per-bone policy, overriding the core/chain default
148
+ alignTo?: Record<string, string[]> // legacy spelling of `alignBones` with reference lists
149
+ ik?: 'none' | 'legs' | 'legs+hands' // foot placement (default 'legs'); '+hands' = the hands as effectors too (humanoid)
150
+ rootMotion?: RootMotion // where the donor's travel lands (default 'auto': root when the rig has one above the hips, else hips)
151
+ headingEnd?: number // the baked heading curve must end here (degrees, + = left): a stop that turns (heading.ts `endAt`)
152
+ plant?: boolean // foot planting (default true): each foot LOCKED through its still windows (marks.ts)
153
+ /** The horizontal stance a planted foot keeps: 'target' (default) = the target rig's own foot-under-hip
154
+ * offset plus only the donor's DEPARTURE from its bind (a quadruped's build — header §2); 'donor' = the
155
+ * donor's foot-under-hip offset transfers whole, scaled. A humanoid whose A-pose bind stands legs apart
156
+ * loses that width on every clip otherwise (MC_RunFast: 25 → 14 cm of step width, 2026-09-04). The
157
+ * height stays stance-relative either way. */
158
+ footStance?: 'target' | 'donor'
159
+ stairs?: (clip: string) => LevelKind // which donor clips carry LEVELS: a staircase, a platform (two levels), or none
160
+ twist?: number | false // twist-helper fraction (default 0.5; false = helpers ride their bone rigidly)
161
+ donorYawDeg?: number // turn the donor about +Y by this much first (facing mismatch fix)
162
+ donorRef?: DonorRef // pose the donor's deltas are measured from (default 'auto': mean of its idle/walk)
163
+ legs?: string[] // target effector bones (feet) — overrides auto-detection
164
+ fps?: number // sampling rate of the produced clips (default 30, the FBX bake rate)
165
+ rootScale?: number // override the automatic root-motion scale
166
+ /** `'yaw'`: a target root bone that takes the donor hips' TRAVEL also takes their HEADING — the
167
+ * hips' turn about the up axis since the clip's first frame — so a turn-start / turn-in-place /
168
+ * arc clip carries its turn on the root, where the engine's root rotation reads it (the hips keep
169
+ * everything else). Default `'none'`: the root never rotates (a walk cycle's hip sway stays a
170
+ * pose). No effect on a donor with its own root bone (the root follows it as any bone). */
171
+ rootRotation?: 'none' | 'yaw'
172
+ clips?: string[] // donor clip names to take (default: all)
173
+ rename?: (name: string) => string
174
+ epsilon?: number // key-reduction tolerance for translations, metres (default 1e-4; 0 disables)
175
+ epsilonDeg?: number // key-reduction tolerance for rotations, degrees (default 0.2; 0 disables)
176
+ epsilonLegDeg?: number // …for the hips and the leg chains (default 0.01: a planted toe stays within a millimetre)
177
+ /** BODY ON THE GROUND: a joint the donor has on its floor (within 5 cm) is brought to the same height
178
+ * over the target's ground PLUS this clearance (metres, default 0.05) — a roll on the back, a
179
+ * knock-down; the clearance is the skin the joint has under it */
180
+ bodyClearance?: number
181
+ humanoid?: { target: Avatar; donor: Avatar } // both rigs' avatars → role-based references + palm roll (see header §5)
182
+ warn?: (message: string) => void
183
+ }
184
+
185
+ export type RootMotion = 'auto' | 'hips' | 'root' | 'none'
186
+
187
+ export interface LegReport {
188
+ effector: string; donorEffector: string; chain: string[]
189
+ ratio: number // target leg length / donor leg length
190
+ shortfall: number // worst distance (m) a goal was out of reach, over all clips
191
+ isFoot: boolean // stands on the ground (else a hand)
192
+ contact: string // the bone whose position is matched to the donor's (the ball of a humanoid foot; else the effector)
193
+ planted: number // frames the goal was locked to a planted position, over all clips
194
+ }
195
+
196
+ /** Something worth a marker on a timeline: a span of a clip, on one bone. */
197
+ export interface RetargetEvent {
198
+ kind: 'plant' | 'reach' | 'below' // foot planted · goal out of reach (value = m) · toe under the ground (value = m)
199
+ bone: string
200
+ from: number; to: number // seconds
201
+ value?: number
202
+ /** plant: where the contact bone stood when the span began — x/z relative to the root's horizontal
203
+ * position that frame (i.e. under the character, valid for an in-place clip too), y absolute */
204
+ at?: Vec3
205
+ }
206
+
207
+ /** A foot on the ground: the touch span, the still window inside it (null: never still), where the
208
+ * contact bone stood at the strike, and the tread index on a stair clip. */
209
+ export interface ContactSpan { from: number; to: number; still: [ number, number ] | null; at: Vec3; step?: number }
210
+ /**
211
+ * The clip's foot contacts — what a runtime needs for phase matching, foot-aware transitions and
212
+ * foot IK: per side the spans a foot stands (`from`/`to` seconds, `at` = the contact bone's position
213
+ * at the strike, x/z relative to the root, y absolute), and which foot is down at the first frame.
214
+ * Written to the GLB animation's `extras.lecodes.contacts` as well.
215
+ */
216
+ export interface ClipContacts {
217
+ feet: { l: string | null; r: string | null } // the contact bones (a humanoid's balls)
218
+ l: ContactSpan[]
219
+ r: ContactSpan[]
220
+ first: 'l' | 'r' | 'both' | null // planted at t = 0
221
+ /** a stair clip's staircase, in the target's space (tread k at base + k · riser) */
222
+ stairs?: Stairs
223
+ /** the hands' contact bones and their ground contacts (a cartwheel's, a roll's, a vault's) */
224
+ hands?: { l: string | null; r: string | null }
225
+ hl?: ContactSpan[]
226
+ hr?: ContactSpan[]
227
+ }
228
+
229
+ export interface TwistReport { bone: string; of: string; at: number } // helper, its limb bone, fraction along it
230
+
231
+ export interface RetargetReport {
232
+ bindGap: { donor: { worst: number; bone: string; skinned: number }; target: { worst: number; bone: string; skinned: number } }
233
+ align: { bone: string; degrees: number; via: string }[]
234
+ kept: string[] // mapped bones whose reference is the bind orientation
235
+ bridges: string[] // unmapped target bones interpolated between mapped ones
236
+ groundOffset: number
237
+ ground: { target: number; donor: number } | null
238
+ legs: LegReport[]
239
+ clips: {
240
+ name: string; duration: number; frames: number; keys: number; events: RetargetEvent[]
241
+ turn: number // the root's net yaw over the clip, degrees (rootRotation 'yaw')
242
+ contacts: ClipContacts // the feet on the ground (see ClipContacts)
243
+ travel: [ number, number ] // the root's horizontal displacement over the clip, target metres (x, z) — before any in-place conversion
244
+ /** a platform take's ledge in the target's space (see PlatformMark) + the root's x/z at frame 0 */
245
+ platform?: PlatformMark & { root0: [ number, number ] }
246
+ }[]
247
+ mapped: { target: string; donor: string }[]
248
+ unmappedTarget: string[]
249
+ rootScale: number
250
+ rootScaleFrom: 'legs' | 'height' | 'option' | 'none'
251
+ donorRef: string // what the donor deltas are measured from
252
+ legDisagreement: number // mean angle (°) between the two binds' leg bone directions
253
+ stance: boolean // the target binds standing (legs kept) rather than posed (legs aligned)
254
+ root: { target: string; donor: string } | null
255
+ rootTranslateOnly: boolean // the root took the donor's travel but not its rotation (it shares the donor's hips)
256
+ roll: { side: string; degrees: number; via: string }[] // humanoid palm-roll corrections per arm (nudges from the avatar included)
257
+ underThreshold: { bone: string; degrees: number; via: string }[] // bones that would have aligned but sat under the threshold: the bind kept
258
+ rootMotion: Exclude<RootMotion, 'auto'> // where the travel went
259
+ rootRotation: 'none' | 'yaw' // whether the root carries the hips' heading (see RetargetOptions.rootRotation)
260
+ hipsBob: 'root' | 'donor' | null // the hips got a translation track: the travel's vertical part / the donor's own hips bob
261
+ twist: TwistReport[] // twist helpers driven (humanoid)
262
+ }
263
+
264
+ const DEG = 180 / Math.PI
265
+ const IDENT: Quat = [ 0, 0, 0, 1 ]
266
+
267
+ /** Which way a rig faces and how big it is, from its rest pose — the retarget precondition. */
268
+ export const analyzeFacing = (rig: Rig, boneNames: string[]): { forward: Vec3; up: Vec3; height: number; length: number } => {
269
+ const pick = (n: string): Vec3 | null => { const i = rig.index.get(n); return i === undefined ? null : rig.bind.worldPos[i] }
270
+ const head = pick(boneNames[0]), tail = pick(boneNames[1])
271
+ let lo = [ Infinity, Infinity, Infinity ], hi = [ -Infinity, -Infinity, -Infinity ]
272
+ for (const p of rig.bind.worldPos) for (let i = 0; i < 3; i++) { lo[i] = Math.min(lo[i], p[i]); hi[i] = Math.max(hi[i], p[i]) }
273
+ // Flattened to the ground plane: a head held higher than the tail is posture, not facing.
274
+ const fwd: Vec3 = head && tail ? [ head[0] - tail[0], 0, head[2] - tail[2] ] : [ 0, 0, 1 ]
275
+ const len = Math.hypot(...fwd) || 1
276
+ return {
277
+ forward: [ fwd[0] / len, fwd[1] / len, fwd[2] / len ],
278
+ up: [ 0, 1, 0 ],
279
+ height: hi[1] - lo[1],
280
+ length: Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]),
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Angle between the two rigs' forward directions, in degrees. Anything much above zero means the
286
+ * rest poses disagree about which way is front and every clip will come out rotated.
287
+ */
288
+ export const facingMismatch = (a: Vec3, b: Vec3): number =>
289
+ Math.acos(Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]))) * DEG
290
+
291
+ interface Channel { node: number; times: number[]; rot?: Quat[]; pos?: Vec3[] }
292
+
293
+ /** Drop keys a linear/slerp interpolation would have reproduced anyway. */
294
+ const reduce = (ch: Channel, eps: number, epsDeg: number): void => {
295
+ if ((eps <= 0 && epsDeg <= 0) || ch.times.length < 3) return
296
+ // A slerp guess within epsDeg of the real key: |dot| > cos(epsDeg/2).
297
+ const minDot = Math.cos((epsDeg / 2) * Math.PI / 180)
298
+ const keepT: number[] = [ ch.times[0] ]
299
+ const keepR: Quat[] | undefined = ch.rot ? [ ch.rot[0] ] : undefined
300
+ const keepP: Vec3[] | undefined = ch.pos ? [ ch.pos[0] ] : undefined
301
+ for (let i = 1; i < ch.times.length - 1; i++) {
302
+ const t0 = keepT[keepT.length - 1], t1 = ch.times[i + 1]
303
+ const u = (ch.times[i] - t0) / (t1 - t0 || 1)
304
+ let flat = true
305
+ if (ch.rot && keepR) {
306
+ const guess = qslerp(keepR[keepR.length - 1], ch.rot[i + 1], u), got = ch.rot[i]
307
+ const dot = Math.abs(guess[0] * got[0] + guess[1] * got[1] + guess[2] * got[2] + guess[3] * got[3])
308
+ if (epsDeg <= 0 ? dot < 1 - 1e-12 : dot < minDot) flat = false
309
+ }
310
+ if (ch.pos && keepP) {
311
+ const a = keepP[keepP.length - 1], b = ch.pos[i + 1], got = ch.pos[i]
312
+ for (let k = 0; k < 3; k++) if (Math.abs(a[k] + (b[k] - a[k]) * u - got[k]) > (eps <= 0 ? 1e-12 : eps)) flat = false
313
+ }
314
+ if (!flat) { keepT.push(ch.times[i]); keepR?.push(ch.rot![i]); keepP?.push(ch.pos![i]) }
315
+ }
316
+ keepT.push(ch.times[ch.times.length - 1])
317
+ keepR?.push(ch.rot![ch.rot!.length - 1])
318
+ keepP?.push(ch.pos![ch.pos!.length - 1])
319
+ ch.times = keepT
320
+ if (keepR) ch.rot = keepR
321
+ if (keepP) ch.pos = keepP
322
+ }
323
+
324
+ const sub = (a: Vec3, b: Vec3): Vec3 => [ a[0] - b[0], a[1] - b[1], a[2] - b[2] ]
325
+ const add = (a: Vec3, b: Vec3): Vec3 => [ a[0] + b[0], a[1] + b[1], a[2] + b[2] ]
326
+ const scl = (a: Vec3, k: number): Vec3 => [ a[0] * k, a[1] * k, a[2] * k ]
327
+ const len = (v: Vec3): number => Math.hypot(v[0], v[1], v[2])
328
+ const unit = (v: Vec3): Vec3 | null => { const l = len(v); return l > 1e-12 ? [ v[0] / l, v[1] / l, v[2] / l ] : null }
329
+
330
+ /** Shortest-arc rotation taking `from` onto `to` (both unit). */
331
+ const arc = (from: Vec3, to: Vec3): Quat => {
332
+ const d = from[0] * to[0] + from[1] * to[1] + from[2] * to[2]
333
+ if (d > 0.999999) return IDENT
334
+ if (d < -0.999999) {
335
+ // Opposite directions: any perpendicular axis is a valid 180° turn; take the most stable one.
336
+ const ax: Vec3 = Math.abs(from[0]) < 0.9 ? [ 1, 0, 0 ] : [ 0, 1, 0 ]
337
+ const c: Vec3 = [ from[1] * ax[2] - from[2] * ax[1], from[2] * ax[0] - from[0] * ax[2], from[0] * ax[1] - from[1] * ax[0] ]
338
+ const u = unit(c)!
339
+ return [ u[0], u[1], u[2], 0 ]
340
+ }
341
+ const c: Vec3 = [ from[1] * to[2] - from[2] * to[1], from[2] * to[0] - from[0] * to[2], from[0] * to[1] - from[1] * to[0] ]
342
+ return qnorm([ c[0], c[1], c[2], 1 + d ])
343
+ }
344
+
345
+ /** Children lists for a rig. */
346
+ const childrenOf = (rig: Rig): number[][] => {
347
+ const children: number[][] = rig.names.map(() => [])
348
+ rig.parents.forEach((p, i) => { if (p >= 0) children[p].push(i) })
349
+ return children
350
+ }
351
+
352
+ /**
353
+ * Reference orientation per target bone — see the header. `byTarget` maps target bone index → donor
354
+ * bone index for every mapped bone; `rootIdx` is the root-motion bone (always core).
355
+ */
356
+ const alignReference = (
357
+ target: DocRig, donor: Rig, byTarget: Map<number, number>, rootIdx: number, align: Align,
358
+ policies: Record<string, AlignPolicy>, minDeg = 0, force: Set<number> = new Set(),
359
+ ): { ref: Quat[]; A: Quat[]; log: { bone: string; degrees: number; via: string }[]; kept: string[]; under: { bone: string; degrees: number; via: string }[] } => {
360
+ const ref: Quat[] = target.bind.worldRot.map((q) => [ ...q ] as Quat)
361
+ const A: Quat[] = target.names.map(() => IDENT)
362
+ const log: { bone: string; degrees: number; via: string }[] = []
363
+ const under: { bone: string; degrees: number; via: string }[] = []
364
+ const kept: string[] = []
365
+ if (align === 'none') { for (const i of byTarget.keys()) kept.push(target.names[i]); return { ref, A, log, kept, under } }
366
+
367
+ const children = childrenOf(target)
368
+ const mappedChildCount = (i: number): number => {
369
+ // Mapped descendants reachable without crossing another mapped bone, per branch.
370
+ let n = 0
371
+ const queue = [ ...children[i] ]
372
+ while (queue.length) { const c = queue.shift()!; if (byTarget.has(c)) n++; else queue.push(...children[c]) }
373
+ return n
374
+ }
375
+
376
+ // Explicit policies from the map win over the core/chain default.
377
+ const explicit = new Map<number, AlignPolicy>()
378
+ for (const [ bone, pol ] of Object.entries(policies)) {
379
+ const i = target.index.get(bone)
380
+ if (i !== undefined && byTarget.has(i)) explicit.set(i, pol)
381
+ }
382
+
383
+ const subtreeSize = new Array(target.names.length).fill(1)
384
+ for (let k = target.order.length - 1; k >= 0; k--) {
385
+ const i = target.order[k]
386
+ if (target.parents[i] >= 0) subtreeSize[target.parents[i]] += subtreeSize[i]
387
+ }
388
+
389
+ /**
390
+ * Mapped descendants that define this bone's direction. At a branch point the heaviest subtree
391
+ * wins — that is the spine, not the tail — and a symmetric tie (a head between two ears) averages,
392
+ * which is the only stable answer when neither side is "the" continuation.
393
+ */
394
+ const directionRefs = (i: number): number[] => {
395
+ const found: number[] = []
396
+ const queue = [ ...children[i] ]
397
+ while (queue.length) {
398
+ const c = queue.shift()!
399
+ if (byTarget.has(c)) { found.push(c); continue } // stop at the first mapped bone per branch
400
+ queue.push(...children[c])
401
+ }
402
+ if (!found.length) return []
403
+ const best = Math.max(...found.map((c) => subtreeSize[c]))
404
+ const heaviest = found.filter((c) => subtreeSize[c] === best)
405
+ if (heaviest.length < 2) return heaviest
406
+ // Tie: the spine continues along the CENTRELINE, so a chest whose head and front legs all weigh
407
+ // the same must follow the head. A symmetric pair (two ears off a head) stays tied and averages.
408
+ const lateral = (c: number): number => Math.abs(target.bind.worldPos[c][0] - target.bind.worldPos[i][0])
409
+ const nearest = Math.min(...heaviest.map(lateral))
410
+ const tol = Math.max(nearest * 1.05, 1e-6)
411
+ return heaviest.filter((c) => lateral(c) <= tol)
412
+ }
413
+
414
+ const meanDir = (from: Vec3, tos: Vec3[]): Vec3 | null => {
415
+ const acc: Vec3 = [ 0, 0, 0 ]
416
+ let n = 0
417
+ for (const to of tos) {
418
+ const u = unit(sub(to, from))
419
+ if (!u) continue
420
+ acc[0] += u[0]; acc[1] += u[1]; acc[2] += u[2]; n++
421
+ }
422
+ return n ? unit(acc) : null
423
+ }
424
+
425
+ for (const i of target.order) {
426
+ const s = byTarget.get(i)
427
+ if (s === undefined) continue
428
+ // Default policy: the root and every branch point are core → keep; chain members → direction.
429
+ let pol: AlignPolicy = explicit.get(i) ?? (i === rootIdx || mappedChildCount(i) >= 2 ? 'keep' : 'direction')
430
+ if (pol === 'keep') { kept.push(target.names[i]); continue }
431
+
432
+ let refs: number[]
433
+ if (Array.isArray(pol)) {
434
+ refs = pol.map((r) => target.index.get(r)).filter((x): x is number => x !== undefined && byTarget.has(x))
435
+ if (!refs.length) refs = directionRefs(i)
436
+ } else refs = directionRefs(i)
437
+
438
+ let ut: Vec3 | null = null, us: Vec3 | null = null, via = ''
439
+ if (refs.length) {
440
+ ut = meanDir(target.bind.worldPos[i], refs.map((c) => target.bind.worldPos[c]))
441
+ us = meanDir(donor.bind.worldPos[s], refs.map((c) => donor.bind.worldPos[byTarget.get(c)!]))
442
+ via = refs.map((c) => target.names[c]).join("+")
443
+ } else if (children[i].length) {
444
+ // Leaf on the map but not in the rig: fall back to any child on each side.
445
+ const donorKids = donor.parents.map((p, k) => [ p, k ] as const).filter(([ p ]) => p === s).map(([ , k ]) => k)
446
+ if (donorKids.length) {
447
+ ut = unit(sub(target.bind.worldPos[children[i][0]], target.bind.worldPos[i]))
448
+ us = unit(sub(donor.bind.worldPos[donorKids[0]], donor.bind.worldPos[s]))
449
+ via = `${target.names[children[i][0]]} (unmapped child)`
450
+ }
451
+ }
452
+ let a: Quat
453
+ if (ut && us) {
454
+ a = arc(ut, us)
455
+ // Under the threshold the mismatch is build (joint placement), not posture: keep the bind —
456
+ // unless a person said this bone IS posed (`force`).
457
+ const off = 2 * Math.acos(Math.min(1, Math.abs(a[3]))) * DEG
458
+ if (minDeg > 0 && off < minDeg && !force.has(i)) { if (off > 0.5) under.push({ bone: target.names[i], degrees: off, via }); a = IDENT; kept.push(target.names[i]); A[i] = a; continue }
459
+ } else {
460
+ // True leaf (a tail tip, an ear end): inherit whatever its parent was corrected by.
461
+ a = A[target.parents[i]] ?? IDENT
462
+ via = 'inherited from parent'
463
+ }
464
+ A[i] = a
465
+ ref[i] = qnorm(qmul(a, target.bind.worldRot[i]))
466
+ const deg = 2 * Math.acos(Math.min(1, Math.abs(a[3]))) * DEG
467
+ if (deg > 0.5) log.push({ bone: target.names[i], degrees: deg, via })
468
+ }
469
+ log.sort((a, b) => b.degrees - a.degrees)
470
+ under.sort((a, b) => b.degrees - a.degrees)
471
+ return { ref, A, log, kept, under }
472
+ }
473
+
474
+ /** A target bone sandwiched between mapped bones: its delta interpolates between theirs. */
475
+ interface Bridge { above: number; below: number; u: number }
476
+
477
+ const findBridges = (target: Rig, byTarget: Map<number, number>): Map<number, Bridge> => {
478
+ const children = childrenOf(target)
479
+ const subtreeSize = new Array(target.names.length).fill(1)
480
+ for (let k = target.order.length - 1; k >= 0; k--) {
481
+ const i = target.order[k]
482
+ if (target.parents[i] >= 0) subtreeSize[target.parents[i]] += subtreeSize[i]
483
+ }
484
+ // Nearest mapped descendant per bone — heaviest branch wins, like the direction heuristic.
485
+ const below = new Map<number, { idx: number; depth: number }>()
486
+ for (let k = target.order.length - 1; k >= 0; k--) {
487
+ const i = target.order[k]
488
+ let best: { idx: number; depth: number; size: number } | null = null
489
+ for (const c of children[i]) {
490
+ const cand = byTarget.has(c) ? { idx: c, depth: 1, size: subtreeSize[c] } : (() => { const b = below.get(c); return b ? { idx: b.idx, depth: b.depth + 1, size: subtreeSize[b.idx] } : null })()
491
+ if (cand && (!best || cand.size > best.size)) best = cand
492
+ }
493
+ if (best) below.set(i, { idx: best.idx, depth: best.depth })
494
+ }
495
+ const out = new Map<number, Bridge>()
496
+ for (const i of target.order) {
497
+ if (byTarget.has(i)) continue
498
+ const b = below.get(i)
499
+ if (!b) continue
500
+ // Nearest mapped ancestor, counting the steps.
501
+ let a = target.parents[i], up = 1
502
+ while (a >= 0 && !byTarget.has(a)) { a = target.parents[a]; up++ }
503
+ if (a < 0) continue
504
+ out.set(i, { above: a, below: b.idx, u: up / (up + b.depth) })
505
+ }
506
+ return out
507
+ }
508
+
509
+ /** A leg: the chain from the hip (child of a core bone) down to the bone that stands on the ground. */
510
+ interface Leg {
511
+ chain: number[] // target bones hip → … → parent of effector (at most 4)
512
+ effector: number // target foot bone whose position is driven
513
+ donorHip: number; donorEffector: number
514
+ contact: number // the bone whose position is matched to the donor's — the ball of a humanoid foot (a child of the effector), else the effector
515
+ donorContact: number
516
+ contactRatio: number // target effector→contact length / the donor's (1 when the contact is the effector)
517
+ lengths: number[] // target segment lengths along chain + effector (bind)
518
+ pole: Vec3 | null // which way the chain bends in the bind pose (knee off the hip→foot line), world
519
+ ratio: number // target leg length / donor leg length
520
+ name: string
521
+ isFoot: boolean // the donor's effector stands on the donor's ground (else: a wrist-style end)
522
+ lockOnly?: boolean // a hand that is an effector only while LOCKED to a ground contact (else FK)
523
+ baseTarget: number // effector height in the grounded reference pose (its stance height)
524
+ baseDonor: number // lowest the donor's effector gets over the clips taken (its stance height)
525
+ // Horizontal foot-under-hip offset in each rig's OWN reference pose. Rigs disagree about where a
526
+ // foot belongs under its hip — this fox tucks its feet 8% of body height inboard, every polyperfect
527
+ // animal stands on vertical legs — and that is build, not motion, so it must not transfer.
528
+ stanceTarget: Vec3
529
+ stanceDonor: Vec3 // the donor's, already scaled segment by segment (`donorReach`)
530
+ refReach: number // hip→effector distance in the target's grounded reference pose (its stance extension)
531
+ }
532
+
533
+ /** How far off the hip→foot line the middle joints must sit (a fraction of the chain's length) before
534
+ * the FK pose's own bend direction is trusted over the bind pose's — ~12° of knee for two equal segments. */
535
+ const BEND_TRUST = 0.05
536
+ /** The most a foot pitches about its ankle to lift its toes back onto the floor (rad). */
537
+ const FOOT_PITCH_CAP = 20 * Math.PI / 180
538
+ const cross = (a: Vec3, b: Vec3): Vec3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
539
+
540
+ /**
541
+ * Leg chains from the map. A leg is a lateral chain of ≥3 mapped bones hanging off a core bone that
542
+ * reaches down into the lower half of the body (tails hang low too, but on the midline; ears and
543
+ * raised arms stay up). Its effector is the first bone from the hip that sits at the chain's lowest
544
+ * level — the ball of the foot, with any toes beyond it left to FK — or, on a chain that never
545
+ * reaches the ground on the donor (a horse whose front leg ends at the wrist), the chain's end.
546
+ * The chain above the effector is capped at four bones (scapula, thigh, shin, metatarsus) — anything
547
+ * further up stays FK and keeps the donor's motion instead of soaking up IK corrections. IK only
548
+ * ever spends the proportion residual, so how many joints share it is not visible.
549
+ */
550
+ const detectLegs = (
551
+ target: Rig, donor: Rig, byTarget: Map<number, number>, refPos: Vec3[], explicit: string[] | undefined, warn: (m: string) => void, maxAbove = 4,
552
+ ): Leg[] => {
553
+ const children = childrenOf(target)
554
+ const mappedKids = (i: number): number[] => {
555
+ const out: number[] = []
556
+ const queue = [ ...children[i] ]
557
+ while (queue.length) { const c = queue.shift()!; if (byTarget.has(c)) out.push(c); else queue.push(...children[c]) }
558
+ return out
559
+ }
560
+ const mappedParent = (i: number): number => { let p = target.parents[i]; while (p >= 0 && !byTarget.has(p)) p = target.parents[p]; return p }
561
+ /** Mapped bones from the first bone below a branch point down to `leaf`. */
562
+ const chainTo = (leaf: number): number[] => {
563
+ const chain = [ leaf ]
564
+ let b = mappedParent(leaf)
565
+ while (b >= 0 && mappedKids(b).length === 1) { chain.unshift(b); b = mappedParent(b) }
566
+ return chain
567
+ }
568
+
569
+ let lo = [ Infinity, Infinity, Infinity ], hi = [ -Infinity, -Infinity, -Infinity ]
570
+ for (const p of refPos) for (let k = 0; k < 3; k++) { lo[k] = Math.min(lo[k], p[k]); hi[k] = Math.max(hi[k], p[k]) }
571
+ const size = Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) || 1
572
+ const height = hi[1] - lo[1] || size
573
+
574
+ let candidates: { full: number[]; effector?: number }[]
575
+ if (explicit?.length) {
576
+ candidates = []
577
+ for (const n of explicit) {
578
+ const i = target.index.get(n)
579
+ if (i === undefined || !byTarget.has(i)) { warn(`legs: "${n}" is not a mapped target bone — skipped`); continue }
580
+ const full = chainTo(i)
581
+ if (full.length < 3) { warn(`legs: "${n}" has no chain of ≥2 mapped bones above it — skipped`); continue }
582
+ candidates.push({ full, effector: i })
583
+ }
584
+ } else {
585
+ const leaves = [ ...byTarget.keys() ].filter((i) => mappedKids(i).length === 0)
586
+ candidates = leaves
587
+ .filter((i) => Math.abs(refPos[i][0] - (lo[0] + hi[0]) / 2) > 0.02 * size)
588
+ .map((i) => ({ full: chainTo(i) }))
589
+ .filter((c) => c.full.length >= 3 && Math.min(...c.full.map((i) => refPos[i][1])) - lo[1] < 0.45 * height)
590
+ }
591
+
592
+ const legs: Leg[] = []
593
+ const seen = new Set<number>()
594
+ for (const cand of candidates) {
595
+ const full = cand.full
596
+ // Effector: the first bone at the chain's lowest level (ball of the foot), toes beyond it stay FK.
597
+ const lowest = Math.min(...full.map((i) => refPos[i][1]))
598
+ const e = cand.effector ?? full.find((i) => refPos[i][1] - lowest < 0.04 * height)!
599
+ if (seen.has(e)) continue
600
+ seen.add(e)
601
+ const k = full.indexOf(e)
602
+ if (k < 2) continue // needs at least two bones above it to bend
603
+ // The IK chain is the ACTUAL skeleton path from the mapped hip down to the effector — unmapped
604
+ // (bridged) bones in between are real joints with real lengths and bend with the rest — capped
605
+ // at four bones above the effector.
606
+ const hipMapped = full[0]
607
+ const path: number[] = []
608
+ for (let b = target.parents[e]; b >= 0 && b !== target.parents[hipMapped]; b = target.parents[b]) path.unshift(b)
609
+ const chain = path.slice(Math.max(0, path.length - maxAbove))
610
+ const legFull = [ ...chain, e ]
611
+ const lengths: number[] = []
612
+ for (let j = 1; j < legFull.length; j++) lengths.push(len(sub(target.bind.worldPos[legFull[j]], target.bind.worldPos[legFull[j - 1]])))
613
+ // Donor counterpart of the chain root: the nearest mapped bone at or above it; donor length is
614
+ // measured along the donor's own skeleton path between the two.
615
+ let hipRef = chain[0]
616
+ while (hipRef >= 0 && !byTarget.has(hipRef)) hipRef = target.parents[hipRef]
617
+ const donorHip = byTarget.get(hipRef)!, donorEffector = byTarget.get(e)!
618
+ let dlen = 0
619
+ for (let b = donorEffector; b >= 0 && b !== donorHip; b = donor.parents[b]) {
620
+ const p = donor.parents[b]
621
+ if (p < 0) { dlen = 0; break }
622
+ dlen += len(sub(donor.bind.worldPos[b], donor.bind.worldPos[p]))
623
+ }
624
+ const dfull = [ donorHip, donorEffector ]
625
+ const tlen = lengths.reduce((a, b) => a + b, 0)
626
+ if (!(tlen > 0) || !(dlen > 0)) continue
627
+ // Bend direction of the bind pose: the middle joints' offset from the hip→foot line. A straight
628
+ // chain has none; the solver then falls back to "forward".
629
+ let pole: Vec3 | null = null
630
+ {
631
+ const hip = target.bind.worldPos[legFull[0]], foot = target.bind.worldPos[legFull[legFull.length - 1]]
632
+ const axis = unit(sub(foot, hip))
633
+ if (axis) {
634
+ const acc: Vec3 = [ 0, 0, 0 ]
635
+ for (let j = 1; j < legFull.length - 1; j++) {
636
+ const v = sub(target.bind.worldPos[legFull[j]], hip)
637
+ const d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]
638
+ acc[0] += v[0] - axis[0] * d; acc[1] += v[1] - axis[1] * d; acc[2] += v[2] - axis[2] * d
639
+ }
640
+ if (len(acc) > 1e-3 * tlen) pole = unit(acc)
641
+ }
642
+ }
643
+ legs.push({
644
+ chain, effector: e, donorHip: dfull[0], donorEffector: dfull[dfull.length - 1], contact: e, donorContact: dfull[dfull.length - 1], contactRatio: 1, lengths, pole,
645
+ ratio: tlen / dlen, name: target.names[e], isFoot: true, baseTarget: 0, baseDonor: 0,
646
+ stanceTarget: [ 0, 0, 0 ], stanceDonor: [ 0, 0, 0 ], refReach: 0,
647
+ })
648
+ }
649
+ return legs
650
+ }
651
+
652
+ /** Swing-twist split: the angle (rad, signed) `q` turns about the unit axis `a`. */
653
+ const twistAbout = (q: Quat, a: Vec3): number => {
654
+ const d = q[0] * a[0] + q[1] * a[1] + q[2] * a[2]
655
+ const ang = 2 * Math.atan2(d, q[3])
656
+ // Wrap to (−π, π]: the quaternion's double cover otherwise reads a small negative twist as ~2π.
657
+ return ang > Math.PI ? ang - 2 * Math.PI : ang < -Math.PI ? ang + 2 * Math.PI : ang
658
+ }
659
+ const axisAngle = (a: Vec3, ang: number): Quat => { const s = Math.sin(ang / 2); return [ a[0] * s, a[1] * s, a[2] * s, Math.cos(ang / 2) ] }
660
+
661
+ /** A twist helper: a child of a limb bone, sitting `t` of the way along it, driven about its axis. */
662
+ interface Twist { bone: number; parent: number; grand: number; child: number; t: number; axisLocal: Vec3; name: string; of: string }
663
+
664
+ /** Foot-planting state of one leg across a clip's frames. */
665
+ interface Plant { anchor: Vec3 | null; run: number; offset: Vec3; fade: number; since: number }
666
+
667
+ /**
668
+ * FABRIK on a chain of joint positions (`p[0]` fixed) with segment lengths `seg`, toward `goal`.
669
+ * Starts from the given positions, so the bend plane of the FK pose is what gets preserved.
670
+ */
671
+ const fabrik = (p: Vec3[], seg: number[], goal: Vec3, iterations = 48): void => {
672
+ const n = p.length
673
+ const root = p[0]
674
+ const total = seg.reduce((a, b) => a + b, 0)
675
+ const toGoal = sub(goal, root)
676
+ const dist = len(toGoal)
677
+ if (dist >= total * 0.9995) {
678
+ // Out of reach: straight line toward the goal (slightly short of full extension).
679
+ const d = unit(toGoal) ?? [ 0, -1, 0 ]
680
+ for (let i = 1; i < n; i++) p[i] = add(p[i - 1], scl(d, seg[i - 1] * 0.9995))
681
+ return
682
+ }
683
+ for (let it = 0; it < iterations; it++) {
684
+ // Backward: end effector onto the goal, pull the chain after it.
685
+ p[n - 1] = goal
686
+ for (let i = n - 2; i >= 0; i--) {
687
+ const d = unit(sub(p[i], p[i + 1])) ?? [ 0, 1, 0 ]
688
+ p[i] = add(p[i + 1], scl(d, seg[i]))
689
+ }
690
+ // Forward: root back home, push the chain after it.
691
+ p[0] = root
692
+ for (let i = 1; i < n; i++) {
693
+ const d = unit(sub(p[i], p[i - 1])) ?? [ 0, -1, 0 ]
694
+ p[i] = add(p[i - 1], scl(d, seg[i - 1]))
695
+ }
696
+ if (len(sub(p[n - 1], goal)) < total * 1e-6) break
697
+ }
698
+ }
699
+
700
+ /** Which donor pose the deltas are measured from. */
701
+ export type DonorRef = 'bind' | 'auto' | { clip: string; time?: number }
702
+
703
+ /**
704
+ * The donor rig with its `bind` replaced by the REFERENCE pose the deltas are measured against.
705
+ * The bind pose is the obvious anchor, but an exporter's bind is routinely a posture the clips
706
+ * never visit: the polyperfect wolf binds upright on vertical thighs and walks 10% lower with its
707
+ * thighs 30° forward — measured from the bind, every clip reads as "crouch and lean", and a target
708
+ * that keeps its own stance inherits that crouch. The MEAN pose of a locomotion clip (walk, else idle,
709
+ * else the longest) is the donor's actual neutral posture, so deltas from it are pure motion.
710
+ * Returns the rig to use and a label for the report.
711
+ */
712
+ const donorReference = (donor: Rig, ref: DonorRef, fps: number): { rig: Rig; from: string } => {
713
+ if (ref === 'bind' || !donor.clips.length) return { rig: donor, from: 'bind' }
714
+ let clip: Clip | undefined, at: number | undefined
715
+ if (typeof ref === 'object') {
716
+ clip = donor.clips.find((c) => c.name === ref.clip)
717
+ if (!clip) return { rig: donor, from: 'bind' }
718
+ at = ref.time
719
+ } else {
720
+ const pick = (re: RegExp): Clip | undefined => donor.clips.filter((c) => re.test(c.name) && c.duration > 0).sort((a, b) => b.duration - a.duration)[0]
721
+ // A walk cycle is periodic and symmetric, so its mean is the cleanest neutral stance; an idle may
722
+ // sit, graze or look around for seconds at a time.
723
+ clip = pick(/walk/i) ?? pick(/idle|breath|stand/i) ?? [ ...donor.clips ].sort((a, b) => b.duration - a.duration)[0]
724
+ }
725
+ const n = donor.names.length
726
+ const rot: Quat[] = Array.from({ length: n }, () => [ 0, 0, 0, 0 ])
727
+ const pos: Vec3[] = Array.from({ length: n }, () => [ 0, 0, 0 ])
728
+ const frames = Math.max(2, Math.round(clip.duration * fps) + 1)
729
+ const times = at !== undefined ? [ at ] : Array.from({ length: frames }, (_, f) => (f / (frames - 1)) * clip.duration)
730
+ for (const t of times) {
731
+ const w = worldOf(donor, clip.sample(t))
732
+ for (let i = 0; i < n; i++) {
733
+ const q = w.rot[i], acc = rot[i]
734
+ // Hemisphere-align on the running sum so the average does not cancel.
735
+ const s = acc[0] * q[0] + acc[1] * q[1] + acc[2] * q[2] + acc[3] * q[3] < 0 ? -1 : 1
736
+ acc[0] += s * q[0]; acc[1] += s * q[1]; acc[2] += s * q[2]; acc[3] += s * q[3]
737
+ pos[i][0] += w.pos[i][0]; pos[i][1] += w.pos[i][1]; pos[i][2] += w.pos[i][2]
738
+ }
739
+ }
740
+ const k = 1 / times.length
741
+ const bind = { ...donor.bind, worldRot: rot.map((q) => qnorm(q)), worldPos: pos.map((p) => [ p[0] * k, p[1] * k, p[2] * k ] as Vec3) }
742
+ return { rig: { ...donor, bind }, from: at !== undefined ? `"${clip.name}" @ ${at}s` : `mean of "${clip.name}"` }
743
+ }
744
+
745
+ /**
746
+ * Retarget `donor`'s clips onto `target`, adding glTF Animations to the target's Document.
747
+ * `map` is target-bone -> donor-bone; `rootBone` names the target bone that receives root motion.
748
+ */
749
+ export function retarget(target: DocRig, donorIn: Rig, map: BoneMap, rootBone: string | null, opts: RetargetOptions = {}): RetargetReport {
750
+ // A donor that faces another way is turned first, so "world" means the same thing on both sides.
751
+ const yaw = ((opts.donorYawDeg ?? 0) / DEG) / 2
752
+ const fps = opts.fps ?? 30
753
+ const turned = Math.abs(opts.donorYawDeg ?? 0) > 1e-6 ? rotateRig(donorIn, [ 0, Math.sin(yaw), 0, Math.cos(yaw) ]) : donorIn
754
+ // Key reduction is a size trade against the FEET: a 0.2° slerp tolerance per joint stacks to
755
+ // ~1 cm of toe motion at the end of a four-joint leg (a locked idle read 2–4 cm/s through it,
756
+ // 0.0 without, 0.8 at 0.05°). So the bones a foot hangs from — the hips and the leg chains —
757
+ // are reduced at `epsilonLegDeg` (0.01°: a planted toe stays within a millimetre); everything
758
+ // else (arms, fingers, spine) keeps the size-friendly tolerance.
759
+ const eps = opts.epsilon ?? 1e-4
760
+ const epsDeg = opts.epsilonDeg ?? 0.2
761
+ const epsLegDeg = opts.epsilonLegDeg ?? 0.01
762
+ const warn = opts.warn ?? (() => {})
763
+ const doc = target.doc
764
+
765
+ // Resolve the map to indices once, warning about names that do not exist on either side.
766
+ const pairs: { d: number; s: number; target: string; donor: string }[] = []
767
+ for (const [ tName, sName ] of Object.entries(map)) {
768
+ const d = target.index.get(tName), s = turned.index.get(sName)
769
+ if (d === undefined) { warn(`target has no bone "${tName}" — skipped`); continue }
770
+ if (s === undefined) { warn(`donor has no bone "${sName}" — target "${tName}" left at rest`); continue }
771
+ pairs.push({ d, s, target: tName, donor: sName })
772
+ }
773
+ const byTarget = new Map(pairs.map((p) => [ p.d, p.s ]))
774
+
775
+ const rootIdx = rootBone === null ? -1 : (target.index.get(rootBone) ?? -1)
776
+ if (rootBone !== null && rootIdx < 0) warn(`target has no root bone "${rootBone}" — no root motion`)
777
+ const donorRoot = rootIdx >= 0 ? byTarget.get(rootIdx) : undefined
778
+ // A root that shares its donor bone with another target bone (a `root` above the pelvis, both
779
+ // mapped to Mixamo's Hips) carries the travel only — the hips rotate, the root must not.
780
+ const rootTranslateOnly = rootIdx >= 0 && donorRoot !== undefined && pairs.some((p) => p.d !== rootIdx && p.s === donorRoot)
781
+
782
+ // Legs are found on the bind pose first: they decide how the donor is referenced and how their
783
+ // own bones are aligned (see below); their reference-pose geometry is filled in further down.
784
+ const legs = opts.ik === 'none' ? [] : detectLegs(target, turned, byTarget, target.bind.worldPos, opts.legs, warn)
785
+ const hum = opts.humanoid
786
+ const humIdx = (av: Avatar, k: HumanBone): number | undefined => { const n = av.bones[k]; return n === undefined ? undefined : target.index.get(n) }
787
+ const hipsIdx = hum ? humIdx(hum.target, 'Hips') ?? -1 : -1
788
+ // A humanoid foot's contact is its ball (header §6): the `Toes` bone right under the ankle, when
789
+ // both rigs have it mapped.
790
+ if (hum) for (const leg of legs) for (const s of [ 'Left', 'Right' ] as const) {
791
+ const foot = humIdx(hum.target, `${s}Foot`), toes = humIdx(hum.target, `${s}Toes`)
792
+ if (leg.effector === foot && toes !== undefined && byTarget.has(toes) && target.parents[toes] === foot) {
793
+ leg.contact = toes; leg.donorContact = byTarget.get(toes)!
794
+ const tf = len(sub(target.bind.worldPos[toes], target.bind.worldPos[foot])), df = len(sub(turned.bind.worldPos[leg.donorContact], turned.bind.worldPos[leg.donorEffector]))
795
+ leg.contactRatio = tf > 0 && df > 0 ? tf / df : 1
796
+ }
797
+ }
798
+ /** The donor's hip→contact vector scaled segment by segment: the leg by the leg ratio, the foot by its own. */
799
+ const donorReach = (leg: Leg, pos: Vec3[]): Vec3 => add(scl(sub(pos[leg.donorEffector], pos[leg.donorHip]), leg.ratio), scl(sub(pos[leg.donorContact], pos[leg.donorEffector]), leg.contactRatio))
800
+
801
+ // Which effectors are FEET is the donor's call: its effector stands on its ground over the clips
802
+ // taken (a horse's front leg ends at the wrist, which never does). Stance heights come with it.
803
+ const wanted = opts.clips?.length ? new Set(opts.clips) : null
804
+ const clips = turned.clips.filter((c) => !wanted || wanted.has(c.name))
805
+ let donorGround = 0
806
+ if (legs.length) {
807
+ for (const l of legs) l.baseDonor = Infinity
808
+ for (const clip of clips) {
809
+ const frames = Math.max(2, Math.round(clip.duration * fps) + 1)
810
+ for (let f = 0; f < frames; f++) {
811
+ const dw = worldOf(turned, clip.sample((f / (frames - 1)) * clip.duration))
812
+ for (const l of legs) l.baseDonor = Math.min(l.baseDonor, dw.pos[l.donorContact][1])
813
+ }
814
+ }
815
+ donorGround = Math.min(...legs.map((l) => l.baseDonor))
816
+ let dlo = Infinity, dhi = -Infinity
817
+ for (const p of turned.bind.worldPos) { dlo = Math.min(dlo, p[1]); dhi = Math.max(dhi, p[1]) }
818
+ for (const l of legs) {
819
+ // a humanoid's feet are feet by ROLE: on a stair take one foot may never come down to the
820
+ // clip's lowest ground (the up-loop's left foot lives a tread above it) — judged by reach
821
+ // it read as a hand, got no lock and no marks, and the staircase fit saw half the plants
822
+ const byRole = hum ? l.effector === humIdx(hum.target, 'LeftFoot') || l.effector === humIdx(hum.target, 'RightFoot') : false
823
+ l.isFoot = byRole || l.baseDonor - donorGround < 0.03 * (dhi - dlo || 1)
824
+ }
825
+ }
826
+
827
+ // Donor reference. If both binds put the legs the same way (same convention), bind-to-bind is the
828
+ // faithful transfer (an identity retarget reproduces the donor exactly); if they disagree by more
829
+ // than a few degrees, the conventions differ (an upright A-stance wolf, a naturally posed horse),
830
+ // and the donor's own standing posture — the mean of its idle/walk — is what the target's stance
831
+ // corresponds to.
832
+ let legDisagreement = 0
833
+ {
834
+ let n = 0
835
+ for (const leg of legs) for (const i of [ ...leg.chain, leg.effector ]) {
836
+ const kid = [ ...leg.chain, leg.effector ].find((c) => target.parents[c] === i || (() => { let p = target.parents[c]; while (p >= 0 && !byTarget.has(p)) p = target.parents[p]; return p === i })())
837
+ if (kid === undefined || !byTarget.has(kid) || !byTarget.has(i)) continue
838
+ const ut = unit(sub(target.bind.worldPos[kid], target.bind.worldPos[i]))
839
+ const us = unit(sub(turned.bind.worldPos[byTarget.get(kid)!], turned.bind.worldPos[byTarget.get(i)!]))
840
+ if (!ut || !us) continue
841
+ legDisagreement += Math.acos(Math.max(-1, Math.min(1, ut[0] * us[0] + ut[1] * us[1] + ut[2] * us[2]))) * DEG; n++
842
+ }
843
+ if (n) legDisagreement /= n
844
+ }
845
+ // A humanoid donor's clips are authored on its bind (Mixamo's T-pose, a pack's A-pose): that IS the reference.
846
+ const donorRefMode: DonorRef = opts.donorRef ?? (opts.humanoid ? 'bind' : legs.length && legDisagreement > 12 ? 'auto' : 'bind')
847
+ const { rig: donor, from: donorRefFrom } = donorReference(turned, donorRefMode, fps)
848
+ // Stance heights are relative to the reference pose too: a foot never lower than its reference.
849
+ // A humanoid's stance IS its bind (the T-pose stands on the ground): the foot's departure from
850
+ // that height is what transfers, so a foot planted on the donor lands on the target's own ground
851
+ // and an identity retarget stays exact (the clip's lowest toe — mid-roll — would put it 8 mm off).
852
+ for (const l of legs) l.baseDonor = opts.humanoid ? donor.bind.worldPos[l.donorContact][1] : Math.min(l.baseDonor, donor.bind.worldPos[l.donorContact][1])
853
+ if (legs.length) donorGround = Math.min(...legs.map((l) => l.baseDonor))
854
+ // …and so is the horizontal stance: where the donor plants its foot under its own hip.
855
+ for (const l of legs) l.stanceDonor = donorReach(l, donor.bind.worldPos)
856
+
857
+ // Alignment default. A target that binds in a STANCE — feet level, legs hanging down — keeps its
858
+ // bind everywhere: that is its natural posture (legs, neck, tail, ears included) and the donor's
859
+ // deltas ride on it. A target that binds posed (T-pose, legs folded) has no stance to keep: its
860
+ // core bones keep, its chains are turned to point the donor's way.
861
+ const policies: Record<string, AlignPolicy> = { ...(opts.alignTo ?? {}), ...(opts.alignBones ?? {}) }
862
+ // A person's word on the avatar (`transfer.align`): `keep` = built that way, `align` = posed —
863
+ // aligned to the donor whatever the threshold says. Bone-name policies above still win.
864
+ const forced = new Set<number>()
865
+ if (hum?.target.transfer?.align) for (const [ k, v ] of Object.entries(hum.target.transfer.align)) {
866
+ const n = hum.target.bones[k as HumanBone]
867
+ if (!n || n in policies) continue
868
+ const i = target.index.get(n)
869
+ if (i === undefined || !byTarget.has(i)) continue
870
+ if (v === 'keep') policies[n] = 'keep'
871
+ else forced.add(i)
872
+ }
873
+ // Humanoid: references by canonical ROLE (header §5). Torso, head and face keep; the arm chain and
874
+ // the fingers align to the donor with explicit references — each bone to the next canonical bone
875
+ // down its chain, the hand to the finger bases. Legs and feet fall to the stance rule below.
876
+ if (hum) {
877
+ const T = hum.target
878
+ const set = (k: HumanBone, pol: AlignPolicy): void => { const n = T.bones[k]; if (n !== undefined && !(n in policies) && target.index.has(n) && byTarget.has(target.index.get(n)!)) policies[n] = pol }
879
+ for (const k of [ 'Hips', 'Spine', 'Chest', 'UpperChest', 'Neck', 'Head', 'Jaw', 'LeftEye', 'RightEye' ] as HumanBone[]) set(k, 'keep')
880
+ const refTo = (k: HumanBone, ...tos: HumanBone[]): void => { const names = tos.map((t) => T.bones[t]).filter((n): n is string => !!n); set(k, names.length ? names : 'direction') }
881
+ for (const s of [ 'Left', 'Right' ] as const) {
882
+ refTo(`${s}Shoulder`, `${s}UpperArm`); refTo(`${s}UpperArm`, `${s}LowerArm`); refTo(`${s}LowerArm`, `${s}Hand`)
883
+ refTo(`${s}Hand`, `${s} Index Proximal`, `${s} Middle Proximal`, `${s} Ring Proximal`, `${s} Little Proximal`)
884
+ for (const f of [ 'Thumb', 'Index', 'Middle', 'Ring', 'Little' ] as const) {
885
+ refTo(`${s} ${f} Proximal`, `${s} ${f} Intermediate`); refTo(`${s} ${f} Intermediate`, `${s} ${f} Distal`); set(`${s} ${f} Distal`, 'direction')
886
+ }
887
+ }
888
+ }
889
+ let stance = legs.length > 0
890
+ if (legs.length) {
891
+ let lo = Infinity, hi = -Infinity
892
+ for (let i = 0; i < target.names.length; i++) if (target.bind.fromSkin[i]) { lo = Math.min(lo, target.bind.worldPos[i][1]); hi = Math.max(hi, target.bind.worldPos[i][1]) }
893
+ const height = hi - lo || 1
894
+ const footYs = legs.map((l) => target.bind.worldPos[l.contact][1])
895
+ const lowestFoot = Math.min(...footYs)
896
+ for (const leg of legs) {
897
+ const hipY = target.bind.worldPos[leg.chain[0]][1], footY = target.bind.worldPos[leg.contact][1]
898
+ const L = leg.lengths.reduce((a, b) => a + b, 0)
899
+ if ((hipY - footY) / L < 0.5) stance = false // not hanging down
900
+ if (leg.isFoot && footY - lowestFoot > 0.05 * height) stance = false // feet not level
901
+ }
902
+ }
903
+ if (stance) for (const i of byTarget.keys()) { const name = target.names[i]; if (!(name in policies)) policies[name] = 'keep' }
904
+ else for (const leg of legs) for (const i of [ ...leg.chain, leg.effector ]) { const name = target.names[i]; if (!(name in policies)) policies[name] = 'direction' }
905
+ // A forced bone that the rules made `keep` (a leg in a stance, a torso bone) aligns by direction.
906
+ for (const i of forced) if (policies[target.names[i]] === 'keep') policies[target.names[i]] = 'direction'
907
+ const { ref, A, log: alignLog, kept, under: alignUnder } = alignReference(target, donor, byTarget, rootIdx, opts.align ?? 'bone-direction', policies, hum ? 8 : 0, forced)
908
+
909
+ // Hands as effectors (header §6): the same goal machinery as a foot, from the upper arm down,
910
+ // chain capped at upper + lower arm (the clavicle stays FK). Added after the stance decision —
911
+ // an arm never hangs like a leg — and never a foot.
912
+ // …and, on a humanoid, ALWAYS as lock-only effectors: a hand that the marks find on the ground (a
913
+ // cartwheel, a roll, a vault) is locked there like a foot, and follows FK everywhere else.
914
+ if (hum && opts.ik !== 'none') {
915
+ const handNames = [ 'LeftHand', 'RightHand' ].map((k) => hum.target.bones[k as HumanBone]).filter((n): n is string => !!n && byTarget.has(target.index.get(n)!))
916
+ for (const h of detectLegs(target, turned, byTarget, target.bind.worldPos, handNames, warn, 2)) {
917
+ h.isFoot = false
918
+ h.lockOnly = opts.ik !== 'legs+hands'
919
+ h.baseDonor = donor.bind.worldPos[h.donorContact][1]
920
+ h.stanceDonor = donorReach(h, donor.bind.worldPos)
921
+ legs.push(h)
922
+ }
923
+ }
924
+
925
+ // Bridges: unmapped bones between mapped ones. Their reference follows the chain ends' corrections.
926
+ const bridges = findBridges(target, byTarget)
927
+ if (hum) {
928
+ // A metacarpal is palm, not finger: it bridges hand → proximal phalanx but must ride the HAND
929
+ // rigidly (u = 0), not lean halfway into the finger's own alignment — that would carry the
930
+ // knuckles away from the palm and with them the roll readout below.
931
+ const hands = new Set([ humIdx(hum.target, 'LeftHand'), humIdx(hum.target, 'RightHand') ].filter((i): i is number => i !== undefined))
932
+ for (const b of bridges.values()) if (hands.has(b.above)) b.u = 0
933
+ }
934
+ for (const [ i, b ] of bridges) {
935
+ const a = qslerp(A[b.above], A[b.below], b.u)
936
+ A[i] = a
937
+ ref[i] = qnorm(qmul(a, target.bind.worldRot[i]))
938
+ }
939
+
940
+ // Humanoid palm roll (header §5): direction alignment leaves the roll about the arm wherever the
941
+ // shortest arc put it. The knuckle line (index base → little base), projected across the forearm
942
+ // axis, is the one readout of that roll every fingered rig has; the forearm chain turns about the
943
+ // forearm until the target's line lies where the donor's does. A rig without fingers on either side
944
+ // keeps whatever the arc gave, with a note.
945
+ const rollLog: RetargetReport['roll'] = []
946
+ if (hum && (opts.align ?? 'bone-direction') !== 'none') {
947
+ const pt = posesFrom(target, ref)
948
+ const D = hum.donor, T = hum.target
949
+ const dIdx = (k: HumanBone): number | undefined => { const n = D.bones[k]; return n === undefined ? undefined : donor.index.get(n) }
950
+ const children = childrenOf(target)
951
+ for (const s of [ 'Left', 'Right' ] as const) {
952
+ const tLower = humIdx(T, `${s}LowerArm`), tHand = humIdx(T, `${s}Hand`), dLower = dIdx(`${s}LowerArm`), dHand = dIdx(`${s}Hand`)
953
+ if (tLower === undefined || tHand === undefined || dLower === undefined || dHand === undefined) continue
954
+ const axisT = unit(sub(pt[tHand], pt[tLower])), axisD = unit(sub(donor.bind.worldPos[dHand], donor.bind.worldPos[dLower]))
955
+ if (!axisT || !axisD) continue
956
+ // Knuckle line on each side; the thumb base off the hand is the fallback readout.
957
+ const line = (av: Avatar, pos: Vec3[], idx: (k: HumanBone) => number | undefined, hand: number): { v: Vec3; via: string } | null => {
958
+ const i = idx(`${s} Index Proximal`), l = idx(`${s} Little Proximal`), th = idx(`${s} Thumb Proximal`)
959
+ if (i !== undefined && l !== undefined) return { v: sub(pos[i], pos[l]), via: 'knuckle line' }
960
+ if (th !== undefined) return { v: sub(pos[th], pos[hand]), via: 'thumb base' }
961
+ void av
962
+ return null
963
+ }
964
+ const kt = line(T, pt, (k) => humIdx(T, k), tHand), kd = line(D, donor.bind.worldPos, dIdx, dHand)
965
+ if (!kt || !kd) { warn(`${s.toLowerCase()} arm: no fingers on the ${kt ? 'donor' : 'target'} — palm roll not corrected`); continue }
966
+ // Both readouts projected across the DONOR's forearm axis (the target's is aligned to it).
967
+ const perp = (v: Vec3, ax: Vec3): Vec3 | null => { const d = v[0] * ax[0] + v[1] * ax[1] + v[2] * ax[2]; return unit([ v[0] - ax[0] * d, v[1] - ax[1] * d, v[2] - ax[2] * d ]) }
968
+ const a = perp(kt.v, axisD), b = perp(kd.v, axisD)
969
+ if (!a || !b) continue
970
+ const cross: Vec3 = [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
971
+ // …plus what a person added on the avatar (`transfer.roll`, degrees, per arm).
972
+ const ang = Math.atan2(cross[0] * axisD[0] + cross[1] * axisD[1] + cross[2] * axisD[2], a[0] * b[0] + a[1] * b[1] + a[2] * b[2]) + (hum.target.transfer?.roll?.[s] ?? 0) / DEG
973
+ const deg = Math.abs(ang) * DEG
974
+ if (deg < 1) { rollLog.push({ side: s, degrees: 0, via: kt.via }); continue }
975
+ const R: Quat = qnorm([ axisD[0] * Math.sin(ang / 2), axisD[1] * Math.sin(ang / 2), axisD[2] * Math.sin(ang / 2), Math.cos(ang / 2) ])
976
+ // The forearm and everything below it (hand, fingers, twist helpers by inheritance) — a roll
977
+ // is forearm pronation, so the elbow, not the shoulder, absorbs it.
978
+ const stack = [ tLower ]
979
+ while (stack.length) { const i = stack.pop()!; ref[i] = qnorm(qmul(R, ref[i])); A[i] = qnorm(qmul(ref[i], qinv(target.bind.worldRot[i]))); stack.push(...children[i]) }
980
+ rollLog.push({ side: s, degrees: deg, via: `${kt.via} vs ${kd.via}` })
981
+ }
982
+ }
983
+
984
+ // The reference pose on the ground. Straightening a crouched rig lifts it, so the root is first put
985
+ // back down until the lowest joint sits where the bind's lowest joint did (the ground). Then, with
986
+ // legs known, every FOOT is planted: the root comes down further if some leg could not reach the
987
+ // ground even straight, and each leg is bent onto it (`placeLeg`) — a standing reference means
988
+ // all feet on the ground, not just the lowest one, and the foot goals below assume exactly that.
989
+ // Which effectors are feet is the DONOR's call: its effector stands on its ground over the clips.
990
+ const refPos = posesFrom(target, ref)
991
+ const skinned = target.bind.fromSkin.map((v, i) => (v ? i : -1)).filter((i) => i >= 0)
992
+ const lowestBind = Math.min(...skinned.map((i) => target.bind.worldPos[i][1]))
993
+ const donorChildren = childrenOf(donor)
994
+ const donorSkinned = donor.bind.fromSkin.some(Boolean) ? donor.bind.fromSkin.map((v, i) => (v ? i : -1)).filter((i) => i >= 0) : donor.names.map((_, i) => i)
995
+ const donorLowestBind = Math.min(...donorSkinned.map((i) => donor.bind.worldPos[i][1]))
996
+ const ground: { target: number; donor: number } | null = legs.length ? { target: lowestBind, donor: donorGround } : null
997
+ const feet = legs.filter((l) => l.isFoot)
998
+ // Where a foot's contact rests above the ground: at its bind height when the target binds in a
999
+ // stance (a ball sits on the ground; an ankle standing in for it is 10–12 cm up), on the ground
1000
+ // when the bind is posed (nothing to keep — the contact is the lowest bone of its chain anyway).
1001
+ const restAbove = (leg: Leg): number => (stance ? target.bind.worldPos[leg.contact][1] - lowestBind : 0)
1002
+ let groundOffset = feet.length ? lowestBind - Math.min(...feet.map((l) => refPos[l.contact][1] - restAbove(l))) : lowestBind - Math.min(...skinned.map((i) => refPos[i][1]))
1003
+ // A leg that cannot reach the ground even at 98% extension pulls the whole body down — a POSED
1004
+ // bind's problem; a stance bind stands on its own legs, however straight (Mixamo's are).
1005
+ if (!stance) for (const leg of feet) {
1006
+ const hipY = refPos[leg.chain[0]][1] + groundOffset
1007
+ const reach = 0.98 * leg.lengths.reduce((a, b) => a + b, 0)
1008
+ const floor = lowestBind + restAbove(leg)
1009
+ if (hipY - floor > reach) groundOffset -= hipY - floor - reach
1010
+ }
1011
+ const refPosGrounded = refPos.map((p) => [ p[0], p[1] + groundOffset, p[2] ] as Vec3)
1012
+
1013
+ // Bone offsets in the parent's BIND frame — the target's own proportions, re-applied every frame.
1014
+ const localOff: Vec3[] = target.names.map((_, i) => {
1015
+ const p = target.parents[i]
1016
+ if (p < 0) return [ 0, 0, 0 ]
1017
+ return qrot(qinv(target.bind.worldRot[p]), sub(target.bind.worldPos[i], target.bind.worldPos[p]))
1018
+ })
1019
+ const children = childrenOf(target)
1020
+
1021
+ /**
1022
+ * Bend one leg so its effector lands on `goal`, in place: FABRIK from the current joint positions,
1023
+ * then each chain bone turned so its segment points at the solved joint (top-down, so the bend
1024
+ * plane of the starting pose is what survives). The effector keeps its world orientation; the
1025
+ * positions of everything below the chain are re-placed. Returns how far (m) the goal was out of reach.
1026
+ */
1027
+ /** The donor's own bend: its chain's middle joints off its hip→effector line, in WORLD (the donor is
1028
+ * turned to face the target), and how far off relative to its chain length. Smooth where the FK's
1029
+ * is noise — a near-straight leg's knee sits millimetres off the line in whatever direction the
1030
+ * transferred rotations' rounding left it, and that direction swings frame to frame. */
1031
+ const donorBend = (leg: Leg, dpos: Vec3[]): { dir: Vec3; off: number } | null => {
1032
+ const hip = dpos[leg.donorHip], end = dpos[leg.donorEffector]
1033
+ const axis = unit(sub(end, hip))
1034
+ if (!axis) return null
1035
+ const acc: Vec3 = [ 0, 0, 0 ]
1036
+ let n = 0
1037
+ for (let k = 1; k < leg.chain.length; k++) {
1038
+ const d = byTarget.get(leg.chain[k])
1039
+ if (d === undefined) continue
1040
+ const v = sub(dpos[d], hip), t = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]
1041
+ acc[0] += v[0] - axis[0] * t; acc[1] += v[1] - axis[1] * t; acc[2] += v[2] - axis[2] * t; n++
1042
+ }
1043
+ const dir = n ? unit(acc) : null
1044
+ return dir ? { dir, off: len(acc) / n / donorLegLen(leg) } : null
1045
+ }
1046
+ const placeLeg = (leg: Leg, goal: Vec3, rot: Quat[], pos: Vec3[], bend: { dir: Vec3; off: number; axisW?: Vec3; sign?: number } | null = null): number => {
1047
+ const full = [ ...leg.chain, leg.effector ]
1048
+ const hip = pos[leg.chain[0]]
1049
+ const pts = full.map((i) => [ ...pos[i] ] as Vec3)
1050
+ const total = leg.lengths.reduce((a, b) => a + b, 0)
1051
+ const short = len(sub(goal, hip)) - total
1052
+ // FABRIK keeps the bend plane it starts from, and a NEAR-straight chain has no reliable one:
1053
+ // the middle joints sit a millimetre off the hip→foot line in whatever direction the FK's
1054
+ // rounding left them, and a goal a centimetre short of full extension turns that millimetre
1055
+ // into a 20° bend — backwards as readily as forwards (a knee flipping behind the leg for a
1056
+ // whole stance phase). So the bend direction is the BIND pose's (`pole`, turned with the hip)
1057
+ // whenever the chain is nearly straight, the FK's own when it is clearly bent (≥ ~12°, where the
1058
+ // direction is real), blended in between; a bend against the pole never counts — a knee or an
1059
+ // elbow has one way to fold. The middle joints are moved onto that direction, sagged by what
1060
+ // the goal needs, so FABRIK converges instead of crawling off the line.
1061
+ const axis = unit(sub(pts[pts.length - 1], hip))
1062
+ if (axis && short < 0) {
1063
+ const hipTurn = qmul(rot[leg.chain[0]], qinv(target.bind.worldRot[leg.chain[0]]))
1064
+ const pole = leg.pole ? qrot(hipTurn, leg.pole) : [ 0, 0, 1 ] as Vec3
1065
+ const acc: Vec3 = [ 0, 0, 0 ]
1066
+ for (let k = 1; k < pts.length - 1; k++) {
1067
+ const v = sub(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]
1068
+ acc[0] += v[0] - axis[0] * d; acc[1] += v[1] - axis[1] * d; acc[2] += v[2] - axis[2] * d
1069
+ }
1070
+ // The donor's bend direction where it has one (the knee bends the way the donor's knee bends);
1071
+ // the FK's own otherwise.
1072
+ const off = bend ? bend.off * total : len(acc) / (pts.length - 2)
1073
+ // The bend DIRECTION: from the calibrated HINGE when the leg has one (the knee is a hinge, so
1074
+ // the donor's thigh rotation times the hinge axis is the bend plane — exact however straight
1075
+ // the leg, the engine's method); else the donor's position-derived offset, which is noise on a
1076
+ // straight leg and therefore blended with the bind pole below 12° of bend.
1077
+ let cur: Vec3 | null = null, w = 0
1078
+ if (bend?.axisW) {
1079
+ const toGoal = unit(sub(goal, hip))
1080
+ const c = toGoal ? unit(cross(bend.axisW, toGoal)) : null
1081
+ if (c) { cur = scl(c, bend.sign ?? 1); w = 1 }
1082
+ }
1083
+ if (!cur) {
1084
+ cur = bend ? bend.dir : unit(acc)
1085
+ const against = cur ? cur[0] * pole[0] + cur[1] * pole[1] + cur[2] * pole[2] < 0 : false
1086
+ w = cur && !against ? Math.min(1, off / (BEND_TRUST * total)) : 0
1087
+ }
1088
+ if (w < 1) {
1089
+ const dir = unit(add(scl(pole, 1 - w), scl(cur ?? pole, w))) ?? pole
1090
+ const dist = len(sub(goal, hip))
1091
+ const sag = Math.max(off, 1e-3 * total, 0.5 * Math.sqrt(Math.max(0, total * total - dist * dist)))
1092
+ for (let k = 1; k < pts.length - 1; k++) {
1093
+ const v = sub(pts[k], hip), d = v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]
1094
+ pts[k] = add(add(hip, scl(axis, d)), scl(dir, sag))
1095
+ }
1096
+ }
1097
+ }
1098
+ fabrik(pts, leg.lengths, goal)
1099
+ for (let k = 0; k < leg.chain.length; k++) {
1100
+ const i = leg.chain[k], next = full[k + 1]
1101
+ const cur = unit(qrot(rot[i], localOff[next])), want = unit(sub(pts[k + 1], pos[i]))
1102
+ if (cur && want) rot[i] = qnorm(qmul(arc(cur, want), rot[i]))
1103
+ pos[next] = add(pos[i], qrot(rot[i], localOff[next]))
1104
+ }
1105
+ const stack = [ leg.effector ]
1106
+ while (stack.length) {
1107
+ const i = stack.pop()!
1108
+ for (const c of children[i]) { pos[c] = add(pos[i], qrot(rot[i], localOff[c])); stack.push(c) }
1109
+ }
1110
+ return short
1111
+ }
1112
+ // Plant every foot in the reference pose (the chain bones' reference orientations change with it);
1113
+ // every effector's grounded reference height is then its stance height.
1114
+ for (const leg of feet) {
1115
+ const c = refPosGrounded[leg.contact], floor = lowestBind + restAbove(leg)
1116
+ if (Math.abs(c[1] - floor) < 1e-6) continue
1117
+ const e = refPosGrounded[leg.effector] // the effector moves by what its contact is short of the floor
1118
+ placeLeg(leg, [ e[0], e[1] + floor - c[1], e[2] ], ref, refPosGrounded)
1119
+ }
1120
+ for (const leg of legs) {
1121
+ leg.baseTarget = refPosGrounded[leg.contact][1]
1122
+ leg.stanceTarget = sub(refPosGrounded[leg.contact], refPosGrounded[leg.chain[0]])
1123
+ leg.refReach = len(sub(refPosGrounded[leg.effector], refPosGrounded[leg.chain[0]]))
1124
+ }
1125
+ for (const i of byTarget.keys()) A[i] = qnorm(qmul(ref[i], qinv(target.bind.worldRot[i])))
1126
+
1127
+ // Twist helpers (header §6): the avatar's helpers that are children of a limb bone with a
1128
+ // canonical child to measure the far end's twist against.
1129
+ const twists: Twist[] = []
1130
+ const twistFraction = opts.twist === false ? 0 : opts.twist ?? 0.5
1131
+ if (hum && twistFraction > 0) {
1132
+ const NEXT: Partial<Record<HumanBone, HumanBone>> = {
1133
+ LeftUpperArm: 'LeftLowerArm', LeftLowerArm: 'LeftHand', RightUpperArm: 'RightLowerArm', RightLowerArm: 'RightHand',
1134
+ LeftUpperLeg: 'LeftLowerLeg', LeftLowerLeg: 'LeftFoot', RightUpperLeg: 'RightLowerLeg', RightLowerLeg: 'RightFoot',
1135
+ }
1136
+ for (const [ name, of ] of Object.entries(hum.target.twist)) {
1137
+ const h = target.index.get(name), p = humIdx(hum.target, of), nxt = NEXT[of], c = nxt ? humIdx(hum.target, nxt) : undefined
1138
+ if (h === undefined || p === undefined || c === undefined || target.parents[h] !== p || !byTarget.has(p) || !byTarget.has(c)) continue
1139
+ let g = target.parents[p]
1140
+ while (g >= 0 && !byTarget.has(g)) g = target.parents[g]
1141
+ if (g < 0) continue
1142
+ const axis = unit(sub(target.bind.worldPos[c], target.bind.worldPos[p]))
1143
+ if (!axis) continue
1144
+ const along = sub(target.bind.worldPos[h], target.bind.worldPos[p])
1145
+ const t = Math.max(0, Math.min(1, (along[0] * axis[0] + along[1] * axis[1] + along[2] * axis[2]) / len(sub(target.bind.worldPos[c], target.bind.worldPos[p]))))
1146
+ twists.push({ bone: h, parent: p, grand: g, child: c, t, axisLocal: qrot(qinv(target.bind.worldRot[p]), axis), name, of })
1147
+ }
1148
+ }
1149
+
1150
+ // Where the travel goes (header §6). A root above the hips: the horizontal travel on it, the bob
1151
+ // on the hips; no such root: the hips carry everything as the donor's did.
1152
+ const separateRoot = rootIdx >= 0 && hipsIdx >= 0 && hipsIdx !== rootIdx && donorRoot !== undefined
1153
+ const rootMotion: Exclude<RootMotion, 'auto'> = (opts.rootMotion ?? 'auto') === 'auto' ? (separateRoot ? 'root' : 'hips') : opts.rootMotion as Exclude<RootMotion, 'auto'>
1154
+ // The root's heading (header §6): only a root that takes the hips' travel can take their turn.
1155
+ const donorHipsForYaw = hum ? (() => { const n = hum.donor.bones.Hips; return n === undefined ? undefined : donor.index.get(n) })() : undefined
1156
+ const rootRotation: 'none' | 'yaw' = (opts.rootRotation ?? 'none') === 'yaw' && rootTranslateOnly && donorHipsForYaw !== undefined ? 'yaw' : 'none'
1157
+ if ((opts.rootRotation ?? 'none') === 'yaw' && rootRotation === 'none') warn(`rootRotation 'yaw' needs a target root above the hips mapped to the donor's hips${hum ? '' : ' (humanoid avatars)'} — the root keeps its bind heading`)
1158
+ /** The donor hips' heading about the up axis, from their bind: where the hips' forward points. */
1159
+ const hipsHeading = (dwRot: Quat[]): number => {
1160
+ const q = qmul(dwRot[donorHipsForYaw!], qinv(donor.bind.worldRot[donorHipsForYaw!]))
1161
+ const f = qrot(q, [ 0, 0, 1 ])
1162
+ return Math.atan2(f[0], f[2])
1163
+ }
1164
+ const wrapRad = (a: number): number => Math.atan2(Math.sin(a), Math.cos(a))
1165
+ // A donor whose hips bob relative to their own root (MoCap Central's `root` + `pelvis`) — that bob
1166
+ // is motion the rotation transfer cannot carry, so it is re-applied on the target's hips.
1167
+ const donorHips = hum ? (() => { const n = hum.donor.bones.Hips; return n === undefined ? undefined : donor.index.get(n) })() : undefined
1168
+ const donorBob = separateRoot && donorHips !== undefined && donorHips !== donorRoot && donor.parents[donorHips] >= 0
1169
+ const hipsParent = hipsIdx >= 0 ? target.parents[hipsIdx] : -1
1170
+
1171
+
1172
+ // Root motion is a distance, so it scales with the size difference between the two animals — a
1173
+ // stride is leg length, so legs set it when known; the root heights are the fallback.
1174
+ let rootScaleFrom: RetargetReport['rootScaleFrom'] = 'none'
1175
+ const rootScale = (() => {
1176
+ if (opts.rootScale !== undefined) { rootScaleFrom = 'option'; return opts.rootScale }
1177
+ if (rootIdx < 0 || donorRoot === undefined) return 1
1178
+ // the FEET's ratio: a stride is leg length. The hands are effectors too (`legs` holds them) but
1179
+ // an arm ratio has no say in the travel — averaged in, it scaled Kachujin's ×1.17 down to ×1.10
1180
+ const striders = feet.length ? feet : legs
1181
+ if (striders.length) { rootScaleFrom = 'legs'; return striders.reduce((a, l) => a + l.ratio, 0) / striders.length }
1182
+ const th = target.bind.worldPos[rootIdx][1], dh = donor.bind.worldPos[donorRoot][1]
1183
+ rootScaleFrom = 'height'
1184
+ return dh !== 0 ? th / dh : 1
1185
+ })()
1186
+
1187
+ // The target root's parent is static, so its inverse is computed once and reused every frame.
1188
+ const rootParent = rootIdx >= 0 ? target.parents[rootIdx] : -1
1189
+ const restWorld = worldOf(target, target.rest)
1190
+ const parentRot = rootParent >= 0 ? restWorld.rot[rootParent] : IDENT
1191
+ const parentPos = rootParent >= 0 ? restWorld.pos[rootParent] : ([ 0, 0, 0 ] as Vec3)
1192
+ const parentScale = rootParent >= 0 ? restWorld.scale[rootParent] : 1
1193
+
1194
+ const report: RetargetReport = {
1195
+ bindGap: { donor: bindVsRest(turned), target: bindVsRest(target) },
1196
+ donorRef: donorRefFrom,
1197
+ legDisagreement,
1198
+ stance,
1199
+ align: alignLog,
1200
+ underThreshold: alignUnder,
1201
+ kept,
1202
+ bridges: [ ...bridges.keys() ].map((i) => target.names[i]),
1203
+ groundOffset,
1204
+ ground,
1205
+ legs: legs.map((l) => ({ effector: l.name, donorEffector: donor.names[l.donorEffector], contact: target.names[l.contact], chain: l.chain.map((i) => target.names[i]), ratio: l.ratio, shortfall: 0, isFoot: l.isFoot, planted: 0 })),
1206
+ clips: [],
1207
+ mapped: pairs.map((p) => ({ target: p.target, donor: p.donor })),
1208
+ unmappedTarget: target.names.filter((n, i) => !byTarget.has(i) && !bridges.has(i) && target.nodes[i].getMesh() === null),
1209
+ rootScale,
1210
+ rootScaleFrom,
1211
+ root: rootIdx >= 0 && donorRoot !== undefined ? { target: target.names[rootIdx], donor: donor.names[donorRoot] } : null,
1212
+ rootTranslateOnly,
1213
+ roll: rollLog,
1214
+ rootMotion,
1215
+ rootRotation,
1216
+ hipsBob: separateRoot ? (donorBob ? 'donor' : rootMotion === 'hips' ? null : 'root') : null,
1217
+ twist: twists.map((t) => ({ bone: t.name, of: t.of, at: t.t })),
1218
+ }
1219
+
1220
+ const animated = new Set<number>([ ...byTarget.keys(), ...bridges.keys(), ...twists.map((t) => t.bone) ])
1221
+ const rootRefPos = rootIdx >= 0 ? refPosGrounded[rootIdx] : null
1222
+ const plantOn = opts.plant !== false
1223
+ // Planting thresholds scale with the donor's leg: a foot moving under 30 % of a leg length per
1224
+ // second, within 6 % of a leg length of its stance height, is standing on the ground.
1225
+ const donorLegLen = (l: Leg): number => len(sub(donor.bind.worldPos[l.donorEffector], donor.bind.worldPos[l.donorHip])) || 1
1226
+ // The donor leg's FULL length (its segments summed) — the extension fraction is measured against
1227
+ // this, not against the bind's hip→ankle DISTANCE: a bind with bent knees (the UE4 mannequin,
1228
+ // ~7°) made a relaxed stand read as "fully extended" and the target, whose bind is straight,
1229
+ // reproduced its own bind's straightness instead of the donor's bend (Fidget V1 knees at 0°).
1230
+ const donorLegSum = (l: Leg): number => {
1231
+ const js = [ ...l.chain.map((i) => byTarget.get(i)), l.donorEffector ]
1232
+ let sum = 0
1233
+ for (let k = 1; k < js.length; k++) { const a = js[k - 1], b = js[k]; if (a === undefined || b === undefined) return donorLegLen(l); sum += len(sub(donor.bind.worldPos[b], donor.bind.worldPos[a])) }
1234
+ return sum || donorLegLen(l)
1235
+ }
1236
+ const PLANT_FADE = 4
1237
+
1238
+ const driven = new Set<number>([ ...byTarget.keys(), ...bridges.keys() ])
1239
+ // How far above the FLOOR each rig's hand reaches standing (bind: the hips' height over the
1240
+ // lowest foot + the shoulder's height over the hips + the arm's two segments), target over donor. A PLATFORM a take's hands hold or push on must stay within
1241
+ // this reach: its height comes across scaled by the LEGS (the root's vertical travel), and on a
1242
+ // rig whose legs grew 17 % but whose overhead reach only 3 % the ledge ended 36 cm past the
1243
+ // hands (the hang from the floor). Such takes scale their vertical travel by the smaller ratio.
1244
+ let reachRatio = rootScale
1245
+ if (hum) {
1246
+ const reach = (rig: Rig, av: Avatar): number | null => {
1247
+ const idx = (k: HumanBone): number | undefined => { const n = av.bones[k]; return n === undefined ? undefined : rig.index.get(n) }
1248
+ const hips = idx('Hips'), arm = idx('LeftUpperArm'), fore = idx('LeftLowerArm'), hand = idx('LeftHand'), foot = idx('LeftFoot'), toes = idx('LeftToes')
1249
+ if (hips === undefined || arm === undefined || fore === undefined || hand === undefined || foot === undefined) return null
1250
+ const P = rig.bind.worldPos
1251
+ const floor = Math.min(P[foot][1], toes !== undefined ? P[toes][1] : P[foot][1])
1252
+ return (P[hips][1] - floor) + (P[arm][1] - P[hips][1]) + len(sub(P[fore], P[arm])) + len(sub(P[hand], P[fore]))
1253
+ }
1254
+ const rt = reach(target, hum.target), rd = reach(donor, hum.donor)
1255
+ if (rt && rd) reachRatio = rt / rd
1256
+ if (process.env.LECODES_MARKS_TRACE) console.log(`[marks] standing reach target ${rt?.toFixed(3)} donor ${rd?.toFixed(3)} → ratio ${reachRatio.toFixed(3)} (root scale ${rootScale.toFixed(3)})`)
1257
+ }
1258
+ // the bones a foot's position depends on: the hips / root and every leg chain (tight key reduction)
1259
+ const legBones = new Set<number>()
1260
+ for (const l of legs) { for (const i of l.chain) legBones.add(i); legBones.add(l.effector); legBones.add(l.contact) }
1261
+ if (legBones.size) { if (hipsIdx >= 0) legBones.add(hipsIdx); if (rootIdx >= 0) legBones.add(rootIdx) }
1262
+
1263
+ // ---- knee hinge axes (per leg, once per pack — the engine's calibration, curves.cpp) ----------
1264
+ // The bend plane the leg solver used to derive from the donor's knee POSITION is noise exactly
1265
+ // where a lock operates: a relaxed stand's knee sits millimetres off the hip→ankle line and that
1266
+ // direction swings frame to frame (Fidget V1: the knee flicked 0–9° at 2.3°/frame, the donor's
1267
+ // moved 0.07°/frame). The knee is a hinge, so the axis of ANY delta between two of its local
1268
+ // rotations is the hinge axis in the thigh's frame — exact however small the step. Calibrated
1269
+ // over every clip's knee track (angle-weighted, sign-aligned, measurements > 30° off the mean
1270
+ // trimmed); the bend direction at a frame is then sign · (R_donorThigh · h) × (hip → goal), the
1271
+ // sign fixed once from the frames where the donor's knee is clearly bent.
1272
+ const hinges = new Map<number, { axis: Vec3; sign: number }>()
1273
+ for (const [ li, leg ] of legs.entries()) {
1274
+ if (leg.chain.length < 2) continue
1275
+ const dThigh = byTarget.get(leg.chain[0]), dKnee = byTarget.get(leg.chain[1])
1276
+ if (dThigh === undefined || dKnee === undefined) continue
1277
+ const meas: { axis: Vec3; w: number }[] = []
1278
+ let acc: Vec3 = [ 0, 0, 0 ]
1279
+ const kMinStep = 0.5 * Math.PI / 180
1280
+ for (const clip of clips) {
1281
+ const n = Math.max(2, Math.round(clip.duration * fps) + 1)
1282
+ let prev = clip.sample(0).r[dKnee]
1283
+ for (let k = 1; k < n; k++) {
1284
+ const q = clip.sample((k / (n - 1)) * clip.duration).r[dKnee]
1285
+ let d = qnorm(qmul(q, qinv(prev)))
1286
+ prev = q
1287
+ if (d[3] < 0) d = [ -d[0], -d[1], -d[2], -d[3] ]
1288
+ const sl = Math.hypot(d[0], d[1], d[2])
1289
+ const ang = 2 * Math.atan2(sl, d[3])
1290
+ if (ang < kMinStep || sl < 1e-6) continue
1291
+ let axis: Vec3 = [ d[0] / sl, d[1] / sl, d[2] / sl ]
1292
+ if (axis[0] * acc[0] + axis[1] * acc[1] + axis[2] * acc[2] < 0) axis = scl(axis, -1)
1293
+ acc = add(acc, scl(axis, ang))
1294
+ meas.push({ axis, w: ang })
1295
+ }
1296
+ }
1297
+ if (meas.length < 8 || len(acc) < 1e-5) continue
1298
+ let h = unit(acc)!
1299
+ for (let pass = 0; pass < 2; pass++) {
1300
+ acc = [ 0, 0, 0 ]
1301
+ for (const m of meas) {
1302
+ if (m.axis[0] * h[0] + m.axis[1] * h[1] + m.axis[2] * h[2] < 0) m.axis = scl(m.axis, -1)
1303
+ if (pass === 1 && Math.acos(Math.max(-1, Math.min(1, m.axis[0] * h[0] + m.axis[1] * h[1] + m.axis[2] * h[2]))) > 30 * Math.PI / 180) continue
1304
+ acc = add(acc, scl(m.axis, m.w))
1305
+ }
1306
+ h = unit(acc) ?? h
1307
+ }
1308
+ // the sign: where the donor's knee is clearly off its hip→ankle line, does (axis × line) point to it?
1309
+ let vote = 0
1310
+ const L = donorLegLen(leg)
1311
+ for (const clip of clips) {
1312
+ const n = Math.max(2, Math.round(clip.duration * fps) + 1)
1313
+ for (let k = 0; k < n; k += 4) {
1314
+ const dw = worldOf(donor, clip.sample((k / (n - 1)) * clip.duration))
1315
+ const line = unit(sub(dw.pos[leg.donorEffector], dw.pos[leg.donorHip]))
1316
+ if (!line) continue
1317
+ const v = sub(dw.pos[dKnee], dw.pos[leg.donorHip])
1318
+ const t = v[0] * line[0] + v[1] * line[1] + v[2] * line[2]
1319
+ const offv = sub(v, scl(line, t))
1320
+ if (len(offv) < 0.05 * L) continue
1321
+ const c = cross(qrot(dw.rot[dThigh], h), line)
1322
+ vote += (c[0] * offv[0] + c[1] * offv[1] + c[2] * offv[2]) > 0 ? 1 : -1
1323
+ }
1324
+ }
1325
+ hinges.set(li, { axis: h, sign: vote >= 0 ? 1 : -1 })
1326
+ }
1327
+ /** The bend the leg solver gets: the donor's offset magnitude, and the calibrated hinge's direction when there is one. */
1328
+ const bendOf = (li: number, leg: Leg, dw: ReturnType<typeof worldOf>): { dir: Vec3; off: number; axisW?: Vec3; sign?: number } | null => {
1329
+ const b = donorBend(leg, dw.pos)
1330
+ const hg = hinges.get(li)
1331
+ if (!hg) return b
1332
+ const dThigh = byTarget.get(leg.chain[0])!
1333
+ return { dir: b?.dir ?? [ 0, 0, 1 ], off: b?.off ?? 0, axisW: qrot(dw.rot[dThigh], hg.axis), sign: hg.sign }
1334
+ }
1335
+ const hipsParentScale = hipsParent >= 0 ? restWorld.scale[hipsParent] : 1
1336
+
1337
+ for (const clip of clips) {
1338
+ const frames = Math.max(2, Math.round(clip.duration * fps) + 1)
1339
+ // the vertical scale of this clip's root travel: the legs' (rootScale), except a PLATFORM take,
1340
+ // whose platform must stay within the hands' reach too
1341
+ const vScale = (opts.stairs ? opts.stairs(clip.name) : false) === 'platform' ? Math.min(rootScale, reachRatio) : rootScale
1342
+ const channels = new Map<number, Channel>()
1343
+ const chan = (node: number): Channel => {
1344
+ let c = channels.get(node)
1345
+ if (!c) { c = { node, times: [] }; channels.set(node, c) }
1346
+ return c
1347
+ }
1348
+ const events: RetargetEvent[] = []
1349
+ const open = new Map<string, RetargetEvent>()
1350
+ const mark = (kind: RetargetEvent['kind'], bone: string, time: number, value?: number, at?: Vec3): void => {
1351
+ const key = `${kind}:${bone}`
1352
+ const e = open.get(key)
1353
+ if (e) { e.to = time; if (value !== undefined) e.value = Math.max(e.value ?? 0, value) }
1354
+ else { const n: RetargetEvent = { kind, bone, from: time, to: time, ...(value !== undefined ? { value } : {}), ...(at ? { at } : {}) }; open.set(key, n); events.push(n) }
1355
+ }
1356
+ // the root's horizontal position per frame: contacts are expressed relative to it, the travel is its displacement
1357
+ const rootXZ = (): Vec3 => (rootIdx >= 0 ? wpos[rootIdx] : hipsIdx >= 0 ? wpos[hipsIdx] : [ 0, 0, 0 ])
1358
+ let rootFirst: Vec3 = [ 0, 0, 0 ], rootLast: Vec3 = [ 0, 0, 0 ]
1359
+ // per frame, for the contacts block: the root's position and each foot's contact bone (after placement)
1360
+ const rootTrack: Vec3[] = []
1361
+ const hipsYawTrack: number[] = [] // the hips' world yaw per output frame (radians), for the heading curve
1362
+ const contactTrack = new Map<number, Vec3[]>()
1363
+ // …and the whole body + the pelvis (after placement): the platform's face is fitted on the OUTPUT
1364
+ const bodyTrack: Vec3[][] = [], hipsTrack: Vec3[] = []
1365
+ const ankleTrack = new Map<number, Vec3[]>() // the feet's ankles (the heel's witness for the ledge)
1366
+ let dHeadingAll: number[] = [] // the donor's heading per frame (pass 1), for the platform fit's facing fallback
1367
+
1368
+ // Per frame: pose the donor, then walk the target top-down so every parent's retargeted world
1369
+ // rotation is already known when its children need it; then place the feet; then emit locals.
1370
+ const wrot: Quat[] = new Array(target.names.length)
1371
+ const wpos: Vec3[] = new Array(target.names.length)
1372
+ const delta: Quat[] = new Array(target.names.length)
1373
+ const timeOf = (f: number): number => (f / (frames - 1)) * clip.duration
1374
+ // The root's turn (rootRotation 'yaw'): the hips' heading since the clip's first frame goes on
1375
+ // the root as a rotation about the up axis; the hips' world rotation is untouched, so their local
1376
+ // rotation loses exactly that turn (the engine takes it off the pose and onto the node).
1377
+ const yaw0 = rootRotation === 'yaw' ? hipsHeading(worldOf(donor, clip.sample(0)).rot) : 0
1378
+ const turn = rootRotation === 'yaw' ? wrapRad(hipsHeading(worldOf(donor, clip.sample(clip.duration)).rot) - yaw0) * DEG : 0
1379
+ /** The donor posed and the target's FK for frame `f` (everything but the effectors). */
1380
+ const frameFK = (f: number): { dw: ReturnType<typeof worldOf> } => {
1381
+ const donorPose: Pose = clip.sample(timeOf(f))
1382
+ const dw = worldOf(donor, donorPose)
1383
+ for (const i of byTarget.keys()) { const s = byTarget.get(i)!; delta[i] = qmul(dw.rot[s], qinv(donor.bind.worldRot[s])) }
1384
+ if (rootTranslateOnly) {
1385
+ if (rootRotation === 'yaw') { const y = wrapRad(hipsHeading(dw.rot) - yaw0); delta[rootIdx] = [ 0, Math.sin(y / 2), 0, Math.cos(y / 2) ] }
1386
+ else delta[rootIdx] = IDENT
1387
+ }
1388
+ for (const [ i, b ] of bridges) delta[i] = qslerp(delta[b.above], delta[b.below], b.u)
1389
+ // The donor's hips bob relative to its root (header §6), scaled, in the world after the root's turn.
1390
+ let bob: Vec3 | null = null
1391
+ if (donorBob && donorRoot !== undefined && donorHips !== undefined) {
1392
+ const v = sub(dw.pos[donorHips], dw.pos[donorRoot])
1393
+ const v0 = sub(donor.bind.worldPos[donorHips], donor.bind.worldPos[donorRoot])
1394
+ const dRoot = qmul(dw.rot[donorRoot], qinv(donor.bind.worldRot[donorRoot]))
1395
+ const bv = sub(v, qrot(dRoot, v0))
1396
+ bob = [ bv[0] * rootScale, bv[1] * vScale, bv[2] * rootScale ] // the vertical at this clip's scale (a platform's within the hands' reach)
1397
+ }
1398
+ for (const i of target.order) {
1399
+ const p = target.parents[i]
1400
+ if (driven.has(i)) wrot[i] = qnorm(qmul(delta[i], ref[i]))
1401
+ // Unmapped: hold the rest LOCAL rotation, so the bone rides its parent without drifting.
1402
+ else wrot[i] = p >= 0 ? qnorm(qmul(wrot[p], target.rest.r[i])) : qnorm(target.rest.r[i])
1403
+
1404
+ if (i === rootIdx && rootRefPos && donorRoot !== undefined) {
1405
+ // Root translation in WORLD space: the reference position plus the donor's scaled travel.
1406
+ const d = dw.pos[donorRoot], r = donor.bind.worldPos[donorRoot]
1407
+ const tv = sub(d, r)
1408
+ wpos[i] = add(rootRefPos, [ tv[0] * rootScale, tv[1] * vScale, tv[2] * rootScale ])
1409
+ } else if (p < 0) wpos[i] = target.rest.t[i]
1410
+ else wpos[i] = add(wpos[p], qrot(wrot[p], localOff[i]))
1411
+ if (i === hipsIdx && bob) wpos[i] = add(wpos[i], bob)
1412
+ }
1413
+ return { dw }
1414
+ }
1415
+ // Feet: the target's OWN stance plus the donor's scaled departure from ITS stance — on all
1416
+ // three axes. Copying the donor's absolute foot-under-hip offset instead would overwrite the
1417
+ // target's build with the donor's: a fox that tucks its feet inboard gets planted on the
1418
+ // donor's vertical legs and reads as bow-legged in every clip.
1419
+ // Hip-relative on all three axes: the donor's hip→foot vector scaled, plus the stance correction
1420
+ // (where THIS rig rests its foot under its hip, against where the donor does — vertically that
1421
+ // is the target's own ground + the donor's scaled departure from ITS ground, since the hips carry
1422
+ // the scaled bob). The correction is a standing thing: when the donor's leg is near straight it
1423
+ // can ask for more than the leg's length (a running push-off on a rig whose hips sit higher
1424
+ // above its feet), so it is shrunk — never the donor's vector — until the goal is in reach.
1425
+ // The goal is the CONTACT's (the ball of a humanoid foot, header §6); the effector's follows
1426
+ // from it through the foot at its FK orientation — `off` is where the FK put the contact
1427
+ // relative to the effector, and the effector keeps that orientation through the IK.
1428
+ const goalOf = (leg: Leg, dw: ReturnType<typeof worldOf>): { effector: Vec3; contact: Vec3 } => {
1429
+ const hip = wpos[leg.chain[0]]
1430
+ const off = sub(wpos[leg.contact], wpos[leg.effector])
1431
+ const a = sub(donorReach(leg, dw.pos), off) // hip → effector: the donor's reach scaled, less the foot
1432
+ const c: Vec3 = [ 0, 1, 2 ].map((i) => leg.stanceTarget[i] - leg.stanceDonor[i]) as Vec3
1433
+ if (opts.footStance === 'donor') { c[0] = 0; c[2] = 0 } // the donor's own foot-under-hip offset, scaled, stands
1434
+ const R = 0.995 * leg.lengths.reduce((x, y) => x + y, 0)
1435
+ let k = 1
1436
+ if (len(add(a, c)) > R) {
1437
+ // Largest k in [0, 1] with |a + k·c| = R (k = 0 if even the donor's vector alone overreaches).
1438
+ const cc = c[0] * c[0] + c[1] * c[1] + c[2] * c[2], ac = a[0] * c[0] + a[1] * c[1] + a[2] * c[2], aa = a[0] * a[0] + a[1] * a[1] + a[2] * a[2] - R * R
1439
+ const disc = ac * ac - cc * aa
1440
+ k = cc > 1e-12 && disc >= 0 ? Math.max(0, Math.min(1, (-ac + Math.sqrt(disc)) / cc)) : 0
1441
+ }
1442
+ let effector = [ 0, 1, 2 ].map((i) => hip[i] + a[i] + c[i] * k) as Vec3
1443
+ // The knee and the ankle are the donor's. Building the ankle from the ball puts the two feet's
1444
+ // SHAPE difference into the goal: this ankle sits lower and further behind its ball than the
1445
+ // donor's (Vex 31° vs Humanoid_F 51° of ankle→ball pitch — a 5 cm vector that turns with the
1446
+ // foot). Spent by the knee it is a 20° wobble through a stance the donor holds straight (a
1447
+ // centimetre short of full extension is 20° of knee); spent by the ankle it is a 6 cm pop at
1448
+ // heel strike, when the extended leg can only swing. So for a foot the goal is the ANKLE's:
1449
+ // direction = the donor's hip→ankle, scaled, plus the stance correction; distance = the
1450
+ // target's own stance reach scaled by how far the donor's leg is extended against its
1451
+ // reference (capped at the chain's full length — the 0.995 alone is a 12° knee). The ball is
1452
+ // then wherever this foot's shape puts it — a constant offset the eye does not catch — and
1453
+ // serves only the planting (horizontal anchor) and the floor (the ankle pitch below).
1454
+ if (leg.isFoot && leg.refReach > 0) {
1455
+ // The stance correction is whole here (not `k`-shrunk): reach is the distance's business
1456
+ // now, and shrinking it only where the leg is straight moved the ankle 6 cm sideways
1457
+ // every time a leg passed through full extension — heel strike and toe-off, twice a stride.
1458
+ const ankle = add(scl(sub(dw.pos[leg.donorEffector], dw.pos[leg.donorHip]), leg.ratio), c)
1459
+ const dir = unit(ankle)
1460
+ // …the donor's extension as a fraction of its FULL leg length, applied to this leg's full
1461
+ // length: the same knee bend, whatever either rig's bind pose does with its knees
1462
+ const ext = len(sub(dw.pos[leg.donorEffector], dw.pos[leg.donorHip])) / donorLegSum(leg)
1463
+ if (dir) effector = add(hip, scl(dir, Math.min(R / 0.995, ext * (R / 0.995))))
1464
+ }
1465
+ return { effector, contact: add(effector, off) }
1466
+ }
1467
+
1468
+ // Pass 1 — MARKS on the donor (marks.ts): where each foot — and each hand — touches, holds still
1469
+ // and lifts, measured on the donor's own limbs (clean by construction: its clips were solved on
1470
+ // it). Every foot's STILL window and every hand's whole TOUCH becomes a LOCK: the target's contact
1471
+ // bone is held at one world point through it (x/z = the mean of where the FK put it — anchoring
1472
+ // on the first frame piles the whole slide onto the lift-off, the mean splits it; y = the target's
1473
+ // ground, its tread on a stair clip, or where the FK put it when that is far from the ground) and
1474
+ // the limb's end keeps one rotation, so the pelvis sway the transfer leaks into the feet (leg
1475
+ // proportions differ, so the donor's own cancellation does not carry over) goes to the knees
1476
+ // instead. Faded in and out over PLANT_FADE frames outside a window, never past its touch. A
1477
+ // window across the loop seam is one window.
1478
+ const dtM = frames > 1 ? clip.duration / (frames - 1) : 1
1479
+ const levelKind: LevelKind = opts.stairs ? opts.stairs(clip.name) : false
1480
+ const sideOfLeg = (li: number): 'l' | 'r' | null => {
1481
+ const n = `${legs[li].name} ${target.names[legs[li].contact]}`
1482
+ if (/left|(^|[_.\s])l([_.\s]|$)/i.test(n)) return 'l'
1483
+ if (/right|(^|[_.\s])r([_.\s]|$)/i.test(n)) return 'r'
1484
+ return null
1485
+ }
1486
+ // `yTrack`: a HEIGHT-ONLY lock — a hand that slides through its contact (a dive roll's hands
1487
+ // travel 25 cm while the fingertips are on the floor) keeps the donor's height above the level
1488
+ // frame by frame, x/z and rotation free
1489
+ type Window = { leg: number; start: number; end: number; touchStart: number; touchEnd: number; anchor: Vec3; rot: Quat; step?: number; openStart?: boolean; openEnd?: boolean; yTrack?: Float32Array }
1490
+ const windows: Window[] = []
1491
+ // per FOOT, per frame: how far the whole foot must shift vertically for its clearance over the
1492
+ // ground to match the donor's (NaN outside the touch spans) — see the block after the windows
1493
+ const heightTracks = new Map<number, Float32Array>()
1494
+ let donorMarks: ReturnType<typeof detectMarks> | null = null
1495
+ const footSide = new Map<number, 'l' | 'r'>(), handSide = new Map<number, 'l' | 'r'>()
1496
+ for (const [ li, leg ] of legs.entries()) { const s = sideOfLeg(li); if (s) (leg.isFoot ? footSide : handSide).set(li, s) }
1497
+ const limbs = [ ...footSide.keys(), ...handSide.keys() ]
1498
+ const marksOf = (li: number): MarkSpan[] => {
1499
+ if (!donorMarks) return []
1500
+ const fs = footSide.get(li); if (fs) return donorMarks[fs]
1501
+ const hs = handSide.get(li); if (hs) return hs === 'l' ? donorMarks.hl : donorMarks.hr
1502
+ return []
1503
+ }
1504
+ let stairsTarget: Stairs | undefined
1505
+ // per frame, per limb, from the FK sweep: the contact goal, the effector goal, the chain root
1506
+ // (hip / shoulder), the effector's rotation, the donor's extension — the lock and the body
1507
+ // drop below are decided from these before any frame is written
1508
+ // BODY ON THE GROUND, per frame: over every mapped joint the DONOR has on its floor (within 5 cm),
1509
+ // how far the target's SAME joint sits above the donor's height over the ground + the clearance —
1510
+ // the largest is what the body must come down (comparing the two rigs' lowest joints instead
1511
+ // matched a hand at 2 cm against the spine and pulled the back onto the floor)
1512
+ const groundDemand = new Float32Array(frames).fill(-Infinity)
1513
+ const bodyClearance = opts.bodyClearance ?? 0.05
1514
+ const dGroundEarly = footSide.size ? Math.min(...[ ...footSide.keys() ].map((li) => donor.bind.worldPos[legs[li].donorContact][1])) : NaN
1515
+ const lockedJoints = new Set<number>()
1516
+ for (const l of legs) { lockedJoints.add(l.effector); lockedJoints.add(l.contact) }
1517
+ const goals = new Map<number, Vec3[]>(), effs = new Map<number, Vec3[]>(), hips = new Map<number, Vec3[]>()
1518
+ const rots = new Map<number, Quat[]>(), exts = new Map<number, number[]>(), rootRel = new Map<number, Vec3[]>()
1519
+ for (const li of limbs) { goals.set(li, []); effs.set(li, []); hips.set(li, []); rots.set(li, []); exts.set(li, []); rootRel.set(li, []) }
1520
+ // the ground a HAND is put on: the feet's (the ball's grounded height; a palm sits ~2 cm higher)
1521
+ const groundY = footSide.size ? [ ...footSide.keys() ].reduce((a, li) => a + legs[li].baseTarget, 0) / footSide.size : 0
1522
+ if (limbs.length) {
1523
+ const dTracks = new Map<number, FootTrack>(limbs.map((li) => [ li, legs[li].isFoot ? { contact: [], ankle: [] } : { contact: [], ankle: [], tip: [] } ]))
1524
+ // the donor effector's world rotation per frame (a foot's heel hangs off the ankle's frame)
1525
+ const dRots = new Map<number, Quat[]>(limbs.map((li) => [ li, [] ]))
1526
+ // the toes, per frame: both rigs' contact (ball) world rotation — a virtual toe is hung off
1527
+ // it, so a dorsiflexed toe at a toe-first landing stays up — and the real toe tips where a
1528
+ // rig has them: the target's lowest descendant of its ball as an offset from the ball (the
1529
+ // placement moves the ball, the foot keeps its FK rotation), the donor's as a height
1530
+ const dBallRots = new Map<number, Quat[]>(limbs.map((li) => [ li, [] ])), tBallRots = new Map<number, Quat[]>(limbs.map((li) => [ li, [] ]))
1531
+ const tToeOff = new Map<number, (Vec3 | null)[]>(limbs.map((li) => [ li, [] ])), dToeY = new Map<number, number[]>(limbs.map((li) => [ li, [] ]))
1532
+ const dRoot: Vec3[] = [], dHeading: number[] = []
1533
+ // the donor's pelvis (the hang rule) and its whole body (the platform's face) per frame
1534
+ const dHips: Vec3[] = [], dBody: Vec3[][] = []
1535
+ for (let f = 0; f < frames; f++) {
1536
+ const { dw } = frameFK(f)
1537
+ dRoot.push(donorRoot !== undefined ? dw.pos[donorRoot] : donorHips !== undefined ? dw.pos[donorHips] : [ 0, 0, 0 ])
1538
+ dHips.push(donorHips !== undefined ? dw.pos[donorHips] : dRoot[f])
1539
+ dBody.push(donorSkinned.map((i) => dw.pos[i]))
1540
+ dHeading.push(donorHipsForYaw !== undefined ? hipsHeading(dw.rot) : 0)
1541
+ if (Number.isFinite(dGroundEarly)) for (const [ i, sIdx ] of byTarget) {
1542
+ if (lockedJoints.has(i) || !wpos[i]) continue
1543
+ const over = dw.pos[sIdx][1] - dGroundEarly
1544
+ if (over > 0.05) continue
1545
+ const d = wpos[i][1] - (groundY + over + bodyClearance)
1546
+ if (d > groundDemand[f]) groundDemand[f] = d
1547
+ }
1548
+ for (const li of limbs) {
1549
+ const leg = legs[li]
1550
+ // the witness above the contact: a foot's ankle, a hand's forearm
1551
+ const witness = leg.isFoot ? leg.donorEffector : (byTarget.get(leg.chain[leg.chain.length - 1]) ?? leg.donorEffector)
1552
+ dTracks.get(li)!.contact.push(dw.pos[leg.donorContact]); dTracks.get(li)!.ankle.push(dw.pos[witness])
1553
+ dRots.get(li)!.push(dw.rot[leg.donorEffector])
1554
+ if (leg.isFoot) {
1555
+ dBallRots.get(li)!.push(dw.rot[leg.donorContact]); tBallRots.get(li)!.push(wrot[leg.contact])
1556
+ let tLow: Vec3 | null = null
1557
+ const st = [ ...children[leg.contact] ]
1558
+ while (st.length) { const c = st.pop()!; if (wpos[c] && (!tLow || wpos[c][1] < tLow[1])) tLow = wpos[c]; st.push(...children[c]) }
1559
+ tToeOff.get(li)!.push(tLow ? sub(tLow, wpos[leg.contact]) : null)
1560
+ let dLow = NaN
1561
+ const ds = [ ...donorChildren[leg.donorContact] ]
1562
+ while (ds.length) { const c = ds.pop()!; if (Number.isNaN(dLow) || dw.pos[c][1] < dLow) dLow = dw.pos[c][1]; ds.push(...donorChildren[c]) }
1563
+ dToeY.get(li)!.push(dLow)
1564
+ }
1565
+ if (!leg.isFoot) {
1566
+ let low = dw.pos[leg.donorEffector]
1567
+ const stack = [ ...donorChildren[leg.donorEffector] ]
1568
+ while (stack.length) { const c = stack.pop()!; if (dw.pos[c][1] < low[1]) low = dw.pos[c]; stack.push(...donorChildren[c]) }
1569
+ dTracks.get(li)!.tip!.push(low)
1570
+ }
1571
+ const g = goalOf(leg, dw)
1572
+ goals.get(li)!.push(g.contact); effs.get(li)!.push(g.effector); hips.get(li)!.push([ ...wpos[leg.chain[0]] ] as Vec3)
1573
+ rots.get(li)!.push(wrot[leg.effector])
1574
+ exts.get(li)!.push(len(sub(dw.pos[leg.donorEffector], dw.pos[leg.donorHip])) / donorLegSum(leg))
1575
+ rootRel.get(li)!.push(sub(dw.pos[leg.donorContact], dw.pos[leg.donorHip]))
1576
+ }
1577
+ }
1578
+ const feetIn: MarksInput['feet'] = {}, handsIn: NonNullable<MarksInput['hands']> = {}
1579
+ for (const [ li, sd ] of footSide) feetIn[sd] = dTracks.get(li)!
1580
+ for (const [ li, sd ] of handSide) handsIn[sd] = dTracks.get(li)!
1581
+ // the donor's ground: its bind's contact height (a platform's level 0)
1582
+ const donorGround = footSide.size ? Math.min(...[ ...footSide.keys() ].map((li) => donor.bind.worldPos[legs[li].donorContact][1])) : undefined
1583
+ dHeadingAll = dHeading
1584
+ donorMarks = detectMarks({ dt: dtM, root: dRoot, heading: dHeading, feet: feetIn, hands: handSide.size ? handsIn : undefined, stairs: levelKind === 'stairs' ? true : levelKind, floor: donorGround, hips: dHips, body: dBody })
1585
+ for (const w of donorMarks.warnings) opts.warn?.(`${clip.name}: ${w}`)
1586
+ if (process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[marks] ${describeMarks(donorMarks)}`)
1587
+ // The target's staircase is FITTED on the target's own plants — the heights the FK put the
1588
+ // donor's plants at, one vote per still window — not scaled from the donor's: the leg ratio
1589
+ // (1.17) is not what the transfer scales the vertical by (the hand-jump's platform came out
1590
+ // at 2.76 m, the scaled riser said 2.94, the tread anchors sat 19 cm too high, the lock
1591
+ // refused them and the feet stood 19 cm under the drawn tread). The donor's step index k
1592
+ // names each plant's level; the target's base + k · riser is where that level actually is.
1593
+ if (donorMarks.stairs && footSide.size) {
1594
+ const feetIdx = [ ...footSide.keys() ]
1595
+ const votes: { y: number; w: number }[] = []
1596
+ const byStep = new Map<number, number[]>()
1597
+ for (const li of feetIdx) for (const sp of donorMarks[footSide.get(li)!]) {
1598
+ if (!sp.still || sp.step === undefined) continue
1599
+ const a = Math.round(sp.still[0] / dtM), b = Math.round(sp.still[1] / dtM)
1600
+ const ys: number[] = []
1601
+ for (let i = a; i <= b; i++) ys.push(goals.get(li)![i][1])
1602
+ const med = [ ...ys ].sort((x, y) => x - y)[ys.length >> 1]
1603
+ votes.push({ y: med, w: ys.length })
1604
+ ;(byStep.get(sp.step) ?? byStep.set(sp.step, []).get(sp.step)!).push(med)
1605
+ }
1606
+ const ks = [ ...byStep.keys() ].sort((a, b) => a - b)
1607
+ let base: number | null = null, riser = 0
1608
+ if (ks.length >= 2) {
1609
+ // the riser from the plants' own levels (median height per step, least squares over k)
1610
+ const pts = ks.map((k) => ({ k, y: [ ...byStep.get(k)! ].sort((a, b) => a - b)[byStep.get(k)!.length >> 1] }))
1611
+ const n = pts.length, sk = pts.reduce((a, p) => a + p.k, 0), sy = pts.reduce((a, p) => a + p.y, 0), skk = pts.reduce((a, p) => a + p.k * p.k, 0), sky = pts.reduce((a, p) => a + p.k * p.y, 0)
1612
+ const det = n * skk - sk * sk
1613
+ if (Math.abs(det) > 1e-9) { riser = (n * sky - sk * sy) / det; base = (sy * skk - sk * sky) / det }
1614
+ }
1615
+ if (base === null || riser < 0.05) {
1616
+ // one level seen on the target: the donor's riser scaled as this clip's vertical travel was
1617
+ riser = donorMarks.stairs.riser * vScale
1618
+ const ys = votes.length ? votes.map((v) => v.y) : [ legs[feetIdx[0]].baseTarget ]
1619
+ const k0 = ks[0] ?? 0
1620
+ base = [ ...ys ].sort((a, b) => a - b)[ys.length >> 1] - k0 * riser
1621
+ // a PLATFORM whose only plants are on the ground (a hang, a drop): the ground IS level 0 —
1622
+ // a fallen body's foot voting 12 cm up must not lift the top by 12 cm
1623
+ if (levelKind === 'platform' && k0 === 0) base = groundY
1624
+ }
1625
+ // a PLATFORM's level 0 is the ground the feet stand on — pinned there (the FK's median can sit
1626
+ // a few cm off it, and a foot locked a few cm under the floor is what the eye catches first)
1627
+ if (levelKind === 'platform') { const top = base + riser; base = groundY; riser = top - base }
1628
+ stairsTarget = { riser, tread: donorMarks.stairs.tread * rootScale, base, dir: donorMarks.stairs.dir }
1629
+ if (process.env.LECODES_MARKS_TRACE) console.log(`[marks] ${clip.name}: target levels ${ks.map((k) => `k${k} ${(byStep.get(k)!.reduce((a, b) => a + b, 0) / byStep.get(k)!.length).toFixed(3)}`).join(' ')} → base ${base.toFixed(3)} riser ${riser.toFixed(3)} (donor ${donorMarks.stairs.riser.toFixed(3)})`)
1630
+ }
1631
+ if (plantOn) for (const li of limbs) {
1632
+ const leg = legs[li]
1633
+ const g = goals.get(li)!, rr = rots.get(li)!
1634
+ const L = donorLegLen(leg)
1635
+ // a foot locks through its STILL windows; a hand through its whole TOUCH (a cartwheel's hand
1636
+ // is down for three frames — there is no still window inside that, and no roll either)
1637
+ // the donor's own contact height over the span, against the level it is on (a hand hangs
1638
+ // 15 cm under a ledge's edge, rests 1–2 cm over a top or a floor): the OFFSET transfers
1639
+ const dTrack = dTracks.get(li)!.contact
1640
+ // the clip's own floor (the lowest a foot got): the level a HAND's offset is measured from
1641
+ const donorFloor = Math.min(...[ ...footSide.keys() ].map((fi) => Math.min(...dTracks.get(fi)!.contact.map((p) => p[1]))))
1642
+ const donorLevel = (step: number | undefined): number => (donorMarks!.stairs && step !== undefined && step >= 0 ? donorMarks!.stairs.base + step * donorMarks!.stairs.riser : donorFloor)
1643
+ // a FOOT's level off a staircase is the donor's BIND ground (the ball's bind height — what
1644
+ // `baseTarget` is on the target), never the clip's lowest: a take whose feet never come
1645
+ // near the ground (a hang) would read as "on the floor", and an identity transfer must
1646
+ // measure both rigs from the same rest height to come out exact
1647
+ const donorFootLevel = (step: number | undefined): number => (donorMarks!.stairs && step !== undefined && step >= 0 ? donorMarks!.stairs.base + step * donorMarks!.stairs.riser : dGroundEarly)
1648
+ const wins = marksOf(li).filter((sp) => leg.isFoot ? !!sp.still : true).map((sp) => {
1649
+ const a = Math.round(sp.from / dtM), b = Math.min(frames - 1, Math.round(sp.to / dtM))
1650
+ // a foot's height over its level is read inside the STILL window — the touch includes the
1651
+ // roll in and out, where the ball is up on a heel strike
1652
+ const [ oa, ob ] = leg.isFoot && sp.still ? [ Math.round(sp.still[0] / dtM), Math.min(frames - 1, Math.round(sp.still[1] / dtM)) ] : [ a, b ]
1653
+ const ys: number[] = []
1654
+ for (let i = oa; i <= ob; i++) ys.push(dTrack[i][1])
1655
+ const donorY = ys.length ? [ ...ys ].sort((x, y) => x - y)[ys.length >> 1] : 0
1656
+ // a hand that TRAVELS through its contact (over 8 cm from where it landed) is sliding /
1657
+ // rolling — it gets the donor's height profile over the level, not a fixed point
1658
+ let slide = 0
1659
+ if (!leg.isFoot) for (let i = a; i <= b; i++) slide = Math.max(slide, Math.hypot(dTrack[i][0] - dTrack[a][0], dTrack[i][2] - dTrack[a][2]))
1660
+ return {
1661
+ start: Math.round((leg.isFoot ? sp.still![0] : sp.from) / dtM),
1662
+ end: Math.min(frames, Math.round((leg.isFoot ? sp.still![1] : sp.to) / dtM) + 1),
1663
+ touchStart: a, touchEnd: Math.min(frames, b + 1), step: sp.step,
1664
+ offset: Number.isFinite(donorFloor) ? Math.max(-0.3, Math.min(0.25, donorY - donorLevel(sp.step))) : 0.02,
1665
+ heightProfile: !leg.isFoot && slide > 0.08 && Number.isFinite(donorFloor) ? Array.from({ length: frames }, (_, i) => dTrack[Math.max(a, Math.min(b, i))][1] - donorLevel(sp.step)) : null,
1666
+ }
1667
+ })
1668
+ // HEIGHT through the whole TOUCH (marks.ts `from`..`to`). Outside the still window the foot
1669
+ // rolls — the heel strike, the toe-off — and the lock is off, but the donor's foot is ON the
1670
+ // ground there and the target's must be too: a walk's heel strike landed 5 cm up and only
1671
+ // came down where the still window's lock faded in. Per frame the foot's CLEARANCE — the
1672
+ // lower of its heel and its ball over where each rests when the foot lies flat (the heel is
1673
+ // virtual: the ankle's bind height hung under the ankle in the ankle's frame — a mannequin
1674
+ // has no heel bone) — is matched to the donor's, scaled: the whole foot shifts vertically,
1675
+ // x/z and rotation stay the FK's. The target's own toes beyond the ball are not compared
1676
+ // (the donor may have none); the floor pitch in pass 2 keeps them out of the ground.
1677
+ // The track holds the ball's ABSOLUTE height (pass-1 goal + the shift): pass 2 lowers the
1678
+ // whole body first (`dropCurve`) and a shift relative to the dropped FK would lower the
1679
+ // foot twice. The toe is virtual on BOTH rigs — the ball pushed along the foot's forward
1680
+ // axis (in the ankle's frame, so it dips with the pitch like a real toe) by the toe's
1681
+ // length: a foot up on its toes has its TIP on the ground, and a mannequin's ball 4 cm up
1682
+ // means its (unmodelled) toe cap is down, not that the foot floats.
1683
+ if (leg.isFoot) {
1684
+ const track = new Float32Array(frames).fill(NaN)
1685
+ const heelT = qrot(qinv(target.bind.worldRot[leg.effector]), [ 0, -(target.bind.worldPos[leg.effector][1] - lowestBind), 0 ])
1686
+ const heelD = qrot(qinv(donor.bind.worldRot[leg.donorEffector]), [ 0, -(donor.bind.worldPos[leg.donorEffector][1] - donorLowestBind), 0 ])
1687
+ const toeReach = (kids: number[][], ball: number, pos: Vec3[]): number => {
1688
+ let r = 0
1689
+ const st = [ ...kids[ball] ]
1690
+ while (st.length) { const c = st.pop()!; r = Math.max(r, Math.hypot(pos[c][0] - pos[ball][0], pos[c][2] - pos[ball][2])); st.push(...kids[c]) }
1691
+ return r
1692
+ }
1693
+ const realT = toeReach(children, leg.contact, target.bind.worldPos), realD = toeReach(donorChildren, leg.donorContact, donor.bind.worldPos)
1694
+ const toeLenT = realT > 0 ? realT : realD * leg.ratio, toeLenD = realD > 0 ? realD : realT / leg.ratio
1695
+ // a virtual toe: the foot's forward (the ankle→ball line, flattened) in the BALL's bind
1696
+ // frame — the ball's rotation carries the toes' flex, so a toe-first landing with the toes
1697
+ // bent up keeps its tip up instead of burying a rigid one 4 cm in the floor
1698
+ const fwdOf = (rig: Rig, ankle: number, ball: number): Vec3 | null => {
1699
+ const v = sub(rig.bind.worldPos[ball], rig.bind.worldPos[ankle])
1700
+ const h = unit([ v[0], 0, v[2] ])
1701
+ return h ? qrot(qinv(rig.bind.worldRot[ball]), h) : null
1702
+ }
1703
+ const fwdT = fwdOf(target, leg.effector, leg.contact), fwdD = fwdOf(donor, leg.donorEffector, leg.donorContact)
1704
+ const dAnkle = dTracks.get(li)!.ankle, dRot = dRots.get(li)!, ef = effs.get(li)!
1705
+ const dBall = dBallRots.get(li)!, tBall = tBallRots.get(li)!, tOff = tToeOff.get(li)!, dToe = dToeY.get(li)!
1706
+ // On flat ground the clearance is matched on EVERY frame, swing included: the body drop
1707
+ // below moves the swing foot down with the hips, and a toe-off 4 cm lower than the
1708
+ // donor's had the toe pinned on the floor by the floor pitch. On stairs only the touch
1709
+ // spans are judged (a foot between two treads has no one level to be measured against).
1710
+ const spans = marksOf(li).map((sp) => ({ a: Math.round(sp.from / dtM), b: Math.min(frames - 1, Math.round(sp.to / dtM)), step: sp.step }))
1711
+ const onLevels = !!donorMarks!.stairs
1712
+ for (let i = 0; i < frames; i++) {
1713
+ const sp = spans.find((s) => i >= s.a && i <= s.b)
1714
+ if (!sp && onLevels) continue
1715
+ const levelD = donorFootLevel(sp?.step)
1716
+ const levelT = stairsTarget && sp?.step !== undefined && sp.step >= 0 ? stairsTarget.base + sp.step * stairsTarget.riser : leg.baseTarget
1717
+ if (!Number.isFinite(levelD)) continue
1718
+ {
1719
+ const dToeAt = !Number.isNaN(dToe[i]) ? dToe[i] : fwdD && toeLenD > 0 ? dTrack[i][1] + qrot(dBall[i], fwdD)[1] * toeLenD : Infinity
1720
+ const tToeAt = tOff[i] ? g[i][1] + tOff[i]![1] : fwdT && toeLenT > 0 ? g[i][1] + qrot(tBall[i], fwdT)[1] * toeLenT : Infinity
1721
+ const want = Math.min(dAnkle[i][1] + qrot(dRot[i], heelD)[1], dTrack[i][1], dToeAt) - levelD
1722
+ const have = Math.min(ef[i][1] + qrot(rr[i], heelT)[1], g[i][1], tToeAt) - levelT
1723
+ // (a donor foot under its own floor carries over, scaled — identity stays exact)
1724
+ const dy = want * leg.ratio - have
1725
+ // a shift past 15 % of the leg is a transfer gone wrong, not a foot to lower
1726
+ if (Math.abs(dy) <= 0.15 * L) track[i] = g[i][1] + dy
1727
+ }
1728
+ }
1729
+ heightTracks.set(li, track)
1730
+ if (process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[height] ${leg.name} ${Array.from(track).map((v, i) => Number.isNaN(v) ? '' : `${i}:${((v - g[i][1]) * 1000).toFixed(0)}`).filter(Boolean).join(' ')}`)
1731
+ }
1732
+ // The last frame of a cycle repeats its first: a window touching both ends is ONE plant
1733
+ // across the seam — held at one place modulo the clip's travel, no fade on the seam side.
1734
+ const rel = rootRel.get(li)!
1735
+ const cyclic = len(sub(rel[0], rel[frames - 1])) < 0.02
1736
+ const head = wins[0], tail = wins[wins.length - 1]
1737
+ const seam = cyclic && wins.length >= 2 && head.start === 0 && tail.end === frames
1738
+ const travel = sub(g[frames - 1], g[0])
1739
+ // the anchor's height: the tread on a fitted staircase; a foot: the target's ground when the
1740
+ // FK put it near it, else where the FK put it (a platform, a stair clip whose staircase did
1741
+ // not fit); a hand: the feet's ground — the FK hand is wherever the arm's proportions left
1742
+ // it, which is the very thing being corrected (24 cm up on the cartwheel)
1743
+ const yOf = (step: number | undefined, fkY: number, offset = 0.02): number => {
1744
+ if (leg.isFoot) {
1745
+ // the target's level — its ground, its tread on a fitted staircase — plus the donor's own
1746
+ // height over ITS level through the window, scaled: a foot the donor holds on the floor
1747
+ // lands on the floor, a foot it dangles 23 cm up (a hang) dangles. Never the FK's
1748
+ // height: the FK is the very thing being corrected (a punch's lunge sat both feet 7 cm
1749
+ // up, and "keep the FK beyond 6 cm" locked them there, in the air)
1750
+ const level = stairsTarget && step !== undefined ? stairsTarget.base + step * stairsTarget.riser : leg.baseTarget
1751
+ return level + Math.max(0, offset) * leg.ratio
1752
+ }
1753
+ // a hand: the level it is on (a platform's top, a tread, the floor) plus the donor's own
1754
+ // offset from that level — a palm rests 1–2 cm over a top, a grip hangs 15 cm under an
1755
+ // edge — so a hang on a rig whose arms reach less far above its hips (Kachujin's hands
1756
+ // hung 50 cm under the ledge) is pulled to the ledge and the body follows up to it
1757
+ const level = stairsTarget && step !== undefined && step >= 0 ? stairsTarget.base + step * stairsTarget.riser : footSide.size ? groundY : null
1758
+ return level === null ? fkY : level + offset
1759
+ }
1760
+ const meanOf = (list: { start: number; end: number; shift: Vec3 }[]): Vec3 => {
1761
+ const acc: Vec3 = [ 0, 0, 0 ]
1762
+ let n = 0
1763
+ for (const r of list) for (let i = r.start; i < r.end; i++) { const p = add(g[i], r.shift); acc[0] += p[0]; acc[1] += p[1]; acc[2] += p[2]; n++ }
1764
+ return scl(acc, 1 / Math.max(1, n))
1765
+ }
1766
+ // a limb the FK puts more than 15 % of its length from its anchor was never planted there —
1767
+ // a transfer gone wrong (a parkour take's hips two metres off), not a lock's business. A hand
1768
+ // is judged horizontally only: its vertical miss is the correction itself.
1769
+ const sane = (r: { start: number; end: number }, anchor: Vec3): boolean => {
1770
+ let worst = 0
1771
+ for (let i = r.start; i < r.end; i++) {
1772
+ const d = leg.isFoot ? len(sub(g[i], anchor)) : Math.hypot(g[i][0] - anchor[0], g[i][2] - anchor[2])
1773
+ worst = Math.max(worst, d)
1774
+ }
1775
+ // a HAND's FK sweeps through its contact while the body passes over it — the arm reaches
1776
+ // 3 % further, the body travels 17 % further — and that sweep IS what the lock corrects:
1777
+ // a cartwheel's hand swept 20 cm (18 at the old, wrong, root scale), so half the arm
1778
+ const limit = (leg.isFoot ? 0.15 : 0.5) * L
1779
+ const ok = worst <= limit
1780
+ if (!ok && process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[sane] ${leg.name} f ${r.start}-${r.end}: FK ${(worst * 1000).toFixed(0)}mm from the anchor (limit ${(limit * 1000).toFixed(0)}mm) — no lock`)
1781
+ return ok
1782
+ }
1783
+ const rotAt = (r: { start: number; end: number }): Quat => rr[Math.min(frames - 1, (r.start + r.end) >> 1)]
1784
+ /** A foot's anchor height: the median of its height track through the window — one rule for
1785
+ * the roll and the stand — falling back to the level + the donor's offset. */
1786
+ const yWin = (r: { start: number; end: number; step?: number; offset: number }, fkY: number): number => {
1787
+ const tr = heightTracks.get(li)
1788
+ const ys: number[] = []
1789
+ if (tr) for (let i = r.start; i < r.end; i++) if (!Number.isNaN(tr[i])) ys.push(tr[i])
1790
+ return ys.length ? [ ...ys ].sort((x, y) => x - y)[ys.length >> 1] : yOf(r.step, fkY, r.offset)
1791
+ }
1792
+ for (const r of wins) {
1793
+ if (seam && (r === head || r === tail)) continue
1794
+ const m = meanOf([ { ...r, shift: [ 0, 0, 0 ] } ])
1795
+ const anchor: Vec3 = [ m[0], leg.isFoot ? yWin(r, m[1]) : yOf(r.step, m[1], r.offset), m[2] ]
1796
+ // a window at the clip's edge has nothing to fade from: the clip begins / ends planted
1797
+ if (r.heightProfile) {
1798
+ const level = stairsTarget && r.step !== undefined && r.step >= 0 ? stairsTarget.base + r.step * stairsTarget.riser : groundY
1799
+ windows.push({ leg: li, start: r.start, end: r.end, touchStart: r.touchStart, touchEnd: r.touchEnd, anchor, rot: rotAt(r), step: r.step, openStart: r.start === 0, openEnd: r.end === frames, yTrack: Float32Array.from(r.heightProfile.map((h) => level + h)) })
1800
+ } else if (sane(r, anchor)) windows.push({ leg: li, start: r.start, end: r.end, touchStart: r.touchStart, touchEnd: r.touchEnd, anchor, rot: rotAt(r), step: r.step, openStart: r.start === 0, openEnd: r.end === frames })
1801
+ }
1802
+ if (seam) {
1803
+ const m = meanOf([ { ...head, shift: [ 0, 0, 0 ] }, { ...tail, shift: scl(travel, -1) } ])
1804
+ const anchor: Vec3 = [ m[0], leg.isFoot ? yWin(head, m[1]) : yOf(head.step, m[1], head.offset), m[2] ]
1805
+ const tailAnchor: Vec3 = [ anchor[0] + travel[0], stairsTarget ? (leg.isFoot ? yWin(tail, m[1] + travel[1]) : yOf(tail.step, m[1] + travel[1], tail.offset)) : anchor[1], anchor[2] + travel[2] ]
1806
+ if (sane(head, anchor) && sane(tail, tailAnchor)) {
1807
+ windows.push({ leg: li, start: head.start, end: head.end, touchStart: head.touchStart, touchEnd: head.touchEnd, anchor, rot: rotAt(head), step: head.step, openStart: true })
1808
+ windows.push({ leg: li, start: tail.start, end: tail.end, touchStart: tail.touchStart, touchEnd: tail.touchEnd, anchor: tailAnchor, rot: rotAt(head), step: tail.step, openEnd: true })
1809
+ }
1810
+ }
1811
+ }
1812
+ }
1813
+ /** The lock on `leg` at frame `f`: the contact's anchor, the limb end's held rotation and the blend
1814
+ * weight — 1 through the whole window (that is the promise: the toe does not move), fading in
1815
+ * over the PLANT_FADE frames BEFORE it and out over the frames AFTER it, i.e. through the
1816
+ * heel-strike roll-in and the heel-rise roll-out, where the foot moves anyway, and only while the
1817
+ * marks say the toe is still TOUCHING: past the touch the foot is in the air. */
1818
+ /** the contact bone's offset in the effector's frame — zero when the contact IS the effector (a hand) */
1819
+ const contactOff = (leg: Leg): Vec3 => (leg.contact === leg.effector ? [ 0, 0, 0 ] : localOff[leg.contact])
1820
+ /** A FADING lock (w < 1) engages as the foot ARRIVES: full share within 1 cm of the anchor, none
1821
+ * beyond 5 cm. Pulling a heel-strike foot still 6 cm up (the walk's f9: 65 mm) pushed the near-
1822
+ * straight landing leg past its reach and snapped the knee. Inside the window (w = 1) the miss
1823
+ * IS the transfer error and gets corrected in full. */
1824
+ const proximity = (pl: { anchor: Vec3; w: number }, ball: Vec3, isFoot = true): number =>
1825
+ pl.w >= 1 || !isFoot ? 1 : Math.max(0, Math.min(1, 1 - (len(sub(ball, pl.anchor)) - 0.01) / 0.04))
1826
+ /** The constraint on `leg` at frame `f`, given the FK's contact `fk` and effector rotation: the
1827
+ * positional lock (its window, faded, proximity-gated for a foot) composed over the foot's
1828
+ * HEIGHT track — the lock takes its share `w`, the track the rest, so a heel strike lands on
1829
+ * the ground first and rolls flat into the lock. A foot in a touch span always returns w = 1
1830
+ * (its height is a promise too); a limb with a lock alone returns the lock's weight. */
1831
+ const plantAt = (li: number, f: number, fk?: Vec3, fkRot?: Quat): { anchor: Vec3; rot: Quat; w: number; heightOnly?: boolean; lockShare?: number } | null => {
1832
+ let best: { anchor: Vec3; rot: Quat; w: number; heightOnly?: boolean; lockShare?: number } | null = null
1833
+ for (const s of windows) {
1834
+ if (s.leg !== li) continue
1835
+ let w = 0
1836
+ if (f >= s.start && f < s.end) w = 1
1837
+ // (a HAND fades over the frames beyond its touch — there is no roll inside a three-frame
1838
+ // contact to fade through, and a hard release popped the hand 20 cm up in one frame)
1839
+ else if (!s.openStart && f < s.start && f >= (legs[s.leg].isFoot ? Math.max(s.touchStart, s.start - PLANT_FADE) : s.start - PLANT_FADE)) w = (f - (s.start - PLANT_FADE) + 1) / (PLANT_FADE + 1)
1840
+ else if (!s.openEnd && f >= s.end && f < (legs[s.leg].isFoot ? Math.min(s.touchEnd, s.end + PLANT_FADE) : s.end + PLANT_FADE)) w = (s.end + PLANT_FADE - f) / (PLANT_FADE + 1)
1841
+ if (w > 0 && (!best || w > best.w)) best = s.yTrack && fk
1842
+ ? { anchor: [ fk[0], s.yTrack[Math.max(0, Math.min(frames - 1, f))], fk[2] ], rot: fkRot ?? s.rot, w, heightOnly: true }
1843
+ : { anchor: s.anchor, rot: s.rot, w }
1844
+ }
1845
+ const isFoot = legs[li].isFoot
1846
+ if (best && fk) { best = { ...best, w: best.w * proximity(best, fk, isFoot) }; if (best.w <= 0) best = null }
1847
+ const hy = fk ? heightTracks.get(li)?.[f] ?? NaN : NaN
1848
+ if (Number.isNaN(hy)) return best
1849
+ const hAnchor: Vec3 = [ fk![0], hy, fk![2] ]
1850
+ const hRot = fkRot ?? best?.rot
1851
+ if (!best || !hRot) return best && !hRot ? best : { anchor: hAnchor, rot: hRot!, w: 1, heightOnly: true, lockShare: 0 }
1852
+ if (best.w >= 1) return best
1853
+ const w = best.w
1854
+ return { anchor: [ 0, 1, 2 ].map((i) => hAnchor[i] + (best!.anchor[i] - hAnchor[i]) * w) as Vec3, rot: qnorm(qslerp(hRot, best.rot, w)), w: 1, lockShare: w }
1855
+ }
1856
+ for (const s of windows) report.legs[s.leg].planted += s.end - s.start
1857
+ /** Is `leg` inside one of its TOUCH spans at frame `f`? */
1858
+ const touching = (li: number, f: number): boolean => marksOf(li).some((sp) => f >= Math.round(sp.from / dtM) && f <= Math.round(sp.to / dtM))
1859
+
1860
+ // ---- the BODY follows the locked limbs: the whole clip's vertical correction, two passes -------
1861
+ // The reference pose is a straight-leg bind and the transfer lands the hips a few millimetres too
1862
+ // high for this leg length — nothing while a foot floats with them, but a locked foot leaves the
1863
+ // knee to absorb it exactly where a two-bone leg cannot (a centimetre short of full length is 20°
1864
+ // of knee: Fidget V1 flicked 0–9° while the donor held 7–10°). And a locked HAND on a rig whose
1865
+ // arms are short for its legs needs the whole body down by decimetres (the cartwheel's hands hung
1866
+ // 24 cm above the floor). So the donor's EXTENSION is the authority: for every locked limb the
1867
+ // chain root must sit at the donor's fraction of the limb's length from the held end; the largest
1868
+ // demand of any frame wins (no limb straighter than the donor's, the others bend a little more)
1869
+ // — capped per limb (feet 2 cm, hands 40 cm; a demand past the cap reduces that lock's weight
1870
+ // instead: the limb is let go, not snapped straight) — and the curve is DILATED at DROP_SLOPE per
1871
+ // frame, so the body descends ahead of a contact and rises after it and is exactly where each
1872
+ // frame asked for when it gets there. A demand to RISE (hips too low) is honoured only where it
1873
+ // was asked, within 2 cm.
1874
+ const wScaleAt = new Map<number, Float32Array>()
1875
+ const dropCurve = new Float32Array(frames)
1876
+ {
1877
+ // per limb, per frame: what the body would have to drop for this lock alone
1878
+ const need = new Map<number, Float64Array>()
1879
+ const demand = new Float64Array(frames).fill(-Infinity)
1880
+ const footDemand = new Float64Array(frames).fill(-Infinity) // the feet's alone
1881
+ for (const li of limbs) {
1882
+ const leg = legs[li]
1883
+ if (leg.isFoot && leg.refReach <= 0) continue
1884
+ const total = leg.lengths.reduce((x, y) => x + y, 0)
1885
+ const dyOf = new Float64Array(frames).fill(NaN)
1886
+ for (let f = 0; f < frames; f++) {
1887
+ const pl = plantAt(li, f, goals.get(li)![f], rots.get(li)![f])
1888
+ if (!pl) continue
1889
+ // a swing foot held at its clearance asks nothing of the body: the leg bends for it
1890
+ if (pl.lockShare === 0 && leg.isFoot && !touching(li, f)) continue
1891
+ const w = pl.w
1892
+ if (w <= 0) continue
1893
+ const want = Math.min(total, exts.get(li)![f] * total)
1894
+ const rotHeld = qnorm(qslerp(rots.get(li)![f], pl.rot, w))
1895
+ const locked = sub(pl.anchor, qrot(rotHeld, contactOff(leg)))
1896
+ const fk = effs.get(li)![f]
1897
+ // lowering the body by d moves only the locked share of the goal closer: solve for d in
1898
+ // |(1 − w)·a + w·(b − d·ŷ)| = want, i.e. the demand divided by w
1899
+ const ankle: Vec3 = [ 0, 1, 2 ].map((i) => fk[i] + (locked[i] - fk[i]) * w) as Vec3
1900
+ const v = sub(hips.get(li)![f], ankle)
1901
+ const xz2 = v[0] * v[0] + v[2] * v[2]
1902
+ if (want * want <= xz2) continue
1903
+ // a HAND moves the body only when its anchor is OUT OF REACH (a cartwheel's floor, a ledge
1904
+ // above the hands): within reach the arm bends and that is all — a foot's "no straighter
1905
+ // than the donor" rule made a hang from a ledge drop the body 18 cm to straighten the arms
1906
+ if (!leg.isFoot && len(v) <= want) continue
1907
+ // the chain root above its end (a foot under a hip): lower the body so the root sits `want`
1908
+ // above the end; the root BELOW its end (a hand overhead, the body hanging): the other root
1909
+ const r = Math.sqrt(want * want - xz2)
1910
+ let dy = (v[1] >= 0 ? v[1] - r : v[1] + r) / Math.max(0.05, w)
1911
+ // a RAISE (the limb more bent than the donor's) is a positional lock's to ask, by its
1912
+ // share: a foot merely held at its height through a touch does not lift the body — a
1913
+ // toe-first landing 4 cm short of the donor's extension popped the pelvis 2 cm in a frame
1914
+ if (dy < 0) dy *= pl.lockShare ?? 1
1915
+ dyOf[f] = dy
1916
+ if (dy > demand[f]) demand[f] = dy
1917
+ if (leg.isFoot && dy > footDemand[f]) footDemand[f] = dy
1918
+ if (process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[hips] f ${f} ${leg.name} w ${w.toFixed(2)} ext ${exts.get(li)![f].toFixed(3)} want ${(want * 1000).toFixed(0)} total ${(total * 1000).toFixed(0)} xz ${(Math.sqrt(xz2) * 1000).toFixed(0)} vy ${(v[1] * 1000).toFixed(0)} dy ${(dy * 1000).toFixed(1)}`)
1919
+ }
1920
+ need.set(li, dyOf)
1921
+ }
1922
+ // The BODY ON THE GROUND: where the donor's lowest joint lies on its floor (a roll on the back,
1923
+ // a knock-down, getting up) and no foot is planted, the target's lowest joint is brought to the
1924
+ // same height over its ground — the transfer scales the body's arc by the legs (17 %) and the
1925
+ // torso is not 17 % thicker, so a roll floated (owner: "he rolls higher than he should").
1926
+ for (let f = 0; f < frames; f++) if (groundDemand[f] > 0.01 && groundDemand[f] > demand[f]) demand[f] = Math.min(0.4, groundDemand[f])
1927
+ // The body may drop DECIMETRES only while no foot is on the ground (a cartwheel's flight); with
1928
+ // a foot touching it may drop the feet's 2 cm, else the character crouches 30 cm into the run-up
1929
+ // (measured: the feet raised 9–22 cm by the floor clamp for 15 frames before the hands landed).
1930
+ const feetDown = new Uint8Array(frames), feetStill = new Uint8Array(frames)
1931
+ for (const li of footSide.keys()) for (let f = 0; f < frames; f++) if (touching(li, f)) feetDown[f] = 1
1932
+ for (const wn of windows) if (legs[wn.leg].isFoot) for (let f = wn.start; f < wn.end; f++) feetStill[f] = 1
1933
+ // a foot STILL (bearing weight): the feet's 2 cm; a foot touching but rolling (a push-off — the
1934
+ // knee may bend, the toe stays on the floor): 10 cm; in the air: 40 cm. NEAR A HAND CONTACT the
1935
+ // feet's caps open up to 25 cm, decaying at 3 cm per frame away from it: a cartwheel is pushed
1936
+ // off from a bent leg and landed into one, so the body dips through the push-off before the
1937
+ // hands land and stays low through the landing after they leave (measured before this: the
1938
+ // first hand landed 5 frames late and left 3 early, hovering 10 cm over the floor both times).
1939
+ const handNear = new Float64Array(frames) // the allowance a hand contact grants each frame
1940
+ for (const wn of windows) {
1941
+ if (legs[wn.leg].isFoot) continue
1942
+ for (let f = 0; f < frames; f++) {
1943
+ const d = f < wn.start ? wn.start - f : f >= wn.end ? f - wn.end + 1 : 0
1944
+ handNear[f] = Math.max(handNear[f], Math.max(0, 0.25 - 0.03 * d))
1945
+ }
1946
+ }
1947
+ // A FOOT's own demand — the body down until the leg reaches its anchor at the donor's
1948
+ // extension — may take it FOOT_CAP whatever the caps above say: that demand IS the transfer's
1949
+ // vertical error under a foot that is on the ground (a punch's lunge sat the hips 6 cm high on
1950
+ // Kachujin, her pelvis rides 6 cm over her hip joints where the mannequin's rides 1.4), and
1951
+ // held at 2 cm the lock was let go instead and the foot hovered. A hand's decimetres stay capped.
1952
+ const FOOT_CAP = 0.1
1953
+ const capAt = (f: number): number => Math.max(feetStill[f] ? 0.02 : feetDown[f] ? 0.1 : 0.4, handNear[f], footDemand[f] > 0 ? Math.min(FOOT_CAP, footDemand[f]) : 0)
1954
+ const base = new Float64Array(frames)
1955
+ // a demand to RAISE the body: the feet's 2 cm while a foot is down, else the hands may carry
1956
+ // it (a hang from a ledge, a pull-up) within 40 cm
1957
+ for (let f = 0; f < frames; f++) base[f] = demand[f] > -Infinity ? Math.max(feetDown[f] ? -0.02 : -0.4, Math.min(capAt(f), demand[f])) : 0
1958
+ // DILATION at DROP_SLOPE per frame: the body descends ahead of a contact and rises after it
1959
+ // (positive demands only; a demand to rise is honoured where it was asked)…
1960
+ const DROP_SLOPE = 0.04
1961
+ const dil = new Float64Array(frames)
1962
+ for (let f = 0; f < frames; f++) {
1963
+ let down = 0
1964
+ for (let g = 0; g < frames; g++) if (base[g] > 0) down = Math.max(down, base[g] - DROP_SLOPE * Math.abs(f - g))
1965
+ dil[f] = down > 0 ? down : base[f] // a raise is honoured where it was asked; the slope passes below smooth it
1966
+ }
1967
+ // …never past a frame's cap (the feet's while they are down), and never STEEPER than the slope
1968
+ // across the cap's boundary: a 2 cm → 24 cm jump the frame the last foot leaves is a pop; the
1969
+ // body dips at the slope from the lift-off on, and a hand whose full drop is not reached by its
1970
+ // landing gets its lock weight reduced to what was (it hovers a little, then settles)
1971
+ // (with a foot down the body moves at the feet's pace — 3 mm per frame — a pelvis that follows
1972
+ // every jump of a fading lock's demand is a knee that jitters)
1973
+ const slopeAt = (f: number): number => (handNear[f] > 0.02 ? 0.03 : feetStill[f] ? 0.003 : feetDown[f] ? 0.02 : DROP_SLOPE)
1974
+ for (let f = 0; f < frames; f++) dil[f] = Math.min(dil[f], capAt(f))
1975
+ // (a RAISE next door does not pull a drop down through the slope: two feet disagreeing — one
1976
+ // leg 2 cm long for the hips, the other 2 cm short — is settled for the short one, whose
1977
+ // foot would float; the long one's knee bends a degree more, which nobody sees. The max
1978
+ // passes below then decay the drop into the raise's frames at the slope instead of a pop.)
1979
+ for (let f = 1; f < frames; f++) dil[f] = Math.min(dil[f], Math.max(0, dil[f - 1]) + slopeAt(f))
1980
+ for (let f = frames - 2; f >= 0; f--) dil[f] = Math.min(dil[f], Math.max(0, dil[f + 1]) + slopeAt(f))
1981
+ for (let f = 1; f < frames; f++) dil[f] = Math.max(dil[f], dil[f - 1] - slopeAt(f))
1982
+ for (let f = frames - 2; f >= 0; f--) dil[f] = Math.max(dil[f], dil[f + 1] - slopeAt(f))
1983
+ for (let f = 0; f < frames; f++) dil[f] = Math.min(dil[f], capAt(f))
1984
+ for (let f = 0; f < frames; f++) dropCurve[f] = dil[f]
1985
+ if (process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[drop] ${Array.from(dropCurve).map((v, i) => `${i}:${(v * 100).toFixed(0)}`).join(' ')}`)
1986
+ for (const [ li, dyOf ] of need) {
1987
+ const ws = new Float32Array(frames).fill(1)
1988
+ for (let f = 0; f < frames; f++) { const dy = dyOf[f]; if (!Number.isNaN(dy) && dy > 0.001 && dropCurve[f] < dy) ws[f] = Math.max(0, dropCurve[f]) / dy }
1989
+ wScaleAt.set(li, ws)
1990
+ }
1991
+ }
1992
+
1993
+ // Pass 2 — the frames.
1994
+ for (let f = 0; f < frames; f++) {
1995
+ const time = timeOf(f)
1996
+ const { dw } = frameFK(f)
1997
+ const active = new Set<string>()
1998
+ if (f === 0) rootFirst = [ ...rootXZ() ]
1999
+ if (f === frames - 1) rootLast = [ ...rootXZ() ]
2000
+
2001
+ if (legs.length) {
2002
+ // the body's vertical correction for this frame (pass 1, `dropCurve`): everything moves
2003
+ {
2004
+ const dy = dropCurve[f]
2005
+ if (dy !== 0) for (let i = 0; i < wpos.length; i++) if (wpos[i]) wpos[i] = [ wpos[i][0], wpos[i][1] - dy, wpos[i][2] ]
2006
+ }
2007
+ for (const [ li, leg ] of legs.entries()) {
2008
+ const g = goalOf(leg, dw)
2009
+ let goal = g.effector
2010
+ let pl = plantAt(li, f, g.contact, wrot[leg.effector])
2011
+ if (pl) { pl.w *= wScaleAt.get(li)?.[f] ?? 1; if (pl.w <= 0) pl = null }
2012
+ // a lock-only hand is FK unless locked
2013
+ if (leg.lockOnly && !pl) continue
2014
+ // a foot in a TOUCH span but not locked never goes UNDER its ground (the body drop for a
2015
+ // hand contact would otherwise push the trailing foot through the floor)
2016
+ const clampBy = leg.isFoot && !pl && g.contact[1] < leg.baseTarget - 0.005 ? leg.baseTarget - g.contact[1] : 0
2017
+ if (clampBy > 0) goal = [ goal[0], goal[1] + clampBy, goal[2] ]
2018
+ if (pl) {
2019
+ // the lock: the foot at its held rotation, the ball at the anchor, the ankle from the two —
2020
+ // and never past the leg's REACH: a share of the pull that would straighten the leg beyond
2021
+ // 99.8 % of its length is cut back until the blended goal is reachable (a snapped-straight
2022
+ // knee is worse than a foot a few millimetres off its anchor for a frame)
2023
+ const rotFK = wrot[leg.effector]
2024
+ const total = leg.lengths.reduce((x, y) => x + y, 0)
2025
+ const hip = wpos[leg.chain[0]]
2026
+ const blend = (w: number): { rot: Quat; goal: Vec3 } => {
2027
+ const rot = qnorm(qslerp(rotFK, pl!.rot, w))
2028
+ const ankle = sub(pl!.anchor, qrot(rot, contactOff(leg)))
2029
+ return { rot, goal: [ 0, 1, 2 ].map((i) => g.effector[i] + (ankle[i] - g.effector[i]) * w) as Vec3 }
2030
+ }
2031
+ let b = blend(pl.w)
2032
+ if (len(sub(b.goal, hip)) > 0.998 * total && len(sub(g.effector, hip)) <= 0.998 * total) {
2033
+ let lo = 0, hi = pl.w
2034
+ for (let it = 0; it < 6; it++) { const mid = (lo + hi) / 2; if (len(sub(blend(mid).goal, hip)) > 0.998 * total) hi = mid; else lo = mid }
2035
+ pl.w = lo; b = blend(lo)
2036
+ }
2037
+ wrot[leg.effector] = b.rot
2038
+ goal = b.goal
2039
+ }
2040
+ // Out of reach: what the LEG cannot do. The two feet differing in shape (the target's ball
2041
+ // elsewhere under its ankle than the donor's, scaled) costs a few cm at full extension and
2042
+ // is nobody's fault — the ball lands short by that much, silently.
2043
+ const slack = len(sub(scl(sub(dw.pos[leg.donorContact], dw.pos[leg.donorEffector]), leg.contactRatio), sub(wpos[leg.contact], wpos[leg.effector])))
2044
+ const short = placeLeg(leg, goal, wrot, wpos, bendOf(li, leg, dw)) - slack
2045
+ if (process.env.LECODES_PLANT_TRACE === clip.name && clampBy > 0) console.log(`[floor] f ${f} ${leg.name} raised ${(clampBy * 1000).toFixed(0)}mm → ball y ${(wpos[leg.contact][1] * 1000).toFixed(0)} (ground ${(leg.baseTarget * 1000).toFixed(0)}) short ${(short * 1000).toFixed(0)}mm drop ${(dropCurve[f] * 1000).toFixed(0)}`)
2046
+ if (process.env.LECODES_PLANT_TRACE === clip.name && leg.chain.length >= 2) { const u = sub(wpos[leg.chain[0]], wpos[leg.chain[1]]), v = sub(wpos[leg.effector], wpos[leg.chain[1]]); console.log(`[knee] f ${f} ${leg.name} bend ${(180 - Math.acos(Math.max(-1, Math.min(1, (u[0] * v[0] + u[1] * v[1] + u[2] * v[2]) / (len(u) * len(v))))) * 180 / Math.PI).toFixed(1)}° dist ${(len(sub(goal, wpos[leg.chain[0]])) * 1000).toFixed(0)} total ${(leg.lengths.reduce((x, y) => x + y, 0) * 1000).toFixed(0)}`) }
2047
+ if (pl && process.env.LECODES_PLANT_TRACE === clip.name) console.log(`[plant] f ${f} ${leg.name} w ${pl.w.toFixed(2)} anchor ${pl.anchor.map((v) => v.toFixed(3)).join(',')} ball ${wpos[leg.contact].map((v) => v.toFixed(3)).join(',')} err ${(len(sub(wpos[leg.contact], pl.anchor)) * 1000).toFixed(1)}mm short ${(short * 1000).toFixed(0)}mm`)
2048
+ if (pl && pl.lockShare !== 0) {
2049
+ // The plant is the CONTACT's (the ball rolls under a rising heel; the ankle moves). Where
2050
+ // it stands is recorded at the strike, relative to the root's horizontal position.
2051
+ // (a foot merely held at its clearance is no plant — the marks carry the touch)
2052
+ const held = target.names[leg.contact]
2053
+ const r = rootXZ(), c = wpos[leg.contact]
2054
+ mark('plant', held, time, undefined, [ c[0] - r[0], c[1], c[2] - r[2] ]); active.add(`plant:${held}`)
2055
+ }
2056
+ if (short > report.legs[li].shortfall) report.legs[li].shortfall = short
2057
+ if (short > 0.005) { mark('reach', leg.name, time, short); active.add(`reach:${leg.name}`) }
2058
+ if (leg.isFoot) {
2059
+ // Everything below the effector — the ball, the toes, their ends — against the ground.
2060
+ const deepest = (): { bone: number; dip: number } => {
2061
+ let worst = { bone: -1, dip: 0 }
2062
+ const below = [ ...children[leg.effector] ]
2063
+ while (below.length) {
2064
+ const c = below.pop()!
2065
+ const dip = lowestBind - wpos[c][1]
2066
+ if (dip > worst.dip) worst = { bone: c, dip }
2067
+ below.push(...children[c])
2068
+ }
2069
+ return worst
2070
+ }
2071
+ // The knee keeps the donor's extension (`goalOf`), so the two feet's shape difference
2072
+ // lands on the ball — a centimetre or two, under the floor when the leg stands on it.
2073
+ // That is the ANKLE's to absorb, as it is on a person: the foot pitches up about the
2074
+ // ankle until the deepest bone is back on the floor (a few degrees of heel-down), the
2075
+ // toes turning with it. Capped: past that the ground really is too far.
2076
+ // Only what the transfer ADDED: the donor's own toes under its own floor (a crouch on
2077
+ // the toe tips, a foot pointed into the ground by its animator) come across scaled and
2078
+ // stay — an identity transfer must be exact.
2079
+ const d = deepest()
2080
+ let donorDip = 0
2081
+ {
2082
+ const stack = [ ...donorChildren[leg.donorEffector] ]
2083
+ let low = Infinity
2084
+ while (stack.length) { const c = stack.pop()!; low = Math.min(low, dw.pos[c][1]); stack.push(...donorChildren[c]) }
2085
+ if (low < Infinity) donorDip = Math.max(0, donorLowestBind - low) * leg.ratio
2086
+ }
2087
+ d.dip -= donorDip
2088
+ if (d.dip > 0.002) {
2089
+ const v = sub(wpos[d.bone], wpos[leg.effector]), r = len(v)
2090
+ const axis = unit(cross(v, [ 0, 1, 0 ]))
2091
+ if (axis && r > 1e-4) {
2092
+ const want = Math.min(1, (v[1] + d.dip) / r), have = v[1] / r
2093
+ let ang = Math.asin(want) - Math.asin(Math.max(-1, Math.min(1, have)))
2094
+ // a locked foot is on its ground already — the floor correction fades out with the lock
2095
+ ang = Math.max(-FOOT_PITCH_CAP, Math.min(FOOT_PITCH_CAP, ang)) * (1 - (pl?.w ?? 0))
2096
+ const tryPitch = (a: number): number => {
2097
+ const q = axisAngle(axis, a)
2098
+ const p = qrot(q, v)
2099
+ return p[1] - v[1] // how much the deepest bone rises
2100
+ }
2101
+ if (tryPitch(ang) < tryPitch(-ang)) ang = -ang
2102
+ const q = axisAngle(axis, ang)
2103
+ const stack = [ leg.effector ]
2104
+ while (stack.length) {
2105
+ const i = stack.pop()!
2106
+ wrot[i] = qnorm(qmul(q, wrot[i]))
2107
+ for (const c of children[i]) { wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c])); stack.push(c) }
2108
+ }
2109
+ }
2110
+ }
2111
+ const left = deepest()
2112
+ if (left.dip > 0.01) { mark('below', target.names[left.bone], time, left.dip); active.add(`below:${target.names[left.bone]}`) }
2113
+ }
2114
+ }
2115
+ }
2116
+ for (const k of [ ...open.keys() ]) if (!active.has(k)) open.delete(k)
2117
+
2118
+ // Twist helpers (header §6), after the legs settled: the bone's own axial turn relative to
2119
+ // its mapped parent (twistIn) and its canonical child's relative to it (twistOut), both about
2120
+ // the bone's reference axis; the helper rides the bone plus its share of the two.
2121
+ for (const tw of twists) {
2122
+ const aRef = qrot(ref[tw.parent], tw.axisLocal)
2123
+ const dP = qmul(wrot[tw.parent], qinv(ref[tw.parent])), dG = qmul(wrot[tw.grand], qinv(ref[tw.grand])), dC = qmul(wrot[tw.child], qinv(ref[tw.child]))
2124
+ const twistIn = twistAbout(qmul(qinv(dG), dP), aRef), twistOut = twistAbout(qmul(qinv(dP), dC), aRef)
2125
+ const ang = twistFraction * (tw.t * twistOut - (1 - tw.t) * twistIn)
2126
+ const aNow = qrot(wrot[tw.parent], tw.axisLocal)
2127
+ wrot[tw.bone] = qnorm(qmul(axisAngle(aNow, ang), qmul(wrot[tw.parent], target.rest.r[tw.bone])))
2128
+ const stack = [ tw.bone ]
2129
+ while (stack.length) {
2130
+ const i = stack.pop()!
2131
+ for (const c of children[i]) { if (!driven.has(c)) wrot[c] = qnorm(qmul(wrot[i], target.rest.r[c])); wpos[c] = add(wpos[i], qrot(wrot[i], localOff[c])); stack.push(c) }
2132
+ }
2133
+ }
2134
+
2135
+ rootTrack.push([ ...rootXZ() ])
2136
+ { const q = hipsIdx >= 0 ? wrot[hipsIdx] : null; hipsYawTrack.push(q ? Math.atan2(2 * (q[0] * q[2] + q[3] * q[1]), 1 - 2 * (q[0] * q[0] + q[1] * q[1])) : 0) }
2137
+ for (const [ li, leg ] of legs.entries()) {
2138
+ if (!footSide.has(li) && !handSide.has(li)) continue
2139
+ let tr = contactTrack.get(li)
2140
+ if (!tr) { tr = []; contactTrack.set(li, tr) }
2141
+ tr.push([ ...wpos[leg.contact] ])
2142
+ }
2143
+ if (donorMarks?.platform) {
2144
+ bodyTrack.push(skinned.map((i) => [ ...wpos[i] ] as Vec3))
2145
+ hipsTrack.push(hipsIdx >= 0 ? [ ...wpos[hipsIdx] ] as Vec3 : [ ...rootXZ() ])
2146
+ for (const li of footSide.keys()) { let tr = ankleTrack.get(li); if (!tr) { tr = []; ankleTrack.set(li, tr) }; tr.push([ ...wpos[legs[li].effector] ] as Vec3) }
2147
+ }
2148
+
2149
+ for (const i of animated) {
2150
+ const p = target.parents[i]
2151
+ const local = p >= 0 ? qnorm(qmul(qinv(wrot[p]), wrot[i])) : wrot[i]
2152
+ const c = chan(i)
2153
+ if (c.times.length === f) { c.times.push(time); (c.rot ??= []).push(local) }
2154
+ }
2155
+ // Root and hips translation (header §6): what the root channel drops of the travel (`strip`)
2156
+ // shifts everything below it, so the hips' channel adds back what they keep (`keep`).
2157
+ if (rootIdx >= 0 && rootRefPos && donorRoot !== undefined) {
2158
+ const travel = sub(wpos[rootIdx], rootRefPos)
2159
+ let strip: Vec3 = [ 0, 0, 0 ], keep: Vec3 = [ 0, 0, 0 ]
2160
+ if (separateRoot) {
2161
+ if (rootMotion === 'root') { strip = [ 0, travel[1], 0 ]; keep = strip }
2162
+ else if (rootMotion === 'none') { strip = travel; keep = [ 0, travel[1], 0 ] }
2163
+ else { strip = travel; keep = travel }
2164
+ } else if (rootMotion === 'none') strip = [ travel[0], 0, travel[2] ]
2165
+ const rel = sub(sub(wpos[rootIdx], strip), parentPos)
2166
+ const [ x, y, z ] = qrot(qinv(parentRot), rel)
2167
+ ;(chan(rootIdx).pos ??= []).push([ x / parentScale, y / parentScale, z / parentScale ])
2168
+ if (separateRoot && hipsParent >= 0) {
2169
+ const [ hx, hy, hz ] = qrot(qinv(wrot[hipsParent]), add(sub(wpos[hipsIdx], wpos[hipsParent]), keep))
2170
+ ;(chan(hipsIdx).pos ??= []).push([ hx / hipsParentScale, hy / hipsParentScale, hz / hipsParentScale ])
2171
+ }
2172
+ }
2173
+ }
2174
+
2175
+ for (const c of channels.values()) reduce(c, eps, legBones.has(c.node) ? Math.min(epsDeg, epsLegDeg) : epsDeg)
2176
+
2177
+ const anim = doc.createAnimation(opts.rename ? opts.rename(clip.name) : clip.name)
2178
+ let keys = 0
2179
+ for (const c of channels.values()) {
2180
+ const times = doc.createAccessor().setType('SCALAR').setArray(new Float32Array(c.times))
2181
+ if (c.rot) {
2182
+ const sampler = doc.createAnimationSampler().setInput(times).setOutput(
2183
+ doc.createAccessor().setType('VEC4').setArray(new Float32Array(c.rot.flat())))
2184
+ anim.addSampler(sampler).addChannel(doc.createAnimationChannel().setTargetNode(target.nodes[c.node]).setTargetPath('rotation').setSampler(sampler))
2185
+ keys += c.times.length
2186
+ }
2187
+ if (c.pos) {
2188
+ const sampler = doc.createAnimationSampler().setInput(times).setOutput(
2189
+ doc.createAccessor().setType('VEC3').setArray(new Float32Array(c.pos.flat())))
2190
+ anim.addSampler(sampler).addChannel(doc.createAnimationChannel().setTargetNode(target.nodes[c.node]).setTargetPath('translation').setSampler(sampler))
2191
+ keys += c.times.length
2192
+ }
2193
+ }
2194
+ // The marks (marks.ts): the donor's touch spans and still windows — what the lock above
2195
+ // enforced — with `at` read off the OUTPUT (the contact bone where it stands, relative to the
2196
+ // root that frame) and the target's staircase. Written to the animation's extras and the report.
2197
+ const contacts: ClipContacts = { feet: { l: null, r: null }, l: [], r: [], first: null }
2198
+ if (donorMarks) {
2199
+ for (const [ li, side ] of footSide) {
2200
+ const leg = legs[li]
2201
+ const tr = contactTrack.get(li)
2202
+ if (!tr || tr.length < 2) continue
2203
+ contacts.feet[side] = target.names[leg.contact]
2204
+ for (const sp of donorMarks[side]) {
2205
+ const f0 = Math.min(tr.length - 1, Math.round(sp.from / dtM))
2206
+ const r = rootTrack[f0], c = tr[f0]
2207
+ contacts[side].push({ from: sp.from, to: sp.to, still: sp.still, at: [ c[0] - r[0], c[1], c[2] - r[2] ], ...(sp.step !== undefined ? { step: sp.step } : {}) })
2208
+ }
2209
+ }
2210
+ contacts.first = donorMarks.first
2211
+ if (stairsTarget) contacts.stairs = stairsTarget
2212
+ for (const [ li, side ] of handSide) {
2213
+ const leg = legs[li]
2214
+ const tr = contactTrack.get(li)
2215
+ if (!tr || tr.length < 2) continue
2216
+ contacts.hands ??= { l: null, r: null }
2217
+ contacts.hands[side] = target.names[leg.contact]
2218
+ const key = side === 'l' ? 'hl' : 'hr'
2219
+ const list = (contacts[key] ??= [])
2220
+ for (const sp of side === 'l' ? donorMarks.hl : donorMarks.hr) {
2221
+ const f0 = Math.min(tr.length - 1, Math.round(sp.from / dtM))
2222
+ const r = rootTrack[f0], c = tr[f0]
2223
+ list.push({ from: sp.from, to: sp.to, still: sp.still, at: [ c[0] - r[0], c[1], c[2] - r[2] ], ...(sp.step !== undefined ? { step: sp.step } : {}) })
2224
+ }
2225
+ }
2226
+ }
2227
+ const travel: [ number, number ] = [ rootLast[0] - rootFirst[0], rootLast[2] - rootFirst[2] ]
2228
+ // …and the BODY DROP the locks asked for (per frame at the clip's fps, metres, + = down): what the
2229
+ // lab plots against the contacts, and what a later runtime pass could re-derive from
2230
+ const bodyDrop = Array.from(dropCurve).some((v) => Math.abs(v) > 1e-4) ? { fps: frames > 1 ? (frames - 1) / clip.duration : fps, drop: Array.from(dropCurve).map((v) => Math.round(v * 10000) / 10000) } : undefined
2231
+ // The PLATFORM's ledge (marks.ts `fitPlatform`), fitted again on the OUTPUT — the target's own
2232
+ // plants, grips and body against its own levels — so it sits where this rig's hands and feet
2233
+ // actually are. Written beside `contacts` (not inside: the engine parses that block), with
2234
+ // the root's x/z at frame 0 so the edge can be read as an absolute position too.
2235
+ let platform: (PlatformMark & { root0: [ number, number ] }) | undefined
2236
+ if (donorMarks?.platform && stairsTarget && bodyTrack.length === frames) {
2237
+ const feetT = [ ...footSide ].filter(([ li ]) => contactTrack.get(li)?.length === frames).map(([ li, sd ]) => ({ track: { contact: contactTrack.get(li)!, ankle: ankleTrack.get(li) ?? contactTrack.get(li)! }, spans: donorMarks![sd] }))
2238
+ const handsT = [ ...handSide ].filter(([ li ]) => contactTrack.get(li)?.length === frames).map(([ li, sd ]) => ({ track: { contact: contactTrack.get(li)!, ankle: contactTrack.get(li)! }, spans: sd === 'l' ? donorMarks!.hl : donorMarks!.hr }))
2239
+ const pf = fitPlatform({ dt: dtM, root: rootTrack, hips: hipsTrack, heading: dHeadingAll, level0: stairsTarget.base, level1: stairsTarget.base + stairsTarget.riser, feet: feetT, hands: handsT, hang: donorMarks.hang, body: bodyTrack })
2240
+ if (pf) platform = { ...pf, root0: [ rootFirst[0], rootFirst[2] ] }
2241
+ else warn(`${clip.name}: the platform's edge could not be placed on the target`)
2242
+ }
2243
+ // the HEADING curve (heading.ts): the smoothed direction of the root's travel on the OUTPUT
2244
+ const heading = rootTrack.length >= 2 ? headingCurve(rootTrack, hipsYawTrack, dtM, { endAt: opts.headingEnd }) : undefined
2245
+ anim.setExtras({ ...(anim.getExtras() as Record<string, unknown>), lecodes: { contacts, travel, ...(bodyDrop ? { bodyDrop } : {}), ...(platform ? { platform } : {}), ...(heading ? { heading } : {}) } })
2246
+ report.clips.push({ name: anim.getName(), duration: clip.duration, frames, keys, events, turn, contacts, travel, ...(platform ? { platform } : {}) })
2247
+ }
2248
+ return report
2249
+ }
2250
+
2251
+ /**
2252
+ * World positions implied by a set of world ROTATIONS, keeping the target's own bone offsets — i.e.
2253
+ * "what does the rig look like posed like this".
2254
+ */
2255
+ const posesFrom = (target: DocRig, worldRot: Quat[]): Vec3[] => {
2256
+ const pos: Vec3[] = new Array(target.names.length)
2257
+ for (const i of target.order) {
2258
+ const p = target.parents[i]
2259
+ if (p < 0) { pos[i] = target.bind.worldPos[i]; continue }
2260
+ // The bone's offset expressed in its parent's BIND frame, re-applied in the parent's new frame.
2261
+ const off = sub(target.bind.worldPos[i], target.bind.worldPos[p])
2262
+ const local = qrot(qinv(target.bind.worldRot[p]), off)
2263
+ pos[i] = add(pos[p], qrot(worldRot[p], local))
2264
+ }
2265
+ return pos
2266
+ }
2267
+
2268
+ /** Attach every accessor in the document to a buffer (glTF requires one). */
2269
+ export const bindAccessors = (doc: Document): void => {
2270
+ const buffer = doc.getRoot().listBuffers()[0] ?? doc.createBuffer()
2271
+ for (const acc of doc.getRoot().listAccessors()) if (!acc.getBuffer()) acc.setBuffer(buffer)
2272
+ }