@rig-c/pose 0.37.1 → 0.39.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/verify.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { poseRawAnimation, poseRawSetup, type CompiledDocument, type RawBone } from 'rig-c/core';
12
12
  import type { Body18 } from './joints.ts';
13
- import { aimAngle, aimEnds, pointAt, type Chain, type JointMap } from './map.ts';
13
+ import { aimAngle, aimEnds, partBone, pointAt, pointParts, type Chain, type JointMap } from './map.ts';
14
14
  import type { HipsInferred, Joint, PelvisFit, Run } from './pose.ts';
15
15
  import type { Retarget } from './retarget.ts';
16
16
  import { meanSecondDiff, pictureAngle, pointOn, wrap } from './retarget.ts';
@@ -311,6 +311,12 @@ export interface Posture {
311
311
  tilt: { reference: Range; rig: Range; ratio: number } | null;
312
312
  /** Present only when `tilt` is null: why it was not read. */
313
313
  tiltWhy?: string;
314
+ /**
315
+ * rig-c/pose#295: present only when the retarget keyed the pelvis bone from the
316
+ * sequence's pelvis track: the reference tilt is the track's on every frame it
317
+ * read (`frames`), and the hip line's on the frames it declined (`declined`).
318
+ */
319
+ tiltTrack?: { source: string; frames: number; declined: Run[] };
314
320
  /**
315
321
  * Present only when some frame with both hips good has an `inferred` hip:
316
322
  * how many such frames the tilt left out, and where the state came from.
@@ -345,6 +351,15 @@ export interface Posture {
345
351
  rig: (Range & { shoulderWidth: number | null; rangeSw: number | null }) | null;
346
352
  ratio: number | null;
347
353
  ratioSw: number | null;
354
+ /**
355
+ * rig-c/pose#295: present only when the pelvis bone keyed from the track
356
+ * (`bone`, `source`) is above the rig's mid-shoulder: the bones it carries
357
+ * (`origins`, its children on the way to the mid-shoulder), the x-range of
358
+ * their origin about the pelvis bone's (`carry`, units), and the sway with
359
+ * that carry taken out — mid_shoulder about the carried origin (`trunk`),
360
+ * against the reference (`ratio`).
361
+ */
362
+ carried?: { bone: string; source: string; origins: string[]; carry: number; trunk: Range; ratio: number | null };
348
363
  } | null;
349
364
  /**
350
365
  * Present with `--plant-feet` when there is at least one support span (#107):
@@ -461,8 +476,14 @@ const medianOf = (xs: number[]): number => {
461
476
  * held from the earlier read. Those four are the measurement, so the tilt is
462
477
  * read on them; any `unmeasured` frame (`fit.corr < 0.9`) or other frame with
463
478
  * an inferred hip is left out and counted.
479
+ *
480
+ * The pelvis track (#295, `track`): when the retarget keyed the pelvis bone from
481
+ * it, the reference tilt is the track's on every frame it read and the hip
482
+ * line's (as above) on the frames it declined. The sway is read as before, and
483
+ * beside it what the track's turn carries: the x-range of the carried bones'
484
+ * origin about the pelvis bone's, and the sway with it taken out.
464
485
  */
