@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 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 both a `drawn` angle in the set and a `range`, turned from its drawn angle past that range. Not clamped; the demand for another drawing of that set.
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.4.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
- if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets });
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 <= opts.switchTolerance && d < bestD) {
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 both a `drawn` angle in the set and a `range` on
325
- * its chain, every frame whose signed art turn wrap(target − drawn) falls
326
- * outside that range, in runs merged across a gap of `gap` frames.
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
  }