@rig-c/pose 0.34.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 +28 -1
- package/cli.ts +11 -2
- package/package.json +4 -1
- package/src/provenance.ts +52 -0
- package/src/run.ts +24 -1
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:
|
|
@@ -506,6 +531,8 @@ Exit codes: `0` compared, whether it found anything or not, since a finding is n
|
|
|
506
531
|
|
|
507
532
|
[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.
|
|
508
533
|
|
|
534
|
+
**The supported surface is the `rig-pose` command.** RELEASING.md's *What 1.x promises* lists every flag, report key, problem code and line tag as promised or courtesy, and why. The package exports no importable API: `exports` exposes only `package.json` ([#262](https://github.com/rig-c/pose/issues/262)).
|
|
535
|
+
|
|
509
536
|
## Why alternate drawings (0.2)
|
|
510
537
|
|
|
511
538
|
A flat painting turned by its bones matches each limb's direction, not its depth. On `dance-a` (a 13 s front-facing dance), retargeted onto a @rig-c/parts rig, five moments were found where it cannot follow. They were found by measuring the pose against the rig and checked against the video:
|
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,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "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": {
|
|
7
7
|
"rig-pose": "./bin/rig-pose.cjs"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
9
12
|
"engines": {
|
|
10
13
|
"bun": ">=1.2.0"
|
|
11
14
|
},
|
|
@@ -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
|
|
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));
|