465
- export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number, inferredSource = 'source not stated', measured: Extract<HipsInferred, { by: 'measured' }> | null = null): Posture | null {
486
+ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number, inferredSource = 'source not stated', measured: Extract<HipsInferred, { by: 'measured' }> | null = null, track: { source: string; tilt: ReadonlyArray<number | null> } | null = null): Posture | null {
466
487
  const pelvis = map.chains.find((c) => c.joints.length === 2 && c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip');
467
488
  if (pelvis === undefined || map.translate === null || r.origin === null || r.scale === null) return null;
468
489
  const s = r.scale.factor;
@@ -476,10 +497,31 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
476
497
  let inferred = 0;
477
498
  const inClass = (rs: Run[], i: number) => rs.some(([a, b]) => a <= i && i <= b);
478
499
  const counts = { read: 0, interpolated: 0, held: 0, scaled: 0, unmeasured: 0 };
500
+ // rig-c/pose#295: the track the retarget keyed the pelvis bone from (its PELVIS line names it), null otherwise.
501
+ const keyed = track !== null && r.pelvis !== null && r.pelvis.source !== 'hip line' && r.pelvis.bone === pelvis.bones[0] && track.tilt.length === frames.length ? track : null;
502
+ const declined: Run[] = [];
503
+ let onTrack = 0;
479
504
  frames.forEach((f, i) => {
505
+ const tv = keyed === null ? null : (keyed.tilt[i] ?? null);
506
+ if (tv !== null) {
507
+ onTrack++;
508
+ refTilt.push(tv);
509
+ } else if (keyed !== null) {
510
+ const last = declined[declined.length - 1];
511
+ if (last !== undefined && last[1] === i - 1) last[1] = i;
512
+ else declined.push([i, i]);
513
+ }
480
514
  const rh = at(i, 'r_hip');
481
515
  const lh = at(i, 'l_hip');
482
516
  if (rh === null || lh === null) return;
517
+ if (tv === null) readHipLine(f, i, rh, lh);
518
+ const rs = at(i, 'r_shoulder');
519
+ const ls = at(i, 'l_shoulder');
520
+ if (rs === null || ls === null) return;
521
+ refSway.push(((rh[0] + lh[0]) / 2 - (rs[0] + ls[0]) / 2) * s);
522
+ refSw.push(Math.hypot(ls[0] - rs[0], ls[1] - rs[1]) * s);
523
+ });
524
+ function readHipLine(f: Partial<Record<Body18, Joint>>, i: number, rh: [number, number], lh: [number, number]): void {
483
525
  const isInferred = f.r_hip?.state === 'inferred' || f.l_hip?.state === 'inferred';
484
526
  const cls = measured === null ? undefined : (['read', 'interpolated', 'held'] as const).find((c) => inClass(measured[c], i));
485
527
  const isScaled = measured !== null && measured.scaled !== undefined && inClass(measured.scaled, i);
@@ -494,12 +536,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
494
536
  counts.unmeasured++;
495
537
  } else if (isInferred) inferred++;
496
538
  else refTilt.push(pictureAngle(rh, lh));
497
- const rs = at(i, 'r_shoulder');
498
- const ls = at(i, 'l_shoulder');
499
- if (rs === null || ls === null) return;
500
- refSway.push(((rh[0] + lh[0]) / 2 - (rs[0] + ls[0]) / 2) * s);
501
- refSw.push(Math.hypot(ls[0] - rs[0], ls[1] - rs[1]) * s);
502
- });
539
+ }
503
540
  const bone = pelvis.bones[0]!;
504
541
  const aim = pelvis.aim[0]!;
505
542
  let tilt: Posture['tilt'] = null;
@@ -538,6 +575,36 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
538
575
  ratio: rig === null ? null : rig.range / reference.range,
539
576
  ratioSw: rig === null || rig.rangeSw === null ? null : rig.rangeSw / reference.rangeSw,
540
577
  };
578
+ // rig-c/pose#295: a pelvis bone keyed from the track turns the bones it carries about its own origin, and the
579
+ // rig's mid-shoulder with them; the trunk's own sway is the mid-shoulder about the origin it carries.
580
+ if (keyed !== null && rigShoulder !== null && rig !== null) {
581
+ const h = pelvis.bones[0]!;
582
+ const b0 = posed[0]!;
583
+ /** The child of `h` on the way up from `name`, or null when `h` is not above it. */
584
+ const childOf = (name: string): string | null => {
585
+ let at: string | null = name;
586
+ while (at !== null) {
587
+ const up: string | null = b0.get(at)?.parent ?? null;
588
+ if (up === h) return at;
589
+ at = up;
590
+ }
591
+ return null;
592
+ };
593
+ const carriers = pointParts(rigShoulder).map((part) => childOf(partBone(part)));
594
+ if (carriers.some((c) => c !== null)) {
595
+ const carryX = (b: Map<string, RawBone>) => carriers.reduce((sum, c) => sum + (c === null ? 0 : b.get(c)!.worldX - b.get(h)!.worldX), 0) / carriers.length;
596
+ const xs = posed.map((b) => b.get(r.origin!.bone)!.worldX - pointAt(b, lengths, rigShoulder)![0] + carryX(b));
597
+ const trunk = rangeOf(xs);
598
+ sway.carried = {
599
+ bone: h,
600
+ source: keyed.source,
601
+ origins: [...new Set(carriers.filter((c): c is string => c !== null))],
602
+ carry: rangeOf(posed.map(carryX)).range,
603
+ trunk,
604
+ ratio: reference.range > 0 ? trunk.range / reference.range : null,
605
+ };
606
+ }
607
+ }
541
608
  }
