@rig-c/pose 0.39.0 → 0.40.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
@@ -180,8 +180,28 @@ export interface Uncovered {
180
180
  times: number[];
181
181
  }
182
182
 
183
+ /** One run of frames (rig-c/pose#279): first and last frame, how many, and the seconds the minimum span measured it by (first frame's time to the next frame's, the animation's end for the last). */
184
+ export interface FrameRun {
185
+ frames: [number, number];
186
+ count: number;
187
+ seconds: number;
188
+ }
189
+
190
+ /**
191
+ * rig-c/pose#279: per set selected by angle (not `phase`), the runs of frames on which it was the nearest set within
192
+ * the tolerance (inside its `frames` window, before bridging, hysteresis and the minimum span), and the runs the
193
+ * minimum span dropped — shorter than `switchMin` where they stood — on frames the set did not show in the end.
194
+ */
195
+ export interface SetRuns {
196
+ set: string;
197
+ nearest: FrameRun[];
198
+ suppressed: FrameRun[];
199
+ }
200
+
183
201
  export interface Switches {
184
202
  switches: Switch[];
203
+ /** rig-c/pose#279: one per set without a `phase`, in the map's order. */
204
+ runs: SetRuns[];
185
205
  /** The `attachment` tracks, one per slot whose picture changes. */
186
206
  slotTracks: SlotTrack[];
187
207
  /** The offsets' `translate` tracks, one per bone a shown set offsets. */
@@ -490,6 +510,11 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
490
510
  const distances = setDistances(map, targets, n, opts.shortInfo);
491
511
  const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
492
512
  const switches: Switch[] = [];
513
+ // rig-c/pose#279: per angle set, the frames it was nearest on and the frames the minimum span dropped it from.
514
+ const nearestOn = new Map<string, boolean[]>();
515
+ const droppedOn = new Map<string, boolean[]>();
516
+ const runOf = (i: number, j: number): FrameRun => ({ frames: [i, j], count: j - i + 1, seconds: r3((j + 1 < n ? times[j + 1]! : duration) - times[i]!) });
517
+ const runsOfFlags = (flag: readonly boolean[]): FrameRun[] => runsOf(flag, 0).map(([i, j]) => runOf(i, j));
493
518
  // rig-c/pose#286: the supporting side on each frame, from the report's support spans (null: no foot, or no set declares a phase).
494
519
  const supportAt: Array<Side | null> = Array.from({ length: n }, () => null);
495
520
  for (const sp of opts.phaseSupports ?? []) for (let i = sp.frames[0]; i <= Math.min(sp.frames[1], n - 1); i++) supportAt[i] = sp.side;
@@ -546,6 +571,10 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
546
571
  }
547
572
  pick.push(best);
548
573
  }
574
+ for (const a of group) {
575
+ nearestOn.set(a.name, pick.map((p) => p === a));
576
+ droppedOn.set(a.name, Array.from({ length: n }, () => false));
577
+ }
549
578
  const bridgeSame = (arr: Array<AlternateSet | null>) => {
550
579
  for (let i = 0; i < n;) {
551
580
  const a = arr[i];
@@ -604,7 +633,13 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
604
633
  let j = i;
605
634
  while (j + 1 < n && pick[j + 1] === a) j++;
606
635
  const to = j + 1 < n ? times[j + 1]! : duration;
607
- if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) for (let k = i; k <= j; k++) pick[k] = null;
636
+ if (a !== null && a !== undefined && to - times[i]! < opts.switchMin - 1e-9) {
637
+ const dropped = droppedOn.get(a.name)!;
638
+ for (let k = i; k <= j; k++) {
639
+ pick[k] = null;
640
+ dropped[k] = true;
641
+ }
642
+ }
608
643
  i = j + 1;
609
644
  }
610
645
  if (hyst > 0) {
@@ -658,8 +693,14 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
658
693
  }
659
694
  if (dropped) bridgeSame(pick);
660
695
  }
696
+ // rig-c/pose#279: a dropped frame the set came back to (hysteresis, bridging) was not suppressed.
697
+ for (const a of group) {
698
+ const dropped = droppedOn.get(a.name)!;
699
+ for (let k = 0; k < n; k++) if (pick[k] === a) dropped[k] = false;
700
+ }
661
701
  spans(pick, opts);
662
702
  }
703
+ const runs: SetRuns[] = map.alternates.filter((a) => nearestOn.has(a.name)).map((a) => ({ set: a.name, nearest: runsOfFlags(nearestOn.get(a.name)!), suppressed: runsOfFlags(droppedOn.get(a.name)!) }));
663
704
  const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
664
705
  switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
665
706
 
@@ -1094,7 +1135,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
1094
1135
  for (let k = 0; k + 1 < keys.length; k++) keys[k]!.ease = 'stepped';
1095
1136
  offsetTracks.push({ bone: b, property: 'translate', keys });
1096
1137
  }
1097
- return { switches, slotTracks, offsetTracks, shown, distances, chainHolds };
1138
+ return { switches, runs, slotTracks, offsetTracks, shown, distances, chainHolds };
1098
1139
  }
1099
1140
 
1100
1141
  /** Per chain, the frames on which a switch of a set depicting it shows: its art was painted for that pose. */
@@ -1281,6 +1322,27 @@ export function phaseLines(map: JointMap, switches: readonly Switch[]): string[]
1281
1322
  return out;
1282
1323
  }
1283
1324
 
1325
+ /** How many runs a `SWITCH-MIN` line lists before it points at the report for the rest. */
1326
+ export const SWITCH_MIN_LISTED = 4;
1327
+
1328
+ /**
1329
+ * rig-c/pose#279: one `SWITCH-MIN` line per set the minimum span held back — the runs it dropped (frames, count,
1330
+ * seconds), and the set's nearest runs (how many, and the longest) — so a set that matched but was held back reads
1331
+ * apart from one that never matched. A set with nothing suppressed prints no line.
1332
+ */
1333
+ export function switchMinLines(runs: readonly SetRuns[], switchMin: number): string[] {
1334
+ const runText = (r: FrameRun) => `${r.count === 1 ? `frame ${r.frames[0]}` : `frames ${r.frames[0]}–${r.frames[1]}`} (${r.count} frame${r.count === 1 ? '' : 's'}, ${r.seconds.toFixed(3)}s)`;
1335
+ const out: string[] = [];
1336
+ for (const s of runs) {
1337
+ if (s.suppressed.length === 0) continue;
1338
+ const listed = s.suppressed.slice(0, SWITCH_MIN_LISTED).map(runText).join(', ');
1339
+ const more = s.suppressed.length > SWITCH_MIN_LISTED ? `, and ${s.suppressed.length - SWITCH_MIN_LISTED} more (the report's switchRuns lists every one)` : '';
1340
+ const longest = s.nearest.reduce((m, r) => (r.count > m.count ? r : m), s.nearest[0]!);
1341
+ out.push(`set "${s.set}": ${s.suppressed.length} run(s) shorter than --switch-min ${switchMin}s not shown: ${listed}${more}; nearest on ${s.nearest.length} run(s), the longest ${runText(longest)}`);
1342
+ }
1343
+ return out;
1344
+ }
1345
+
1284
1346
  const spd = (v: number) => `${v.toFixed(1)}°/frame`;
1285
1347
  const win = (w: [number, number]) => (w[0] === w[1] ? `window frame ${w[0]}` : `window frames ${w[0]}–${w[1]}`);
1286
1348
  function moveText(m: Move): string {