@rig-c/pose 0.37.1 → 0.38.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
@@ -12,6 +12,11 @@
12
12
  * span is dropped, and the base art shows there; a gap shorter than the minimum
13
13
  * span between two runs of one set is bridged by that set. No other hysteresis.
14
14
  *
15
+ * A set with a `phase` (rig-c/pose#286) is not selected by distance: it shows
16
+ * exactly on its supporting foot's spans (`phaseSupports`, the
17
+ * report's `feet.supports`) and switches on the hand-over, with none of the
18
+ * rules here or below applied to it.
19
+ *
15
20
  * Sets compete only with the sets they share a slot with (shown or hidden,
16
21
  * transitively), or a bone they offset: two that touch nothing in common — a
17
22
  * drawing of each arm — can show at once, and two that do never overlap, so
@@ -52,8 +57,9 @@
52
57
  * selection put it; it is the CLI's default, because on HQ's dancer the bounded
53
58
  * peak still raised the sets' ART-RANGE runs from 24 to 31 (#31, round 2).
54
59
  */
55
- import type { AlternateSet, JointMap, Rest, RigSlots } from './map.ts';
56
- import { descends } from './map.ts';
60
+ import type { Side } from './legs.ts';
61
+ import type { AlternateSet, JointMap, Phase, Rest, RigSlots } from './map.ts';
62
+ import { descends, PHASE_SIDE } from './map.ts';
57
63
  import type { MotionKey, MotionTrack } from './retarget.ts';
58
64
  import { pictureAngle, wrap } from './retarget.ts';
59
65
  import type { MeshFolds } from './rigc.ts';
@@ -80,6 +86,19 @@ export interface SwitchOptions {
80
86
  shortInfo?: ShortSwitchInfo;
81
87
  /** Per chain, the reference pose's normalised wrist-from-shoulder offset on each frame, `(wrist - shoulder) / scale`, y down (#180). */
82
88
  handOffsets?: ReadonlyMap<string, ReadonlyArray<readonly [number, number]>>;
89
+ /** The report's `feet.supports` (rig-c/pose#286): the spans a set with a `phase` shows on. Given only when a set declares one. */
90
+ phaseSupports?: ReadonlyArray<{ side: Side; frames: [number, number] }>;
91
+ }
92
+
93
+ /** A phase set's span (rig-c/pose#286): its step side, and the support hand-overs it was switched on and off at. */
94
+ export interface PhaseSpan {
95
+ step: Phase;
96
+ /** The supporting foot it shows on (left-step: the left foot is stepping, so the right foot supports). */
97
+ side: Side;
98
+ /** The hand-over it starts on: the frame, and the side that supported on the frame before (null: no foot). Null from the first frame. */
99
+ on: { frame: number; from: Side | null } | null;
100
+ /** The hand-over it ends on: the first frame after it, and the side that supports there (null: no foot). Null to the end. */
101
+ off: { frame: number; to: Side | null } | null;
83
102
  }
84
103
 
85
104
  /** One boundary of a span: where the selection put it, where it is keyed, and how fast the chains it changes move on each. */
@@ -115,6 +134,8 @@ export interface Switch {
115
134
  artTurn: Record<string, { max: number; median: number }>;
116
135
  /** The boundary it starts on (null from the first frame) and the one it ends on (null to the end); a boundary between two sets is in both. */
117
136
  moved: { on: Move | null; off: Move | null };
137
+ /** Only on a set with a `phase` (rig-c/pose#286): the step side and the hand-overs the span was switched on. */
138
+ phase?: PhaseSpan;
118
139
  }
119
140
 
120
141
  export interface AttachmentKey {
@@ -245,6 +266,28 @@ function touches(a: AlternateSet): string[] {
245
266
  return [...Object.keys(a.slots), ...a.hides].map((s) => `slot:${s}`).concat(Object.keys(a.offsets).map((b) => `bone:${b}`));
246
267
  }
247
268
 
269
+ /**
270
+ * rig-c/pose#286: what the retarget refuses about the map's phase sets, one text each. A group of competing sets (a
271
+ * common slot or offset bone, transitively) that holds a phase set must hold only phase sets, and at most one of each
272
+ * side: the hand-over decides which of them shows, and nothing would decide between two of one side, or between a phase
273
+ * set and an angle set over the frames they share.
274
+ */
275
+ export function phaseGroupProblems(sets: readonly AlternateSet[]): string[] {
276
+ const out: string[] = [];
277
+ for (const group of competing(sets)) {
278
+ const phased = group.filter((a) => a.phase !== undefined);
279
+ if (phased.length === 0) continue;
280
+ const angle = group.filter((a) => a.phase === undefined);
281
+ const named = (xs: readonly AlternateSet[]) => xs.map((a) => `"${a.name}"`).join(', ');
282
+ if (angle.length > 0) out.push(`set(s) ${named(phased)} declare "phase" and compete with set(s) ${named(angle)}, which declare none (they switch a common slot or offset bone); a phase set shows on its supporting foot's spans and an angle set on its distance, and nothing decides between them — give every set of the group a "phase", or none`);
283
+ for (const step of Object.keys(PHASE_SIDE) as Phase[]) {
284
+ const same = phased.filter((a) => a.phase === step);
285
+ if (same.length > 1) out.push(`set(s) ${named(same)} all declare "phase": "${step}" and compete (they switch a common slot or offset bone); one drawing shows per support span — keep one "${step}" set per group`);
286
+ }
287
+ }
288
+ return out;
289
+ }
290
+
248
291
  /** Groups of sets that switch a common slot or offset a common bone, transitively; each in map order. */
249
292
  function competing(sets: readonly AlternateSet[]): AlternateSet[][] {
250
293
  const parent = sets.map((_, i) => i);
@@ -447,8 +490,41 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
447
490
  const distances = setDistances(map, targets, n, opts.shortInfo);
448
491
  const hyst = opts.switchHysteresis ?? SWITCH_LEAVE_HYSTERESIS;
449
492
  const switches: Switch[] = [];
493
+ // 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
+ const supportAt: Array<Side | null> = Array.from({ length: n }, () => null);
495
+ 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;
496
+ /** One group's pick, as segments between boundaries, then each boundary moved (#31), pushed as switches. */
497
+ const spans = (pick: ReadonlyArray<AlternateSet | null>, moveOpts: SwitchOptions): void => {
498
+ const vals: Array<AlternateSet | null> = [pick[0] ?? null];
499
+ const pos: number[] = [];
500
+ for (let i = 1; i < n; i++) {
501
+ if (pick[i] === pick[i - 1]) continue;
502
+ pos.push(i);
503
+ vals.push(pick[i] ?? null);
504
+ }
505
+ const moves = moveBoundaries(vals, pos, map, targets, distances, times, duration, moveOpts);
506
+ for (let s = 0; s < vals.length; s++) {
507
+ const a = vals[s];
508
+ if (a === null || a === undefined) continue;
509
+ const i = s === 0 ? 0 : moves[s - 1]!.frame[1];
510
+ const j = s < pos.length ? moves[s]!.frame[1] - 1 : n - 1;
511
+ const from = times[i]!;
512
+ const to = j + 1 < n ? times[j + 1]! : duration;
513
+ const ds = distances.get(a.name)!.slice(i, j + 1);
514
+ const phase: PhaseSpan | undefined = a.phase === undefined ? undefined : { step: a.phase, side: PHASE_SIDE[a.phase], on: i === 0 ? null : { frame: i, from: supportAt[i - 1] ?? null }, off: j + 1 < n ? { frame: j + 1, to: supportAt[j + 1] ?? null } : null };
515
+ switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {}, moved: { on: s === 0 ? null : moves[s - 1]!, off: s < pos.length ? moves[s]! : null }, ...(phase === undefined ? {} : { phase }) });
516
+ }
517
+ };
450
518
  const groups = competing(map.alternates);
451
519
  for (const group of groups) {
520
+ // rig-c/pose#286: a group of phase sets shows each exactly on its supporting foot's spans — no tolerance, no minimum run,
521
+ // no hysteresis, no bridging, and its boundaries stay on the hand-overs (no --switch-at peak move). The retarget refuses
522
+ // a group mixing phase and angle sets, or two sets of one side (`phaseGroupProblems`), before it gets here.
523
+ if (group.some((a) => a.phase !== undefined)) {
524
+ const pick = supportAt.map((side) => (side === null ? null : group.find((a) => a.phase !== undefined && PHASE_SIDE[a.phase] === side) ?? null));
525
+ spans(pick, { ...opts, switchAt: 'first' });
526
+ continue;
527
+ }
452
528
  const layer = groupLayering(group, map, slots);
453
529
  const pick: Array<AlternateSet | null> = [];
454
530
  for (let i = 0; i < n; i++) {
@@ -582,25 +658,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
582
658
  }
583
659
  if (dropped) bridgeSame(pick);
584
660
  }
585
- // The segments between boundaries, then each boundary moved (#31).
586
- const vals: Array<AlternateSet | null> = [pick[0] ?? null];
587
- const pos: number[] = [];
588
- for (let i = 1; i < n; i++) {
589
- if (pick[i] === pick[i - 1]) continue;
590
- pos.push(i);
591
- vals.push(pick[i] ?? null);
592
- }
593
- const moves = moveBoundaries(vals, pos, map, targets, distances, times, duration, opts);
594
- for (let s = 0; s < vals.length; s++) {
595
- const a = vals[s];
596
- if (a === null || a === undefined) continue;
597
- const i = s === 0 ? 0 : moves[s - 1]!.frame[1];
598
- const j = s < pos.length ? moves[s]!.frame[1] - 1 : n - 1;
599
- const from = times[i]!;
600
- const to = j + 1 < n ? times[j + 1]! : duration;
601
- const ds = distances.get(a.name)!.slice(i, j + 1);
602
- switches.push({ set: a.name, chains: a.chains, slots: a.slots, hides: a.hides, offsets: a.offsets, from, to, frames: [i, j], distance: { max: r3(Math.max(...ds)), median: r3(median(ds)) }, artTurn: {}, moved: { on: s === 0 ? null : moves[s - 1]!, off: s < pos.length ? moves[s]! : null } });
603
- }
661
+ spans(pick, opts);
604
662
  }
