@rig-c/pose 0.35.0 → 0.36.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
@@ -16,7 +16,7 @@ Part of the rig-c toolchain: [core](https://github.com/rig-c/core) compiles and
16
16
  rig-pose retarget --rig examples/wave/rig --map examples/wave/map.json --sequence examples/wave/pose.json --out wave.motion.json --build wave-build
17
17
  ```
18
18
 
19
- `pose.json` is already cleaned (`make-pose.ts` passes it through `clean`). It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json`, its report `wave.motion.report.json` and the gated build. `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
19
+ `pose.json` is already cleaned (`make-pose.ts` passes it through `clean`). It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json`, its report `wave.motion.report.json` and the gated build, with its record `retarget.json` beside it ([below](#the-builds-record-retargetjson)). `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
20
20
 
21
21
  ## What the rig needs
22
22
 
@@ -220,6 +220,31 @@ Both have a meaningless direction; only the first is a pose the rig cannot draw.
220
220
 
221
221
  Re-checked on a @rig-c/parts fixture rig with the short hold, the flip guard and clamped bends. On `dance-b` the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with the flip guard alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On `dance-a` `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands. When `dance-b`'s pelvis is mapped (`r_hip → l_hip`), its side-on turn (frames 463–491, body yaw +79.5°) foreshortens the hip line to 0.17 of standing and crosses `r_hip.x` past `l_hip.x` on frames 482–484 (+177.4° / −179.4°); `holdAcross` ([#184](https://github.com/rig-c/pose/issues/184)) holds `hip.rotate` across the unconfident span (`[−2.2°, +2.6°]` over 463–491, `0` `FLIP`s) without listing horizontal lines as limb `END-ON`. The default stays off: a default needs its own criterion and a third sequence.
222
222
 
223
+ ### The build's record: `retarget.json`
224
+
225
+ `--build <dir>` keeps the gated build — rigc's own files, byte for byte as rigc wrote them — and beside them one more file, `retarget.json` (`rig-pose-retarget/1`, [#270](https://github.com/rig-c/pose/issues/270)): what made the build. It is written once the kept build is green and the inputs are re-read unchanged, and a `RECORD` line says so (`RECORD retarget.json written beside the build (reference: <path> | none given)`). Its keys, in this order:
226
+
227
+ ```json
228
+ {
229
+ "spec": "rig-pose-retarget/1",
230
+ "pose": { "version": "<this package's version>" },
231
+ "rigc": { "version": "<the rig-c that gated the run, as the RIGC line names it>" },
232
+ "inputs": {
233
+ "rig": { "path": "<--rig as given>", "sha256": "<the rig spec's bytes>" },
234
+ "map": { "path": "<--map as given>", "sha256": "<the map's bytes>" },
235
+ "sequence": { "path": "<--sequence as given>", "sha256": "<the sequence's bytes>", "source": "<its \"source\", verbatim>" }
236
+ },
237
+ "flags": { "name": "<the animation>", "minScore": 0.3, "...": "every retarget option as resolved — the report's options, in their order" },
238
+ "reference": "<--reference as given>"
239
+ }
240
+ ```
241
+
242
+ - **Paths are as typed.** Each `path` is the argument as given on the command line — not resolved, not made relative, not normalised. Each `sha256` is the hex digest of the file's bytes the run read (the same bytes the report's `inputs` hash). `--rig` given as a directory or as a `rig.json` hashes the same file: the rig spec, `rig.json`, that rigc builds from. A `motion.json` read beside it (or `--base-motion`), a `sets.json` and the images are not in the hash, as they are not in the report's.
243
+ - **`reference` is the author's, and only when given.** `--reference <path>` (with `--build`; without it the run is refused: `--reference needs --build: the record it goes in is written beside the build`) names what the motion was made to match — the video, say — and the record carries it exactly as typed. It is echoed, never read: the path is not checked to exist. Without `--reference` the record has no `reference` key — not `null`, and nothing is derived from the sequence's `source` or its path. (`compare --reference` is another flag.)
244
+ - **Deterministic.** No clock, no host name, no absolute path the user did not type: the same inputs and flags write the same bytes into any build directory. 2-space JSON, a trailing newline.
245
+
246
+ `rigc review` is the intended reader (rig-c's later card): it reads the build and, through this file, what made it.
247
+
223
248
  ### The legs: foreshortening and planted feet
224
249
 
225
250
  A knee bending toward the camera is seen from the front only as a shorter leg, and a rig with rigid legs cannot lower its hip without lifting its feet ([#47](https://github.com/rig-c/pose/issues/47)). Two flags, both off by default:
package/cli.ts CHANGED
@@ -13,6 +13,7 @@ import { convertCoco17, readPoseSequence, writePoseSequence } from './src/pose.t
13
13
  import { parseWindows } from './src/compare.ts';
14
14
  import { lineOf, Refusal } from './src/problems.ts';
15
15
  import { runCompare, type CompareArgs } from './src/runcompare.ts';
16
+ import { PROVENANCE_FILE } from './src/provenance.ts';
16
17
  import { runRetarget, type RetargetArgs } from './src/run.ts';
17
18
 
18
19
  const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
@@ -182,7 +183,13 @@ retarget
182
183
  changes turn fastest, among those that newly show a set only on frames within
183
184
  2 × the switch tolerance of its key skeleton (it stays when none qualifies)
184
185
  --profile spine|spine-html rigc's gate profile (default: spine, rigc's own default)
185
- --build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
186
+ --build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here, with the record of
187
+ what made it beside it: retarget.json (rig-pose-retarget/1 — both versions, each input
188
+ as given with its sha256, the sequence's source, every option as resolved; rig-c/pose#270)
189
+ --reference <path> with --build: written into retarget.json as "reference", exactly as given — the
190
+ video or file the motion was made to match. Echoed, never read: the path is not
191
+ checked to exist. Without it the record has no "reference" (compare's --reference
192
+ is another flag)
186
193
  --render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
187
194
  --parts <path> @rig-c/parts' parts.json, or the directory holding it (its build's --out, beside rig/):
188
195
  the neighbour pairs the seam reading reads — each limb's consecutive segments. Each
@@ -324,8 +331,9 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
324
331
  const RETARGET_ALIASES = { sequence: 'pose', animation: 'name' } as const;
325
332
 
326
333
  function retargetArgs(argv: string[]): RetargetArgs {
327
- const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
334
+ const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
328
335
  if (f.has('verbose') && f.has('summary')) throw new Usage('--verbose and --summary are both given; --verbose prints every line in full and --summary one line per measure, so give one');
336
+ if (f.has('reference') && !f.has('build')) throw new Usage(`--reference needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
329
337
  const bends = f.get('bends') ?? 'refuse';
330
338
  if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
331
339
  const switchAt = f.get('switch-at') ?? 'first';
@@ -360,6 +368,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
360
368
  switchAt,
361
369
  profile,
362
370
  build: f.get('build'),
371
+ reference: f.get('reference'),
363
372
  render: f.get('render'),
364
373
  parts: f.get('parts'),
365
374
  report: f.get('report'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.35.0",
3
+ "version": "0.36.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": {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * rig-c/pose#270: the record `retarget --build` writes beside the build —
3
+ * `rig-pose-retarget/1`, what made it: this package's and rig-c's versions,
4
+ * each input as given on the command line with the sha256 of the bytes the run
5
+ * read, the sequence's own `source`, every retarget option as resolved, and the
6
+ * author's `--reference` echoed verbatim when one was given. Pure: the caller
7
+ * hashes and writes. No clock, no host, no path is normalised; a value the run
8
+ * was not given is absent, never derived.
9
+ */
10
+ import pkg from '../package.json' with { type: 'json' };
11
+
12
+ /** The record's file name inside the `--build` directory. */
13
+ export const PROVENANCE_FILE = 'retarget.json';
14
+ export const PROVENANCE_SPEC = 'rig-pose-retarget/1';
15
+
16
+ export interface ProvenanceInput {
17
+ /** The path as given on the command line, and the sha256 (hex) of the file's bytes. */
18
+ path: string;
19
+ sha256: string;
20
+ }
21
+
22
+ export interface ProvenanceArgs {
23
+ /** rig-c's version, as `rigcVersion()` read it from the gate's own launcher. */
24
+ rigc: string;
25
+ /** `--rig` as given; the hash is of the rig spec (`rig.json`) rigc gated. */
26
+ rig: ProvenanceInput;
27
+ map: ProvenanceInput;
28
+ /** `--sequence` as given, with the document's `source` string verbatim. */
29
+ sequence: ProvenanceInput & { source: string };
30
+ /** The animation's name, then the report's `options` in their order. */
31
+ name: string;
32
+ options: Record<string, unknown>;
33
+ /** `--reference` exactly as given; absent when it was not. */
34
+ reference?: string;
35
+ }
36
+
37
+ /** The record's text: fixed key order, 2-space JSON, a trailing newline. */
38
+ export function provenanceText(a: ProvenanceArgs): string {
39
+ const record = {
40
+ spec: PROVENANCE_SPEC,
41
+ pose: { version: pkg.version },
42
+ rigc: { version: a.rigc },
43
+ inputs: {
44
+ rig: { path: a.rig.path, sha256: a.rig.sha256 },
45
+ map: { path: a.map.path, sha256: a.map.sha256 },
46
+ sequence: { path: a.sequence.path, sha256: a.sequence.sha256, source: a.sequence.source },
47
+ },
48
+ flags: { name: a.name, ...a.options },
49
+ ...(a.reference === undefined ? {} : { reference: a.reference }),
50
+ };
51
+ return `${JSON.stringify(record, null, 2)}\n`;
52
+ }
package/src/run.ts CHANGED
@@ -22,6 +22,7 @@ import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
22
22
  import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
23
23
  import type { BlendHold, Closure, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
24
24
  import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
25
+ import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
25
26
  import { bodyLine, faceLine } from './face.ts';
26
27
 
27
28
  export interface RetargetArgs extends RetargetOptions {
@@ -40,6 +41,11 @@ export interface RetargetArgs extends RetargetOptions {
40
41
  profile: 'spine' | 'spine-html';
41
42
  /** Keep the gated build here. */
42
43
  build?: string;
44
+ /**
45
+ * With `build`: echoed verbatim into the build's record (`retarget.json`, #270) as `reference`;
46
+ * never read, never checked to exist. Absent, the record has no `reference` key.
47
+ */
48
+ reference?: string;
43
49
  /** Keep rigc's render of the animation (frames, contact sheet, folds.json) here. */
44
50
  render?: string;
45
51
  /** @rig-c/parts' `parts.json` (or the directory holding it): the neighbour pairs the seam reading reads (#216). */
@@ -107,6 +113,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
107
113
  if (!statSync(up).isDirectory()) args.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not a directory` : `${flag} ${path} — ${up} is a file, so directory ${dir} cannot be made under it`);
108
114
  else if (up !== dir) makeDirs.set(dir, [...(makeDirs.get(dir) ?? []), `${flag} ${path}`]);
109
115
  }
116
+ if (a.reference !== undefined && a.build === undefined) args.add('ARG_REFERENCE', `--reference ${a.reference} needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
110
117
  args.throwIfAny();
111
118
  const input = resolveRig(a.rig, a.baseMotion);
112
119
  const rigSnapshot = readReportInput(input.rig);
@@ -377,6 +384,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
377
384
  for (const d of drift) say(` REDUCE ${ankleDriftLine(d)}`);
378
385
 
379
386
  assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
387
+ // Every retarget option as resolved: the report's `options`, and with the name the build record's `flags` (#270).
388
+ const options = { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}) };
380
389
  const reportText = writeReport({
381
390
  spec: 'rig-pose-report/1',
382
391
  rigc,
@@ -385,7 +394,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
385
394
  frames: seq.frames.length,
386
395
  duration: r.animation.duration,
387
396
  cleaned: seq.cleaned,
388
- options: { minScore: a.minScore, flip: a.flip, short: a.short, bends: a.bends, bendTolerance: a.bendTolerance, switchTolerance: a.switchTolerance, switchMin: a.switchMin, switchAt: a.switchAt, profile: a.profile, legScale: a.legScale ?? 'off', plantFeet: a.plantFeet === true, stance: a.stance ?? 'off', floor: a.floor ?? null, raisedByHead: a.raisedByHead === true, idle: idleName, carryIdle: a.carryIdle === true, merge: a.merge ?? null, reduce: a.reduce ?? 'off', reduceHip: a.reduceHip ?? 'off', ...(a.reduceKeepPlanted === true ? { reduceKeepPlanted: true } : {}) },
397
+ options,
389
398
  scale: r.scale,
390
399
  scaleFit: r.scaleFit,
391
400
  holds: r.holds,
@@ -448,6 +457,20 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
448
457
  }
449
458
  // The kept build reads the rig again; its bytes must still match the report.
450
459
  assertReportInputs([rigSnapshot,mapSnapshot,poseSnapshot]);
460
+ if (a.build !== undefined) {
461
+ // #270: what made the build, beside it — the inputs as given with the bytes' hashes (the ones just
462
+ // re-checked), the versions, the options; `reference` only when the author gave one.
463
+ writeFileSync(join(a.build, PROVENANCE_FILE), provenanceText({
464
+ rigc: rigc.version,
465
+ rig: { path: a.rig, sha256: rigSnapshot.identity.sha256 },
466
+ map: { path: a.map, sha256: mapSnapshot.identity.sha256 },
467
+ sequence: { path: a.pose, sha256: poseSnapshot.identity.sha256, source: seq.source },
468
+ name: a.name,
469
+ options,
470
+ ...(a.reference === undefined ? {} : { reference: a.reference }),
471
+ }));
472
+ say(` RECORD ${PROVENANCE_FILE} written beside the build (reference: ${a.reference ?? 'none given'})`);
473
+ }
451
474
  for (const [dir, flags] of makeDirs) {
452
475
  mkdirSync(dir, { recursive: true });
453
476
  tell(madeLine(dir, flags));