@rig-c/pose 0.4.0 → 0.5.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,8 @@ 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
+ **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
176
  ## Requirements
175
177
 
176
178
  [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.5.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,8 @@ 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]>;
94
96
  }
95
97
  export interface JointMap {
96
98
  chains: Chain[];
@@ -224,7 +226,7 @@ export function readMap(text: string, where: string): JointMap {
224
226
  return { chains, translate, alternates };
225
227
  }
226
228
 
227
- const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] } }';
229
+ 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] } }';
228
230
 
229
231
  /** `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
232
  function readAlternates(p: Problems, where: string, raw: unknown, chains: readonly Chain[], translate: Translate | null): AlternateSet[] {
@@ -241,7 +243,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
241
243
  p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
242
244
  return;
243
245
  }
244
- refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets']);
246
+ refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn']);
245
247
  const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
246
248
  if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
247
249
  else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
@@ -331,7 +333,18 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
331
333
  }
332
334
  }
333
335
  }
334
- if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets });
336
+ const turn: Record<string, [number, number]> = {};
337
+ if ('turn' in a) {
338
+ 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`);
339
+ else {
340
+ for (const [b, v] of Object.entries(a.turn)) {
341
+ 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'})`);
342
+ 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`);
343
+ else turn[b] = [v[0] as number, v[1] as number];
344
+ }
345
+ }
346
+ }
347
+ if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn });
335
348
  });
336
349
  return out;
337
350
  }
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 {
@@ -321,9 +323,11 @@ export const overflow = (v: number, [min, max]: readonly [number, number]): numb
321
323
 
322
324
  /**
323
325
  * 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.
326
+ * inside a span, per bone with a `drawn` angle in the set and limits — the
327
+ * set's `turn` for it, else its chain's `range` — every frame whose signed art
328
+ * turn wrap(target − drawn) falls outside them, in runs merged across a gap of
329
+ * `gap` frames. Measured on the targets as the pose asks them, before
330
+ * `clampArt` holds the set's `turn` limits.
327
331
  */
328
332
  export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyMap<string, readonly number[]>, endOf: (i: number) => number, times: readonly number[], gap: number): ArtRange[] {
329
333
  const out: ArtRange[] = [];
@@ -336,7 +340,7 @@ export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyM
336
340
  const abs = turns.map(Math.abs);
337
341
  sw.artTurn[bone] = { max: r3(Math.max(...abs)), median: r3(median(abs)) };
338
342
  const chain = map.chains.find((c) => c.bones.includes(bone))!;
339
- const limits = chain.range[bone];
343
+ const limits = set.turn[bone] ?? chain.range[bone];
340
344
  if (limits === undefined) continue;
341
345
  const flag = turns.map((t) => overflow(t, limits) > RANGE_SLACK);
342
346
  for (const [a, b] of runsOf(flag, gap)) {
@@ -344,7 +348,7 @@ export function measureArt(switches: Switch[], map: JointMap, targets: ReadonlyM
344
348
  for (let k = a; k <= b; k++) if (overflow(turns[k]!, limits) > overflow(turns[worst]!, limits)) worst = k;
345
349
  const first = sw.frames[0] + a;
346
350
  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 });
351
+ 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
352
  }
349
353
  }
350
354
  }
@@ -426,5 +430,26 @@ export function switchLine(s: Switch, times: readonly number[]): string {
426
430
  }
427
431
 
428
432
  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`;
433
+ 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`;
434
+ }
435
+
436
+ /**
437
+ * Inside each span, a bone with a `turn` in the set is held within it: its
438
+ * target world angle is drawn + clamp(wrap(target − drawn), min, max). Each
439
+ * bone is clamped on its own world angle, and keys are differences of world
440
+ * angles, so a forearm under a clamped upper arm keeps its own target.
441
+ */
442
+ export function clampArt(switches: readonly Switch[], map: JointMap, targets: Map<string, number[]>): void {
443
+ for (const sw of switches) {
444
+ const set = map.alternates.find((a) => a.name === sw.set)!;
445
+ for (const [bone, limits] of Object.entries(set.turn)) {
446
+ const drawn = set.drawn[bone]!;
447
+ const target = targets.get(bone)!;
448
+ for (let i = sw.frames[0]; i <= sw.frames[1]; i++) {
449
+ const t = wrap(target[i]! - drawn);
450
+ const c = Math.min(Math.max(t, limits[0]), limits[1]);
451
+ if (Math.abs(c - t) > RANGE_SLACK) target[i] = wrap(drawn + c);
452
+ }
453
+ }
454
+ }
430
455
  }