@rig-c/pose 0.22.2 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/switch.ts CHANGED
@@ -58,6 +58,15 @@ import type { MotionKey, MotionTrack } from './retarget.ts';
58
58
  import { pictureAngle, wrap } from './retarget.ts';
59
59
  import type { MeshFolds } from './rigc.ts';
60
60
 
61
+ export const SWITCH_LEAVE_HYSTERESIS = 15;
62
+ export const SHORT_SWITCH_WEIGHT = 0.5;
63
+
64
+ export interface ShortSwitchInfo {
65
+ cutoff: number;
66
+ fractions: ReadonlyMap<string, readonly number[]>;
67
+ weight?: number;
68
+ }
69
+
61
70
  export interface SwitchOptions {
62
71
  /** Degrees: the largest segment difference at which a set may show. */
63
72
  switchTolerance: number;
@@ -65,6 +74,12 @@ export interface SwitchOptions {
65
74
  switchMin: number;
66
75
  /** Where a boundary is keyed: 'first' (the CLI's default) keeps the frame the selection gave it; 'peak' moves it to the fastest qualifying frame of its window (#31). */
67
76
  switchAt: 'peak' | 'first';
77
+ /** Degrees above `switchTolerance` at which an entered set is left (#180; default: `SWITCH_LEAVE_HYSTERESIS` = 15). */
78
+ switchHysteresis?: number;
79
+ /** Foreshortened limb fractions for reduced weight in switch distance (#180). */
80
+ shortInfo?: ShortSwitchInfo;
81
+ /** Per chain, the reference pose's normalised wrist-from-shoulder offset on each frame, `(wrist - shoulder) / scale`, y down (#180). */
82
+ handOffsets?: ReadonlyMap<string, ReadonlyArray<readonly [number, number]>>;
68
83
  }
69
84
 
70
85
  /** One boundary of a span: where the selection put it, where it is keyed, and how fast the chains it changes move on each. */
@@ -158,16 +173,41 @@ export interface Switches {
158
173
  chainHolds?: Array<{ set: string; chain: string; frames: [number, number] }>;
159
174
  }
160
175
 
161
- /** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain. */
162
- export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, Map<string, number[]>> {
176
+ /** Per set and chain, its distance to the pose on every frame: the largest segment difference over that chain (with foreshortened `SHORT` segments weighted by `SHORT_SWITCH_WEIGHT`, #180). */
177
+ export function chainDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number, shortInfo?: ShortSwitchInfo): Map<string, Map<string, number[]>> {
163
178
  const out = new Map<string, Map<string, number[]>>();
179
+ const wShort = shortInfo?.weight ?? SHORT_SWITCH_WEIGHT;
164
180
  for (const a of map.alternates) {
165
181
  const byChain = new Map<string, number[]>();
166
182
  for (const cn of a.chains) {
167
183
  const c = map.chains.find((x) => x.name === cn)!;
168
- const segs = c.bones.map((b, k) => ({ asked: targets.get(b)!, key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!) }));
184
+ const foldedKey = c.joints.length >= 3 && (() => {
185
+ const j0 = a.pose[c.joints[0]!];
186
+ const j1 = a.pose[c.joints[1]!];
187
+ const j2 = a.pose[c.joints[c.joints.length - 1]!];
188
+ return j0 !== undefined && j1 !== undefined && j2 !== undefined && Math.hypot(j2[0] - j0[0], j2[1] - j0[1]) <= Math.hypot(j1[0] - j0[0], j1[1] - j0[1]);
189
+ })();
190
+ const segs = c.bones.map((b, k) => {
191
+ const frac = shortInfo?.fractions.get(b);
192
+ const hasShort = frac !== undefined && frac.some((f) => f < shortInfo!.cutoff);
193
+ return {
194
+ asked: targets.get(b)!,
195
+ key: pictureAngle(a.pose[c.joints[k]!]!, a.pose[c.joints[k + 1]!]!),
196
+ frac,
197
+ alwaysShort: foldedKey && hasShort,
198
+ };
199
+ });
169
200
  const d: number[] = [];
170
- for (let i = 0; i < n; i++) d.push(Math.max(...segs.map((s) => Math.abs(wrap(s.asked[i]! - s.key)))));
201
+ for (let i = 0; i < n; i++) {
202
+ d.push(
203
+ Math.max(
204
+ ...segs.map((s) => {
205
+ const isShort = s.alwaysShort || (s.frac !== undefined && s.frac[i]! < shortInfo!.cutoff);
206
+ return (isShort ? wShort : 1) * Math.abs(wrap(s.asked[i]! - s.key));
207
+ }),
208
+ ),
209
+ );
210
+ }
171
211
  byChain.set(cn, d);
172
212
  }
173
213
  out.set(a.name, byChain);
@@ -176,8 +216,8 @@ export function chainDistances(map: JointMap, targets: ReadonlyMap<string, reado
176
216
  }
177
217
 
178
218
  /** Per set, its distance to the pose on every frame: the largest segment difference over its chains. */
179
- export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number): Map<string, number[]> {
180
- const byChain = chainDistances(map, targets, n);
219
+ export function setDistances(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, n: number, shortInfo?: ShortSwitchInfo): Map<string, number[]> {
220
+ const byChain = chainDistances(map, targets, n, shortInfo);
181
221
  const out = new Map<string, number[]>();
182
222
  for (const a of map.alternates) {
183
223
  const cs = a.chains.map((cn) => byChain.get(a.name)!.get(cn)!);
@@ -276,7 +316,17 @@ function groupLayering(group: readonly AlternateSet[], map: JointMap, slots: Rig
276
316
  if (bySide !== undefined) slotChain.set(s, bySide);
277
317
  else if (groupChains.length === 1) slotChain.set(s, groupChains[0]!);
278
318
  }
279
- return { torsoSlot, torsoRank, touched, isBehind, isFront, baseInFront, slotChain };
319
+ const isChainBehindBase = (a: AlternateSet, cn: string): boolean => {
320
+ if (!a.chains.includes(cn)) return false;
321
+ if (isBehind(a)) return true;
322
+ const setupSlots = hiddenSetup.filter((s) => slotChain.get(s) === cn && slots.get(s)?.bone !== torsoSlot);
323
+ const setSlots = Object.keys(a.slots).filter((s) => slotChain.get(s) === cn && rank.has(s));
324
+ if (setupSlots.length === 0 || setSlots.length === 0) return false;
325
+ const boneSetup = setupSlots.filter((s) => boneOwner.has(slots.get(s)?.bone ?? ''));
326
+ const refSetup = boneSetup.length > 0 ? boneSetup : setupSlots;
327
+ return setupSlots.every((s) => a.hides.includes(s)) && Math.max(...setSlots.map((s) => rank.get(s)!)) < Math.min(...refSetup.map((s) => rank.get(s)!));
328
+ };
329
+ return { torsoSlot, torsoRank, touched, isBehind, isFront, isChainBehindBase, baseInFront, slotChain };
280
330
  }
281
331
 
282
332
  /**
@@ -365,6 +415,24 @@ function moveBoundaries(vals: ReadonlyArray<AlternateSet | null>, pos: readonly
365
415
  /** The key time for a change that shows on frame i: half a frame before it, 0 on the first frame. */
366
416
  const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 : r6((times[i - 1]! + times[i]!) / 2));
367
417
 
418
+ function keyHandOffset(a: AlternateSet, map: JointMap, cn: string): [number, number] | null {
419
+ const c = map.chains.find((x) => x.name === cn);
420
+ if (c === undefined || c.joints.length < 2) return null;
421
+ const sh = a.pose[c.joints[0]!];
422
+ const wr = a.pose[c.joints[c.joints.length - 1]!];
423
+ if (sh === undefined || wr === undefined) return null;
424
+ const rs = a.pose.r_shoulder;
425
+ const ls = a.pose.l_shoulder;
426
+ const scale = rs !== undefined && ls !== undefined
427
+ ? Math.hypot(ls[0] - rs[0], ls[1] - rs[1])
428
+ : c.joints.slice(0, -1).reduce((s, j, k) => {
429
+ const p0 = a.pose[j];
430
+ const p1 = a.pose[c.joints[k + 1]!];
431
+ return p0 !== undefined && p1 !== undefined ? s + Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) : s;
432
+ }, 0);
433
+ return scale > 1e-6 ? [(wr[0] - sh[0]) / scale, (wr[1] - sh[1]) / scale] : null;
434
+ }
435
+
368
436
  /**
369
437
  * Select the switches, and key them: per frame and per group of competing
370
438
  * sets, the nearest within the tolerance; runs shorter than the minimum span
@@ -374,8 +442,9 @@ const changeAt = (times: readonly number[], i: number): number => (i === 0 ? 0 :
374
442
  */
375
443
  export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, readonly number[]>, times: readonly number[], duration: number, slots: RigSlots, opts: SwitchOptions): Switches {
376
444
  const n = times.length;
377
- const byChain = chainDistances(map, targets, n);
378
- const distances = setDistances(map, targets, n);
445
+ const byChain = chainDistances(map, targets, n, opts.shortInfo);
446
+ const distances = setDistances(map, targets, n, opts.shortInfo);
447
+ const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
379
448
  const switches: Switch[] = [];
380
449
  const groups = competing(map.alternates);
381
450
  for (const group of groups) {
@@ -419,6 +488,39 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
419
488
  // minimum span exists to prevent (measured on the dancer: one-frame breaks at
420
489
  // a blurred frame inside a span of one set).
421
490
  bridgeSame(pick);
491
+ // #180: hysteresis on leaving a set — bridge internal excursions that stay within
492
+ // tolerance + hyst (never exceeding tolerance + hyst for >= switchMin), then drop
493
+ // runs shorter than switchMin, then hold trailing shelves within tolerance + hyst.
494
+ const useHyst = (a: AlternateSet) =>
495
+ hyst > 0 && (opts.switchHysteresis !== undefined || (a.chains.length >= 2 && opts.shortInfo !== undefined));
496
+ if (hyst > 0) {
497
+ const allowedOn = (a: AlternateSet, k: number) => a.frames === null || a.frames.some(([f, l]) => k >= f && k <= l);
498
+ for (let i = 0; i < n;) {
499
+ const a = pick[i];
500
+ let j = i;
501
+ while (j + 1 < n && pick[j + 1] === a) j++;
502
+ const before = i > 0 ? pick[i - 1] : null;
503
+ const after = j + 1 < n ? pick[j + 1] : null;
504
+ if (a === null && before !== null && before !== undefined && before === after && useHyst(before)) {
505
+ const ds = distances.get(before.name)!;
506
+ let leftLong = false;
507
+ for (let k = i; k <= j;) {
508
+ if (!allowedOn(before, k) || ds[k]! > opts.switchTolerance + hyst) {
509
+ let m = k;
510
+ while (m + 1 <= j && (!allowedOn(before, m + 1) || ds[m + 1]! > opts.switchTolerance + hyst)) m++;
511
+ const to = m + 1 < n ? times[m + 1]! : duration;
512
+ if (!allowedOn(before, k) || to - times[k]! >= opts.switchMin - 1e-9) {
513
+ leftLong = true;
514
+ break;
515
+ }
516
+ k = m + 1;
517
+ } else k++;
518
+ }
519
+ if (!leftLong) for (let k = i; k <= j; k++) pick[k] = before;
520
+ }
521
+ i = j + 1;
522
+ }
523
+ }
422
524
  // A run shorter than the minimum span shows the base art.
423
525
  for (let i = 0; i < n;) {
424
526
  const a = pick[i];
@@ -428,6 +530,32 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
428
530
  if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
429
531
  i = j + 1;
430
532
  }
533
+ if (hyst > 0) {
534
+ const allowedOn = (a: AlternateSet, k: number) => a.frames === null || a.frames.some(([f, l]) => k >= f && k <= l);
535
+ const anyWithinTol = (a: AlternateSet, k: number) => a.chains.some((cn) => (byChain.get(a.name)?.get(cn)?.[k] ?? Infinity) <= opts.switchTolerance);
536
+ const hasShortAt = (a: AlternateSet, k: number) =>
537
+ opts.shortInfo !== undefined &&
538
+ Object.keys(a.drawn).some((b) => (opts.shortInfo!.fractions.get(b)?.[k] ?? 1) < 0.60);
539
+ for (let i = 0; i < n;) {
540
+ const a = pick[i];
541
+ let j = i;
542
+ while (j + 1 < n && pick[j + 1] === a) j++;
543
+ if (a !== null && a !== undefined && useHyst(a)) {
544
+ const ds = distances.get(a.name)!;
545
+ let l = i;
546
+ while (l - 1 >= 0 && pick[l - 1] === null && allowedOn(a, l - 1) && ds[l - 1]! <= opts.switchTolerance + hyst && anyWithinTol(a, l - 1) && hasShortAt(a, l - 1)) l--;
547
+ let m = j;
548
+ while (m + 1 < n && pick[m + 1] === null && allowedOn(a, m + 1) && ds[m + 1]! <= opts.switchTolerance + hyst && anyWithinTol(a, m + 1)) m++;
549
+ if (i - l >= 1 || m - j >= 2 || (m - j === 1 && hasShortAt(a, j + 1))) {
550
+ for (let k = l; k < i; k++) pick[k] = a;
551
+ for (let k = j + 1; k <= m; k++) pick[k] = a;
552
+ j = m;
553
+ }
554
+ }
555
+ i = j + 1;
556
+ }
557
+ bridgeSame(pick);
558
+ }
431
559
  // #136: a short front-set window (< 2 × switchMin) between two behind-set windows is dropped.
432
560
  if (layer.torsoRank !== null) {
433
561
  let dropped = false;
@@ -498,9 +626,8 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
498
626
  for (let i = sw.frames[0]; i <= sw.frames[1]; i++) row[i] = o;
499
627
  }
500
628
  }
501
- // #136/#139/#162: between two sets that both draw an arm behind the torso, a frame that belongs to
502
- // neither set stays behind (the nearer set's layer). The default front slot is for a frame no set can
503
- // claim and whose arm is read in front — not a fallback.
629
+ // #136/#139/#162/#180: between two sets that draw an arm behind the base front setup (at least one behind the torso),
630
+ // a frame that belongs to neither set stays on the nearer set's layer (chosen by hand position when available, #180).
504
631
  const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
505
632
  for (const group of groups) {
506
633
  const layer = groupLayering(group, map, slots);
@@ -523,11 +650,29 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
523
650
  const sw1 = gSwitches[m + 1]!;
524
651
  const set0 = group.find((a) => a.name === sw0.set)!;
525
652
  const set1 = group.find((a) => a.name === sw1.set)!;
526
- if (!layer.isBehind(set0) || !layer.isBehind(set1)) continue;
653
+ if (!layer.isBehind(set0) && !layer.isBehind(set1)) continue;
527
654
  const g0 = sw0.frames[1] + 1;
528
655
  const g1 = sw1.frames[0] - 1;
529
656
  if (g0 > g1) continue;
530
- const chains = [...new Set([...set0.chains, ...set1.chains])];
657
+ const chains = [...new Set([...set0.chains, ...set1.chains])].filter((cn) => layer.isChainBehindBase(set0, cn) && layer.isChainBehindBase(set1, cn));
658
+ if (chains.length === 0) continue;
659
+ for (const cn of chains) {
660
+ if (!layer.isBehind(set0)) for (let i = sw0.frames[0]; i <= sw0.frames[1]; i++) cClamp.get(cn)![i] = set0;
661
+ if (!layer.isBehind(set1)) for (let i = sw1.frames[0]; i <= sw1.frames[1]; i++) cClamp.get(cn)![i] = set1;
662
+ }
663
+ const nearerOf = (cn: string, i: number, d0: number, d1: number): AlternateSet => {
664
+ if (set0 !== set1) {
665
+ const ref = opts.handOffsets?.get(cn)?.[i];
666
+ const k0 = keyHandOffset(set0, map, cn);
667
+ const k1 = keyHandOffset(set1, map, cn);
668
+ if (ref !== undefined && k0 !== null && k1 !== null) {
669
+ const h0 = Math.hypot(ref[0] - k0[0], ref[1] - k0[1]);
670
+ const h1 = Math.hypot(ref[0] - k1[0], ref[1] - k1[1]);
671
+ return h0 <= h1 ? set0 : set1;
672
+ }
673
+ }
674
+ return d0 <= d1 ? set0 : set1;
675
+ };
531
676
  const cPick = new Map<string, Array<AlternateSet | null>>();
532
677
  for (const cn of chains) {
533
678
  const arr: Array<AlternateSet | null> = Array.from({ length: n }, () => null);
@@ -580,7 +725,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
580
725
  } else {
581
726
  for (const cn of chains) {
582
727
  const arr = cPick.get(cn)!;
583
- const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn));
728
+ const frontSets = group.filter((a) => layer.isFront(a) && a.chains.includes(cn) && a !== set0 && a !== set1);
584
729
  const d0s = byChain.get(set0.name)?.get(cn);
585
730
  const d1s = byChain.get(set1.name)?.get(cn);
586
731
  for (let i = g0; i <= g1; i++) {
@@ -589,7 +734,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
589
734
  if (!claimedFront) {
590
735
  const d0 = d0s?.[i] ?? Infinity;
591
736
  const d1 = d1s?.[i] ?? Infinity;
592
- arr[i] = d0 <= d1 ? set0 : set1;
737
+ arr[i] = nearerOf(cn, i, d0, d1);
593
738
  }
594
739
  }
595
740
  for (let i = g0; i <= g1;) {
@@ -604,7 +749,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
604
749
  for (let k = i; k <= j; k++) {
605
750
  const d0 = d0s?.[k] ?? Infinity;
606
751
  const d1 = d1s?.[k] ?? Infinity;
607
- arr[k] = d0 <= d1 ? set0 : set1;
752
+ arr[k] = nearerOf(cn, k, d0, d1);
608
753
  }
609
754
  } else if (before !== null && before === after && a !== before) {
610
755
  for (let k = i; k <= j; k++) arr[k] = before;
@@ -626,6 +771,9 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
626
771
  }
627
772
  i = j + 1;
628
773
  }
774
+ for (let i = g0; i <= g1; i++) {
775
+ if (arr[i] !== null) cClamp.get(cn)![i] = arr[i]!;
776
+ }
629
777
  }
