@rig-c/pose 0.39.0 → 0.41.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/reduce.ts CHANGED
@@ -48,6 +48,8 @@ export interface ReduceBone {
48
48
  dense: number[];
49
49
  /** Frame indices that must keep a key regardless (the first and last always do). */
50
50
  keep: ReadonlySet<number>;
51
+ /** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283: its chain's). */
52
+ tolerance?: number;
51
53
  }
52
54
  export interface ReduceHip {
53
55
  bone: string;
@@ -78,6 +80,12 @@ export interface Reduced {
78
80
  rows: ReduceRow[];
79
81
  /** Each bone's world-aim error on every frame (reduced less dense, degrees), as the poser evaluates the reduced keys. */
80
82
  errors: Map<string, number[]>;
83
+ /**
84
+ * rig-c/pose#320: each trunk root whose smoothing (`smoothTrunk`, #116) was not kept because it left a bone it
85
+ * manages past that bone's own tolerance — the bone, its smoothed worst error and its tolerance (degrees); the
86
+ * fitted keys stand. Absent when every smoothing was kept.
87
+ */
88
+ unsmoothed?: Array<{ root: string; bone: string; worst: number; tolerance: number }>;
81
89
  }
82
90
 
83
91
  // ---------------------------------------------------------------------------
@@ -328,6 +336,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
328
336
  const byName = new Map<string, ReduceBone>();
329
337
  for (const b of bones) {
330
338
  if (byName.has(b.bone)) throw new Error(`reduce: bone "${b.bone}" is given twice`);
339
+ if (b.tolerance !== undefined && !(Number.isFinite(b.tolerance) && b.tolerance > 0)) throw new Error(`reduce: bone "${b.bone}"'s tolerance is ${b.tolerance}°, not a positive number`);
331
340
  if (b.dense.length !== n) throw new Error(`reduce: bone "${b.bone}" has ${b.dense.length} dense key(s) for ${n} frame(s)`);
332
341
  byName.set(b.bone, b);
333
342
  }
@@ -349,7 +358,7 @@ export function reduceKeys(times: readonly number[], bones: readonly ReduceBone[
349
358
  const keys = fitTrack(times, samples, {
350
359
  targets,
351
360
  keep: keptFrames(b.keep, n, `bone "${b.bone}"`),
352
- tolerance,
361
+ tolerance: b.tolerance ?? tolerance,
353
362
  error: (i, v) => Math.abs(v[0]! - targets[i]![0]!),
354
363
  });
355
364
  const e = trackAt(keys, samples, 1).map((v, i) => v[0]! - targets[i]![0]!);
@@ -604,7 +613,9 @@ function solveLimbPreservingJitter(
604
613
  * above the sequence's own. Optimizing knot values and handle y-coordinates
605
614
  * jointly within the ±tolerance world-aim tube removes those C0 breaks while
606
615
  * keeping any limb chains hanging from the trunk within 0.04 of their landing
607
- * and jitter.
616
+ * and jitter. The tube's bounds are a penalty, not a constraint: a root whose
617
+ * smoothing leaves any bone it manages past that bone's own tolerance keeps its
618
+ * fitted keys instead, recorded in `out.unsmoothed` (rig-c/pose#320).
608
619
  */
609
620
  export function smoothTrunk(
610
621
  times: readonly number[],
@@ -617,11 +628,12 @@ export function smoothTrunk(
617
628
  if (n < 4 || trunkBones.size === 0) return;
618
629
  const samples = samplesOf(times);
619
630
  const byName = new Map(bones.map((b) => [b.bone, b]));
631
+ /** rig-c/pose#283: a bone's own tolerance (its chain's under a per-chain --reduce), else the call's. */
632
+ const tolOf = (b: string): number => byName.get(b)?.tolerance ?? tolerance;
620
633
  const roots = [...trunkBones].filter((b) => {
621
634
  const info = byName.get(b);
622
635
  return info !== undefined && (info.above === null || !trunkBones.has(info.above));
623
636
  });
624
- const tolInner = 0.985 * tolerance;
625
637
 
626
638
  for (const root of roots) {
627
639
  const rootKeys = out.rotate.get(root);
@@ -646,8 +658,10 @@ export function smoothTrunk(
646
658
  const targetRoot = rootInfo.dense.map((d, i) => d - eAboveRoot[i]!);
647
659
  const yRootOld = yOldOf.get(root)!;
648
660
 
649
- const rootLb = targetRoot.map((t) => t - tolInner);
650
- const rootUb = targetRoot.map((t) => t + tolInner);
661
+ // #283: each bone's own tolerance (its chain's under a per-chain --reduce; the call's otherwise).
662
+ const rootInner = 0.985 * tolOf(root);
663
+ const rootLb = targetRoot.map((t) => t - rootInner);
664
+ const rootUb = targetRoot.map((t) => t + rootInner);
651
665
  let smRoot = optimizeBasis(basisOf.get(root)!, rootLb, rootUb);
652
666
  let eRoot = smRoot.y.map((v, i) => v - targetRoot[i]!);
653
667
 
@@ -667,14 +681,15 @@ export function smoothTrunk(
667
681
  for (const h of headBones) {
668
682
  const dH = byName.get(h)!.dense;
669
683
  const targetH = dH.map((d, i) => d - eRoot[i]!);
670
- const smH = optimizeBasis(basisOf.get(h)!, targetH.map((t) => t - tolInner), targetH.map((t) => t + tolInner));
684
+ const hInner = 0.985 * tolOf(h);
685
+ const smH = optimizeBasis(basisOf.get(h)!, targetH.map((t) => t - hInner), targetH.map((t) => t + hInner));
671
686
  smSub.set(h, smH);
672
687
  eSub.set(h, smH.y.map((v, i) => v - targetH[i]!));
673
688
  }
674
689
  for (const b0 of arm0Bones) {
675
690
  const yOld = yOldOf.get(b0)!;
676
691
  const wOld = Math.max(...out.errors.get(b0)!.map(Math.abs));
677
- const cap = Math.min(0.98 * tolerance, wOld + 0.035 * tolerance);
692
+ const cap = Math.min(0.98 * tolOf(b0), wOld + 0.035 * tolOf(b0));
678
693
  const targetB = byName.get(b0)!.dense.map((d, i) => d - eRoot[i]!);
679
694
  const smB = solveLimbPreservingJitter(basisOf.get(b0)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
680
695
  smSub.set(b0, smB);
@@ -685,7 +700,7 @@ export function smoothTrunk(
685
700
  const eUp = eSub.get(up)!;
686
701
  const yOld = yOldOf.get(b1)!;
687
702
  const wOld = Math.max(...out.errors.get(b1)!.map(Math.abs));
688
- const cap = Math.min(0.98 * tolerance, wOld + 0.035 * tolerance);
703
+ const cap = Math.min(0.98 * tolOf(b1), wOld + 0.035 * tolOf(b1));
689
704
  const targetB = byName.get(b1)!.dense.map((d, i) => d - eUp[i]!);
690
705
  const smB = solveLimbPreservingJitter(basisOf.get(b1)!, yOld, targetB.map((t) => t - cap), targetB.map((t) => t + cap));
691
706
  smSub.set(b1, smB);
@@ -699,17 +714,17 @@ export function smoothTrunk(
699
714
  for (let i = 0; i < n; i++) {
700
715
  for (const h of headBones) {
701
716
  const eh = eSub.get(h)![i]!;
702
- if (Math.abs(eh) > 0.99 * tolerance) {
703
- tightenAround(i, Math.abs(eh) - 0.975 * tolerance);
717
+ if (Math.abs(eh) > 0.99 * tolOf(h)) {
718
+ tightenAround(i, Math.abs(eh) - 0.975 * tolOf(h));
704
719
  anyTightened = true;
705
720
  }
706
721
  }
707
722
  for (const b of [...arm0Bones, ...arm1Bones]) {
708
723
  const wOld = Math.max(...out.errors.get(b)!.map(Math.abs));
709
- const cap = Math.min(0.985 * tolerance, wOld + 0.04 * tolerance);
724
+ const cap = Math.min(0.985 * tolOf(b), wOld + 0.04 * tolOf(b));
710
725
  const eb = eSub.get(b)![i]!;
711
726
  if (Math.abs(eb) > cap) {
712
- tightenAround(i, Math.abs(eb) - (cap - 0.01 * tolerance));
727
+ tightenAround(i, Math.abs(eb) - (cap - 0.01 * tolOf(b)));
713
728
  anyTightened = true;
714
729
  }
715
730
  }
@@ -730,6 +745,16 @@ export function smoothTrunk(
730
745
  }
731
746
  };
732
747
 
748
+ // rig-c/pose#320: the smoothing is kept only within every managed bone's own tolerance; past it, the fitted keys
749
+ // (within it by construction) stand, and the run says so.
750
+ const past = allManaged.map((b) => {
751
+ const e = b === root ? eRoot : eSub.get(b)!;
752
+ return { b, worst: Math.max(...e.map(Math.abs)) };
753
+ }).find((x) => x.worst > tolOf(x.b));
754
+ if (past !== undefined) {
755
+ (out.unsmoothed ??= []).push({ root, bone: past.b, worst: past.worst, tolerance: tolOf(past.b) });
756
+ continue;
757
+ }
733
758
  updateBone(root, smRoot.keys, eRoot);
734
759
  for (const b of [...headBones, ...arm0Bones, ...arm1Bones]) {
735
760
  updateBone(b, smSub.get(b)!.keys, eSub.get(b)!);
@@ -827,6 +852,8 @@ export interface UnderBone {
827
852
  /** The dense rotate keys (unwrapped, as written). */
828
853
  dense: readonly number[];
829
854
  keep: ReadonlySet<number>;
855
+ /** Its own world-aim tolerance in degrees, in place of the call's (rig-c/pose#283). */
856
+ tolerance?: number;
830
857
  }
831
858
 
832
859
  /**
@@ -879,7 +906,7 @@ export function reduceUnderScale(times: readonly number[], bones: readonly Under
879
906
  return Math.abs(wrap180(Math.atan2(w[1], w[0]) * DEG - b.target[i]!));
880
907
  };
881
908
  const keep = keptFrames(b.keep, n, `bone "${b.bone}"`);
882
- const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance, error: (i, v) => aimError(i, v[0]!) });
909
+ const rk = fitTrack(times, samples, { targets: solved.map((k) => [k]), keep, tolerance: b.tolerance ?? tolerance, error: (i, v) => aimError(i, v[0]!) });
883
910
  const kAt = trackAt(rk, samples, 1).map((v) => v[0]!);
884
911
  const ae = worstOf(kAt.map((_, i) => i), (i) => aimError(i, kAt[i]!))!;
885
912
  rows.push({ bone: b.bone, kind: 'angle', before: n, after: rk.length, worst: ae.worst, frame: ae.frame });
package/src/retarget.ts CHANGED
@@ -79,7 +79,7 @@ import { aimAngle, aimEnds, bodyYawSets, descends, faceYawSets, knownSetsOf, par
79
79
  import type { Joint, Travel } from './pose.ts';
80
80
  import { TRAVEL_IN_PLACE } from './pose.ts';
81
81
  import { Problems, Refusal } from './problems.ts';
82
- import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
82
+ import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SetRuns, SlotTrack, Switch, SwitchOptions } from './switch.ts';
83
83
  import { clampArt, coveredChains, isRaised, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
84
84
  import type { BlendHold, Closure, Feet, Floor, LegScale, LegSegmentRatio, PathPose, Posing, Side, Stance } from './legs.ts';
85
85
  import { figureScaleOf, freeAnkleShiftOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, shiftOver, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
@@ -369,6 +369,34 @@ export interface ReachReading {
369
369
  ratio: number | null;
370
370
  }
371
371
 
372
+ /**
373
+ * rig-c/pose#282: where a `reach` chain's bone starts against the source joint
374
+ * it re-aims from. The pose's pair starts at the translated joint, which the
375
+ * rig puts at the translated bone's posed origin; the reach aims from its own
376
+ * bone's posed origin. The distance between the two, at setup and on the
377
+ * frames, is the lever #236 names — a reading, with a WARN past
378
+ * `REACH_ORIGIN_WARN` of the segment. One per reach chain, in map order.
379
+ */
380
+ export interface ReachOrigin {
381
+ chain: string;
382
+ bone: string;
383
+ /** The pair: the chain's first joint (the translated joint) and its second. */
384
+ joints: [string, string];
385
+ /** The translated bone, whose posed origin is where the rig puts the source joint. */
386
+ anchor: string;
387
+ /** The distance at setup, world units. */
388
+ setup: number;
389
+ /** The largest distance as posed on the frames, world units, and its frame. */
390
+ posed: { max: number; frame: number };
391
+ /** The reached length: the pair's median picture length, scaled (world units). */
392
+ segment: number;
393
+ /** max(setup, posed.max) ÷ segment. */
394
+ ofSegment: number;
395
+ /** ofSegment > threshold. */
396
+ warn: boolean;
397
+ threshold: number;
398
+ }
399
+
372
400
  /**
373
401
  * Mean |second difference| of a series of angles, degrees: |wrap(v[i+1] − v[i])
374
402
  * − wrap(v[i] − v[i−1])| over every i whose three frames `ok` admits (every
@@ -419,6 +447,8 @@ export interface Retarget {
419
447
  scaleFit: ScaleFit | null;
420
448
  /** The spans each alternate set shows, with their art turn. */
421
449
  switches: Switch[];
450
+ /** rig-c/pose#279: per set without a `phase`, the runs it was nearest on and the runs the minimum span suppressed. */
451
+ switchRuns: SetRuns[];
422
452
  /** Per switched or hidden slot, what it shows on each frame (null: nothing). */
423
453
  shown: Map<string, Array<string | null>>;
424
454
  /** Per set, its distance to the pose on each frame, degrees. */
@@ -475,6 +505,8 @@ export interface Retarget {
475
505
  nodSkipped: NodSkipped | null;
476
506
  /** Each `reach` chain hanging below a bone the animation turns or translates (#236): its aim's departure from the pose pair's angle. */
477
507
  reach: ReachReading[];
508
+ /** rig-c/pose#282: each `reach` chain's origin against the source joint it re-aims from; empty when the map has none. */
509
+ reachOrigin: ReachOrigin[];
478
510
  /** rig-c/pose#288: the pelvis bone's source; null when the map has no pelvis chain and the sequence no track. */
479
511
  pelvis: PelvisReading | null;
480
512
  }
@@ -1486,6 +1518,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
1486
1518
  const reaches = map.chains.filter((c) => c.reach).sort((x, y) => depth(x.bones[0]!) - depth(y.bones[0]!));
1487
1519
  let aimReach: ((c: Chain) => void) | null = null;
1488
1520
  const reachAims = new Map<string, { aim: number[]; pair: number[]; length: number }>();
1521
+ /** #282: per reach chain, the distance from its bone's posed origin to the anchor's on each frame (the last aim's). */
1522
+ const reachGaps = new Map<string, number[]>();
1489
1523
  if (reaches.length > 0 && t !== null && scale !== null) {
1490
1524
  const s = scale.factor;
1491
1525
  const tb = rest.get(t.bone)!;
@@ -1494,8 +1528,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
1494
1528
  const anchor = series.get(c.joints[0]!)!;
1495
1529
  const end = series.get(c.joints[1]!)!;
1496
1530
  const angles: number[] = [];
1531
+ const gaps: number[] = [];
1497
1532
  for (let i = 0; i < n; i++) {
1498
1533
  const o = posedOrigin(b, i);
1534
+ gaps.push(Math.hypot(o[0] - (tb.worldX + travel[i]![0]), o[1] - (tb.worldY + travel[i]![1])));
1499
1535
  const tx = tb.worldX + travel[i]![0] + (end[i]![0] - anchor[i]![0]) * s;
1500
1536
  const ty = tb.worldY + travel[i]![1] - (end[i]![1] - anchor[i]![1]) * s;
1501
1537
  const d = Math.hypot(tx - o[0], ty - o[1]);
@@ -1503,6 +1539,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
1503
1539
  }
1504
1540
  fillLeading(angles, () => p.add('POSE_SEGMENT_ZERO', `chain "${c.name}" — bone "${b}" reaches for ${c.joints[1]}, which the pose puts on the bone's posed origin on every frame; it has no direction`));
1505
1541
  targets.set(b, angles);
1542
+ reachGaps.set(c.name, gaps);
1506
1543
  // #236: the aim as reached, before its own range clamp, beside the pair's own angle (a reading).
1507
1544
  reachAims.set(c.name, { aim: [...angles], pair: Array.from({ length: n }, (_, i) => pictureAngle(anchor[i]!, end[i]!)), length: median(Array.from({ length: n }, (_, i) => Math.hypot(end[i]![0] - anchor[i]![0], end[i]![1] - anchor[i]![1]) * s)) });
1508
1545
  };
@@ -3087,6 +3124,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
3087
3124
  if (headNodTrack !== null) tracks.push(headNodTrack);
3088
3125
  tracks.push(...sw.offsetTracks, ...sw.slotTracks);
3089
3126
  const reach = reachReadings(rest, map, tracks, reachAims, t?.bone ?? null);
3127
+ const reachOrigin = reachOrigins(rest, map, reachAims, reachGaps, t?.bone ?? null);
3090
3128
 
3091
3129
  const pivot = pivotOf(rest, t?.bone ?? null, legs);
3092
3130
  const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
@@ -3099,8 +3137,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
3099
3137
  return {
3100
3138
  animation: { duration: keyTimes[n - 1]!, tracks },
3101
3139
  times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale, scaleFit,
3102
- switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
3103
- legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, pelvis,
3140
+ switches: sw.switches, switchRuns: sw.runs, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
3141
+ legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, reachOrigin, pelvis,
3104
3142
  };
3105
3143
  }
3106
3144
 
@@ -3209,6 +3247,47 @@ function reachReadings(rest: Rest, map: JointMap, tracks: ReadonlyArray<MotionTr
3209
3247
  return out;
3210
3248
  }
3211
3249
 
3250
+ /**
3251
+ * #282: a reach bone whose origin sits more than this fraction of the segment
3252
+ * from the source joint is warned. The aim from an origin a fraction f up the
3253
+ * pair turns about 1 ÷ (1 − f) times the pair's turn (#236's lever: the example
3254
+ * wave's trunk, f = 0.5, measured 2.00×); 0.2 is 1.25×, a quarter more turn
3255
+ * than the pose has.
3256
+ */
3257
+ export const REACH_ORIGIN_WARN = 0.2;
3258
+
3259
+ /** #282: each reach chain's origin against the anchor's, at setup and as posed, in map order. */
3260
+ function reachOrigins(rest: Rest, map: JointMap, aims: ReadonlyMap<string, { length: number }>, gaps: ReadonlyMap<string, number[]>, anchor: string | null): ReachOrigin[] {
3261
+ const out: ReachOrigin[] = [];
3262
+ if (anchor === null) return out;
3263
+ const a0 = rest.get(anchor)!;
3264
+ for (const c of map.chains) {
3265
+ const got = aims.get(c.name);
3266
+ const g = gaps.get(c.name);
3267
+ if (!c.reach || got === undefined || g === undefined) continue;
3268
+ const b = c.bones[0]!;
3269
+ const bo = rest.get(b)!;
3270
+ const setup = r3(Math.hypot(bo.worldX - a0.worldX, bo.worldY - a0.worldY));
3271
+ let frame = 0;
3272
+ for (let i = 1; i < g.length; i++) if (g[i]! > g[frame]!) frame = i;
3273
+ const max = r3(g[frame] ?? 0);
3274
+ const segment = r3(got.length);
3275
+ const ofSegment = got.length > 0 ? r3(Math.max(setup, max) / got.length) : 0;
3276
+ out.push({ chain: c.name, bone: b, joints: [c.joints[0]!, c.joints[1]!], anchor, setup, posed: { max, frame }, segment, ofSegment, warn: ofSegment > REACH_ORIGIN_WARN, threshold: REACH_ORIGIN_WARN });
3277
+ }
3278
+ return out;
3279
+ }
3280
+
3281
+ /** #282: the `REACH-ORIGIN` line, one per reach chain. */
3282
+ export function reachOriginLine(e: ReachOrigin, times: readonly number[]): string {
3283
+ return `chain "${e.chain}" bone "${e.bone}" (${e.joints[0]} → ${e.joints[1]}): its origin ${e.setup.toFixed(2)} units from the source joint ${e.joints[0]} (bone "${e.anchor}"'s origin) at setup, up to ${e.posed.max.toFixed(2)} on the frames (frame ${e.posed.frame}, t=${times[e.posed.frame]!.toFixed(3)}s): ${e.ofSegment.toFixed(3)} of the ${e.segment.toFixed(2)}-unit segment`;
3284
+ }
3285
+
3286
+ /** #282: the WARN of a reach chain whose origin sits past REACH_ORIGIN_WARN of the segment. */
3287
+ export function reachOriginWarn(e: ReachOrigin): string {
3288
+ return `REACH_ORIGIN: chain "${e.chain}" bone "${e.bone}" starts ${e.ofSegment.toFixed(3)} of the segment from ${e.joints[0]}, past ${e.threshold.toFixed(2)}: re-aimed from there, it turns more than the pair (JITTER bone "${e.bone}"); "reach": false follows the measured angle`;
3289
+ }
3290
+
3212
3291
  /** The `REACH` line of a reach chain whose aim departs from its pair (#236). */
3213
3292
  export function reachCostLine(e: ReachReading, times: readonly number[]): string {
3214
3293
  const f = (v: number | null) => (v === null ? 'not measured' : `${v.toFixed(4)}°/f²`);