542
609
  const bounce: BounceSpan[] = [];
543
610
  if (r.planted && r.feet !== null && r.feet.supports.length > 0) {
@@ -648,6 +715,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
648
715
  frames: { tilt: refTilt.length, sway: refSway.length, of: frames.length },
649
716
  tilt,
650
717
  ...(tilt === null ? { tiltWhy: why } : {}),
718
+ ...(keyed !== null ? { tiltTrack: { source: keyed.source, frames: onTrack, declined } } : {}),
651
719
  ...(inferred > 0 ? { hipsInferred: { frames: inferred, source: inferredSource } } : {}),
652
720
  ...(measured !== null
653
721
  ? {
@@ -696,10 +764,18 @@ export function postureLines(q: Posture): string[] {
696
764
  : '';
697
765
  const measured = m === undefined ? '' : `; tilt measured (${m.method}) on ${m.frames} frame(s): ${m.read} read, ${m.interpolated} interpolated${scaledPart}, ${m.held} held (held: ${heldSpans})${unmeasuredPart}`;
698
766
  const left = q.hipsInferred === undefined ? '' : `; ${q.hipsInferred.frames} frame(s) with an inferred hip not read (${q.hipsInferred.source})`;
767
+ // rig-c/pose#295: the reference read off the pelvis track, named as the PELVIS line names it.
768
+ const tr = q.tiltTrack;
769
+ const fromTrack =
770
+ tr === undefined
771
+ ? ''
772
+ : tr.declined.length === 0
773
+ ? `; all ${q.frames.of} frames from the track`
774
+ : `; ${tr.frames} of ${q.frames.of} frames from the track, the hip line on the ${q.frames.of - tr.frames} it declined (frames ${spansText(tr.declined)})`;
699
775
  const out = [
700
776
  t === null
701
777
  ? `hip tilt not read: ${q.tiltWhy}`
702
- : `hip tilt range (chain "${q.pelvis}", r_hip → l_hip, Spine degrees): reference ${f1(t.reference.range)}° (${sg(t.reference.min)}° … ${sg(t.reference.max)}°${over(q.frames.tilt)}), rig ${f1(t.rig.range)}° (${sg(t.rig.min)}° … ${sg(t.rig.max)}°) — rig/reference ${f3(t.ratio)}${measured}${left}`,
778
+ : `hip tilt range (chain "${q.pelvis}", r_hip → l_hip, Spine degrees): reference ${f1(t.reference.range)}°${tr === undefined ? '' : ` from ${tr.source}`} (${sg(t.reference.min)}° … ${sg(t.reference.max)}°${over(q.frames.tilt)}${fromTrack}), rig ${f1(t.rig.range)}° (${sg(t.rig.min)}° … ${sg(t.rig.max)}°) — rig/reference ${f3(t.ratio)}${measured}${left}`,
703
779
  ];
704
780
  const w = q.sway;
705
781
  if (w === null) out.push('sway: the reference has no frame with both hips and both shoulders good; not measured');
@@ -710,6 +786,11 @@ export function postureLines(q: Posture): string[] {
710
786
  const sw = w.rig.rangeSw === null ? 'no shoulder width' : `${f3(w.rig.rangeSw)} shoulder widths`;
711
787
  const ratioSw = w.ratioSw === null ? '' : `, ${f3(w.ratioSw)} in shoulder widths`;
712
788
  out.push(`sway, x-range of mid_hip − mid_shoulder: ${ref}, rig ${f1(w.rig.range)} units (${sw}; mid_hip = "${q.rigHip}", mid_shoulder = "${q.rigShoulder}") — rig/reference ${f3(w.ratio!)} in units${ratioSw}`);
789
+ const c = w.carried;
790
+ if (c !== undefined) {
791
+ const origins = c.origins.map((o) => `"${o}"`).join(', ');
792
+ out.push(`sway: the rig's ${f1(w.rig.range)} units include the pelvis track's turn — bone "${c.bone}" (keyed from ${c.source}) carries ${origins} over ${f1(c.carry)} units of x; the trunk alone (mid_shoulder about that origin) ${f1(c.trunk.range)} units — rig/reference ${c.ratio === null ? 'n/a' : f3(c.ratio)} in units`);
793
+ }
713
794
  }
714
795
  }
715
796
  if (q.bounce !== undefined) {