@rig-c/pose 0.4.0 → 0.6.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 +6 -2
- package/package.json +1 -1
- package/src/map.ts +26 -3
- package/src/retarget.ts +2 -1
- package/src/switch.ts +39 -7
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ and what the run cannot decide for you it lists:
|
|
|
48
48
|
- `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton, and how far each bone with a `drawn` angle was turned from it.
|
|
49
49
|
- `UNCOVERED` — with `alternates` in the map: every fold on a frame where no switch took the folding mesh off screen. This is the list of poses still to paint.
|
|
50
50
|
- `RANGE` — a chain's `range` ([below](#a-joints-range-range)): a bone whose rotate key the pose asks past its range, outside every span of a set depicting its chain, is clamped at the limit; one line per run, with the frames, the seconds and the largest overflow. The demand for an alternate.
|
|
51
|
-
- `ART-RANGE` — inside a set's span: a bone with
|
|
51
|
+
- `ART-RANGE` — inside a set's span: a bone with a `drawn` angle in the set, turned from it past its limits — the set's `turn` for that bone (held at the limit) or, without one, its chain's `range` (the bone follows the pose). Listed as the pose asked it either way: the demand for another drawing of that set.
|
|
52
52
|
- `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
|
|
53
53
|
|
|
54
54
|
`--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance` and `switchMin` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, and the demand lists:
|
|
@@ -58,7 +58,7 @@ and what the run cannot decide for you it lists:
|
|
|
58
58
|
- `endOn` — above, under the short hold.
|
|
59
59
|
- `turns: [{ line, joints, frames, seconds, minFraction, reversed }]` — `line` is `hip`, `shoulder` or `eye`.
|
|
60
60
|
- `range: [{ chain, bone, frames, seconds, maxOverflow, key, limits }]` — `key` is the rotate key the pose asked for on the frame of the largest overflow, before the clamp.
|
|
61
|
-
- `artRange: [{ set, chain, bone, frames, seconds, maxOverflow, turn, limits }]` — `turn` is the signed art turn on that frame.
|
|
61
|
+
- `artRange: [{ set, chain, bone, frames, seconds, maxOverflow, turn, limits, clamped }]` — `turn` is the signed art turn the pose asked on that frame; `clamped` says whether the set's `turn` held the bone there.
|
|
62
62
|
|
|
63
63
|
Runs in `turns`, `range` and `artRange` merge across a gap of one frame, as `endOn` does; `seconds` runs from the first frame's key time to the end of the last frame.
|
|
64
64
|
|
|
@@ -171,6 +171,10 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
171
171
|
|
|
172
172
|
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds and smoothing, held and smoothed as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
|
|
173
173
|
|
|
174
|
+
|
|
175
|
+
**A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on HQ's dancer, the hands clasped behind the back and the hands low in front differ by only 2–21°, and the wrist and hand-point scores do not separate them either; the cue that might is #33's depth state. It is recorded in the map as a stopgap, not a selection rule.
|
|
176
|
+
|
|
177
|
+
**How far a set's art may turn: `turn`.** A drawing reads right only near the angle it was painted at. A set may state, per bone with a `drawn` angle, how far its art may turn from it, `"turn": { "sleeve_r0": [-20, 20] }`. While the set shows, the bone is held within it: its target becomes drawn + clamp(wrap(target − drawn), min, max), each bone on its own world angle, so a forearm under a held upper arm keeps its own target. The landing check measures against the held angle. The overflow the pose asked for is listed as `ART-RANGE`. A `turn` on a bone the set has no `drawn` angle for is refused (`MAP_ALT_TURN`).
|
|
174
178
|
## Requirements
|
|
175
179
|
|
|
176
180
|
[Bun](https://bun.sh) ≥ 1.2 on `PATH` — rig-pose, like rig-c, runs its TypeScript sources directly. `rig-c` comes as a dependency. With `@esotericsoftware/spine-core` installed beside it, rig-c's gate runs the spine-core round trip; without it, rig-c's own gate, and the gate's report says which.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/map.ts
CHANGED
|
@@ -91,6 +91,10 @@ export interface AlternateSet {
|
|
|
91
91
|
drawn: Record<string, number>;
|
|
92
92
|
/** Bone → a translate, in its parent's axes, held while the set shows. */
|
|
93
93
|
offsets: Record<string, [number, number]>;
|
|
94
|
+
/** Bone → how far its art may turn from `drawn` while the set shows, [min, max] degrees: the bone is clamped there, and the overflow is ART-RANGE demand. */
|
|
95
|
+
turn: Record<string, [number, number]>;
|
|
96
|
+
/** A stopgap: the only frames the set may show on, a list of [first, last] windows; on them it is chosen over any set without windows. Null: any frame. */
|
|
97
|
+
frames: Array<[number, number]> | null;
|
|
94
98
|
}
|
|
95
99
|
export interface JointMap {
|
|
96
100
|
chains: Chain[];
|
|
@@ -224,7 +228,7 @@ export function readMap(text: string, where: string): JointMap {
|
|
|
224
228
|
return { chains, translate, alternates };
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] } }';
|
|
231
|
+
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …] }';
|
|
228
232
|
|
|
229
233
|
/** `alternates`: each set's shape, its chains against the map's, its key skeleton against those chains' joints, and its bones against those chains' bones. */
|
|
230
234
|
function readAlternates(p: Problems, where: string, raw: unknown, chains: readonly Chain[], translate: Translate | null): AlternateSet[] {
|
|
@@ -241,7 +245,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
241
245
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
242
246
|
return;
|
|
243
247
|
}
|
|
244
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets']);
|
|
248
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames']);
|
|
245
249
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
246
250
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
247
251
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -331,7 +335,26 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
|
-
|
|
338
|
+
const turn: Record<string, [number, number]> = {};
|
|
339
|
+
if ('turn' in a) {
|
|
340
|
+
if (!isObject(a.turn)) p.add('MAP_ALT_TURN', `${label} "turn" is ${typeName(a.turn)}; it is { "<bone>": [min, max] }, how far each bone's art may turn from its drawn angle, degrees`);
|
|
341
|
+
else {
|
|
342
|
+
for (const [b, v] of Object.entries(a.turn)) {
|
|
343
|
+
if (!(b in drawn)) p.add('MAP_ALT_TURN', `${label} "turn" names bone "${b}", which has no "drawn" angle in the set; a turn is measured from the drawn angle (the set draws ${Object.keys(drawn).join(', ') || 'none'})`);
|
|
344
|
+
else if (!Array.isArray(v) || v.length !== 2 || !v.every((x) => typeof x === 'number' && Number.isFinite(x)) || (v[0] as number) > (v[1] as number)) p.add('MAP_ALT_TURN', `${label} "turn" bone "${b}" is ${JSON.stringify(v)}; it must be [min, max], two finite numbers of degrees with min ≤ max`);
|
|
345
|
+
else turn[b] = [v[0] as number, v[1] as number];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let frames: Array<[number, number]> | null = null;
|
|
350
|
+
if ('frames' in a) {
|
|
351
|
+
const f = a.frames;
|
|
352
|
+
const pair = (w: unknown): w is [number, number] => Array.isArray(w) && w.length === 2 && w.every((x) => Number.isInteger(x) && (x as number) >= 0) && (w[0] as number) <= (w[1] as number);
|
|
353
|
+
if (!Array.isArray(f) || f.length === 0 || !f.every(pair)) {
|
|
354
|
+
p.add('MAP_ALT_FRAMES', `${label} "frames" is ${JSON.stringify(f)}; it must be a non-empty list of windows [[first, last], …], whole frame numbers ≥ 0 with first ≤ last`);
|
|
355
|
+
} else frames = (f as Array<[number, number]>).map((w) => [w[0], w[1]]);
|
|
356
|
+
}
|
|
357
|
+
if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn, frames });
|
|
335
358
|
});
|
|
336
359
|
return out;
|
|
337
360
|
}
|
package/src/retarget.ts
CHANGED
|
@@ -67,7 +67,7 @@ import { aimAngle, rigPoint } from './map.ts';
|
|
|
67
67
|
import type { Joint } from './pose.ts';
|
|
68
68
|
import { Problems } from './problems.ts';
|
|
69
69
|
import type { ArtRange, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
70
|
-
import { coveredChains, measureArt, overflow, RANGE_SLACK, runsOf, selectSwitches } from './switch.ts';
|
|
70
|
+
import { clampArt, coveredChains, measureArt, overflow, RANGE_SLACK, runsOf, selectSwitches } from './switch.ts';
|
|
71
71
|
|
|
72
72
|
const DEG = 180 / Math.PI;
|
|
73
73
|
|
|
@@ -682,6 +682,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
682
682
|
}
|
|
683
683
|
p.throwIfAny();
|
|
684
684
|
const artRange = measureArt(sw.switches, map, targets, endOf, keyTimes, RANGE_GAP);
|
|
685
|
+
clampArt(sw.switches, map, targets);
|
|
685
686
|
|
|
686
687
|
// Keys: the bone's turn less the turn of the nearest mapped bone above it.
|
|
687
688
|
const tracks: Array<MotionTrack | SlotTrack> = [];
|
package/src/switch.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface ArtRange {
|
|
|
90
90
|
turn: number;
|
|
91
91
|
/** The bone's range, degrees. */
|
|
92
92
|
limits: [number, number];
|
|
93
|
+
/** Whether the bone was held at the limit (the set's `turn`) or followed the pose (its chain's `range` only). */
|
|
94
|
+
clamped: boolean;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export interface Uncovered {
|
|
@@ -184,13 +186,20 @@ export function selectSwitches(map: JointMap, targets: ReadonlyMap<string, reado
|
|
|
184
186
|
for (const group of competing(map.alternates)) {
|
|
185
187
|
const pick: Array<AlternateSet | null> = [];
|
|
186
188
|
for (let i = 0; i < n; i++) {
|
|
189
|
+
// A set with a `frames` window shows only inside it, and there it is chosen
|
|
190
|
+
// over any set without one (a stopgap for what the angles cannot tell apart).
|
|
187
191
|
let best: AlternateSet | null = null;
|
|
188
192
|
let bestD = Infinity;
|
|
193
|
+
let bestWindowed = false;
|
|
189
194
|
for (const a of group) {
|
|
195
|
+
if (a.frames !== null && !a.frames.some(([f, l]) => i >= f && i <= l)) continue;
|
|
190
196
|
const d = distances.get(a.name)![i]!;
|
|
191
|
-
if (d
|
|
197
|
+
if (d > opts.switchTolerance) continue;
|
|
198
|
+
const windowed = a.frames !== null;
|
|
199
|
+
if ((windowed && !bestWindowed) || (windowed === bestWindowed && d < bestD)) {
|
|
192
200
|
best = a;
|
|
193
201
|
bestD = d;
|
|
202
|
+
bestWindowed = windowed;
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
pick.push(best);
|
|
@@ -321,9 +330,11 @@ export const overflow = (v: number, [min, max]: readonly [number, number]): numb
|
|
|
321
330
|
|
|
322
331
|
/**
|
|
323
332
|
* The art turn of each switch, in place (`artTurn`), and the art-range demand:
|
|
324
|
-
* inside a span, per bone with
|
|
325
|
-
* its chain
|
|
326
|
-
* outside
|
|
333
|
+
* inside a span, per bone with a `drawn` angle in the set and limits — the
|
|
334
|
+
* set's `turn` for it, else its chain's `range` — every frame whose signed art
|
|
335
|
+
* turn wrap(target − drawn) falls outside them, in runs merged across a gap of
|
|
336
|
+
* `gap` frames. Measured on the targets as the pose asks them, before
|
|
337
|
+
* `clampArt` holds the set's `turn` limits.
|
|
327
338
|
*/
|
|
328
339
|
export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyMap<string, readonly number[]>, endOf: (i: number) => number, times: readonly number[], gap: number): ArtRange[] {
|
|
329
340
|
const out: ArtRange[] = [];
|
|
@@ -336,7 +347,7 @@ export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyM
|
|
|
336
347
|
const abs = turns.map(Math.abs);
|
|
337
348
|
sw.artTurn[bone] = { max: r3(Math.max(...abs)), median: r3(median(abs)) };
|
|
338
349
|
const chain = map.chains.find((c) => c.bones.includes(bone))!;
|
|
339
|
-
const limits = chain.range[bone];
|
|
350
|
+
const limits = set.turn[bone] ?? chain.range[bone];
|
|
340
351
|
if (limits === undefined) continue;
|
|
341
352
|
const flag = turns.map((t) => overflow(t, limits) > RANGE_SLACK);
|
|
342
353
|
for (const [a, b] of runsOf(flag, gap)) {
|
|
@@ -344,7 +355,7 @@ export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyM
|
|
|
344
355
|
for (let k = a; k <= b; k++) if (overflow(turns[k]!, limits) > overflow(turns[worst]!, limits)) worst = k;
|
|
345
356
|
const first = sw.frames[0] + a;
|
|
346
357
|
const last = sw.frames[0] + b;
|
|
347
|
-
out.push({ set: sw.set, chain: chain.name, bone, frames: [first, last], seconds: r6(endOf(last) - times[first]!), maxOverflow: r3(overflow(turns[worst]!, limits)), turn: r3(turns[worst]!), limits });
|
|
358
|
+
out.push({ set: sw.set, chain: chain.name, bone, frames: [first, last], seconds: r6(endOf(last) - times[first]!), maxOverflow: r3(overflow(turns[worst]!, limits)), turn: r3(turns[worst]!), limits, clamped: bone in set.turn });
|
|
348
359
|
}
|
|
349
360
|
}
|
|
350
361
|
}
|
|
@@ -426,5 +437,26 @@ export function switchLine(s: Switch, times: readonly number[]): string {
|
|
|
426
437
|
}
|
|
427
438
|
|
|
428
439
|
export function artRangeLine(e: ArtRange, times: readonly number[]): string {
|
|
429
|
-
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it — demand for another drawing of the set`;
|
|
440
|
+
return `set "${e.set}" chain "${e.chain}" bone "${e.bone}" turned ${sg(e.turn)} from drawn (range [${e.limits[0]}, ${e.limits[1]}]) ${spanOf(e.frames, times)}: ${e.seconds.toFixed(3)}s, up to ${deg(e.maxOverflow)} past it; ${e.clamped ? 'held at the limit' : 'the bone follows the pose'} — demand for another drawing of the set`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Inside each span, a bone with a `turn` in the set is held within it: its
|
|
445
|
+
* target world angle is drawn + clamp(wrap(target − drawn), min, max). Each
|
|
446
|
+
* bone is clamped on its own world angle, and keys are differences of world
|
|
447
|
+
* angles, so a forearm under a clamped upper arm keeps its own target.
|
|
448
|
+
*/
|
|
449
|
+
export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
|
|
450
|
+
for (const sw of switches) {
|
|
451
|
+
const set = map.alternates.find((a) => a.name === sw.set)!;
|
|
452
|
+
for (const [bone, limits] of Object.entries(set.turn)) {
|
|
453
|
+
const drawn = set.drawn[bone]!;
|
|
454
|
+
const target = targets.get(bone)!;
|
|
455
|
+
for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
|
|
456
|
+
const t = wrap(target[i]! - drawn);
|
|
457
|
+
const c = Math.min(Math.max(t, limits[0]), limits[1]);
|
|
458
|
+
if (Math.abs(c - t) > RANGE_SLACK) target[i] = wrap(drawn + c);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
430
462
|
}
|