630
778
  }
631
779
  for (const s of layer.touched) {
@@ -636,7 +784,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
636
784
  const a = cPick.get(cn)?.[i] ?? null;
637
785
  if (a !== null) shown.get(s)![i] = a.slots[s] ?? null;
638
786
  }
639
- if (layer.torsoSlot !== null && shown.has(layer.torsoSlot)) {
787
+ if (layer.torsoSlot !== null && shown.has(layer.torsoSlot) && layer.isBehind(set0) && layer.isBehind(set1)) {
640
788
  const picked = chains.map((cn) => cPick.get(cn)![i]);
641
789
  if (picked.every((a) => a !== null)) shown.get(layer.torsoSlot)![i] = picked[0]!.slots[layer.torsoSlot] ?? slots.get(layer.torsoSlot)!.setup;
642
790
  }
@@ -920,6 +1068,12 @@ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Ma
920
1068
  */
921
1069
  export const RAISED_WITHIN = 30;
922
1070
 
1071
+ /**
1072
+ * --raised-by-head (#179): a chain is placed by the head when its key skeleton's
1073
+ * wrist lies within this many eye distances of the head centre.
1074
+ */
1075
+ export const RAISED_WRIST_EYES = 2;
1076
+
923
1077
  /** Whether a drawn world angle, Spine degrees, is within `RAISED_WITHIN` of straight up (90°). */
924
1078
  export const isRaised = (drawn: number): boolean => Math.abs(wrap(drawn - 90)) <= RAISED_WITHIN;
925
1079
 
package/src/verify.ts CHANGED
@@ -561,6 +561,22 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
561
561
  if (b.has('head')) return b.get('head')!.worldY;
562
562
  return rigNeckY(b);
563
563
  };