605
663
  const order = (name: string) => map.alternates.findIndex((a) => a.name === name);
606
664
  switches.sort((x, y) => x.frames[0] - y.frames[0] || order(x.set) - order(y.set));
@@ -632,6 +690,7 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
632
690
  // and in a one-sided gesture set keep an arm whose wrist stays behind the torso on its previous behind set.
633
691
  const chainHolds: Array<{ set: string; chain: string; frames: [number, number] }> = [];
634
692
  for (const group of groups) {
693
+ if (group.some((a) => a.phase !== undefined)) continue; // #286: a phase group shows exactly its support spans
635
694
  const layer = groupLayering(group, map, slots);
636
695
  if (!layer.baseInFront) continue;
637
696
  const names = new Set(group.map((a) => a.name));
@@ -1193,6 +1252,35 @@ export function switchLine(s: Switch, times: readonly number[]): string {
1193
1252
  return parts.join('; ');
1194
1253
  }
1195
1254
 
1255
+ /**
1256
+ * rig-c/pose#286: one `PHASE` line per set with a `phase` — the support side it shows on, every span it showed, and the
1257
+ * hand-over each span was switched on and off at (the side before → the side after; "none" where no foot supports).
1258
+ */
1259
+ export function phaseLines(map: JointMap, switches: readonly Switch[]): string[] {
1260
+ const out: string[] = [];
1261
+ const sideText = (x: Side | null) => x ?? 'none';
1262
+ for (const set of map.alternates) {
1263
+ if (set.phase === undefined) continue;
1264
+ const side = PHASE_SIDE[set.phase];
1265
+ const foot = (x: Side) => (x === 'l' ? 'left' : 'right');
1266
+ const head = `set "${set.name}" "${set.phase}" — the ${foot(side === 'l' ? 'r' : 'l')} foot is stepping; shown while the ${foot(side)} foot supports (feet.supports)`;
1267
+ const own = switches.filter((s) => s.set === set.name && s.phase !== undefined);
1268
+ if (own.length === 0) {
1269
+ out.push(`${head}: on no frame — no ${side} support span`);
1270
+ continue;
1271
+ }
1272
+ const count = own.reduce((t, s) => t + s.frames[1] - s.frames[0] + 1, 0);
1273
+ const spansText = own.map((s) => {
1274
+ const ph = s.phase!;
1275
+ const on = ph.on === null ? 'on the first frame' : `on frame ${ph.on.frame} ${sideText(ph.on.from)} → ${side}`;
1276
+ const off = ph.off === null ? 'off at the end' : `off frame ${ph.off.frame} ${side} → ${sideText(ph.off.to)}`;
1277
+ return `${s.frames[0]}–${s.frames[1]} (${on}, ${off})`;
1278
+ });
1279
+ out.push(`${head}: ${own.length} span(s), ${count} frame(s): ${spansText.join(', ')}`);
1280
+ }
1281
+ return out;
1282
+ }
1283
+
1196
1284
  const spd = (v: number) => `${v.toFixed(1)}°/frame`;
1197
1285
  const win = (w: [number, number]) => (w[0] === w[1] ? `window frame ${w[0]}` : `window frames ${w[0]}–${w[1]}`);
1198
1286
  function moveText(m: Move): string {