@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/README.md +36 -3
- package/cli.ts +40 -1
- package/package.json +1 -1
- package/src/clean.ts +50 -3
- package/src/map.ts +24 -3
- package/src/pelvis.ts +413 -0
- package/src/pose.ts +126 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +6 -3
- package/src/switch.ts +109 -21
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 {
|
|
56
|
-
import {
|
|
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
|
-
|
|
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 {
|