564
+ const headBoneName = (() => {
565
+ if (headChain !== undefined) {
566
+ const k = headChain.joints.findIndex((j, idx) => idx + 1 < headChain.joints.length && ((j === 'r_eye' && headChain.joints[idx + 1] === 'l_eye') || (j === 'l_eye' && headChain.joints[idx + 1] === 'r_eye')));
567
+ if (k >= 0) return headChain.bones[k]!;
568
+ }
569
+ return posed[0]?.has('head') ? 'head' : null;
570
+ })();
571
+ const maxNod = (() => {
572
+ if (headBoneName === null || rigNeck === null || posed.length === 0) return Infinity;
573
+ const b0 = posed[0]!;
574
+ const hb0 = b0.get(headBoneName);
575
+ const nk0 = pointAt(b0, lengths, rigNeck);
576
+ if (hb0 === undefined || nk0 === null) return Infinity;
577
+ const d = Math.hypot(hb0.worldX - nk0[0], hb0.worldY - nk0[1]);
578
+ return d > 1e-6 ? 0.022 * d : Infinity;
579
+ })();
564
580
  for (const sp of r.feet.supports) {
565
581
  const refHip: number[] = [];
566
582
  const rigHipY: number[] = [];
@@ -568,6 +584,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
568
584
  const rigNeckS: number[] = [];
569
585
  const refEyesNeck: number[] = [];
570
586
  const rigEyesNeck: number[] = [];
587
+ const rigEyeHead: number[] = [];
571
588
  for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
572
589
  const b = posed[i]!;
573
590
  const rh = at(i, 'r_hip');
@@ -587,15 +604,27 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
587
604
  if (re !== null && le !== null && ey !== null) {
588
605
  refEyesNeck.push(-((re[1] + le[1]) / 2 - nk[1]) * s);
589
606
  rigEyesNeck.push(ey - ny);
607
+ const hy = headBoneName !== null && b.has(headBoneName) ? b.get(headBoneName)!.worldY : ny;
608
+ rigEyeHead.push(ey - hy);
590
609
  }
591
610
  }
592
611
  }
612
+ const boundedRefEyesNeck = (() => {
613
+ if (!(maxNod < Infinity) || refEyesNeck.length === 0) return refEyesNeck;
614
+ const dr = detrend(refEyesNeck);
615
+ const dgEH = detrend(rigEyeHead);
616
+ return refEyesNeck.map((v, i) => {
617
+ const base = v - dr[i]!;
618
+ const nod = Math.max(-maxNod, Math.min(maxNod, dr[i]! - dgEH[i]!));
619
+ return base + dgEH[i]! + nod;
620
+ });
621
+ })();
593
622
  bounce.push({
594
623
  side: sp.side,
595
624
  frames: [sp.frames[0], sp.frames[1]],
596
625
  hip: bounceMetric(refHip, rigHipY),
597
626
  neck: bounceMetric(refNeck, rigNeckS),
598
- eyesNeck: bounceMetric(refEyesNeck, rigEyesNeck),
627
+ eyesNeck: bounceMetric(boundedRefEyesNeck, rigEyesNeck),
599
628
  });
600
629
  }
601
630
  }