spine-rigc 0.7.0 โ 0.8.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 +57 -2
- package/cli.ts +116 -0
- package/docs/AUTHORING.md +136 -1
- package/docs/MOTION.md +990 -0
- package/package.json +3 -1
- package/src/pose.ts +1386 -0
package/README.md
CHANGED
|
@@ -531,6 +531,10 @@ They complement each other rather than overlap. `render` is offline, determinist
|
|
|
531
531
|
and measurable โ its pixels are the ones `check` reports on. `preview` is the
|
|
532
532
|
interop proof: what plays there was played by Esoteric's own runtime, not by ours.
|
|
533
533
|
|
|
534
|
+
๐๏ธ **Authoring the movement that these two show you** โ key poses, in-betweening,
|
|
535
|
+
and how to spread candidates so a ballot informs โ is
|
|
536
|
+
[docs/MOTION.md](docs/MOTION.md).
|
|
537
|
+
|
|
534
538
|
### Letting someone choose โ `rigc vote`
|
|
535
539
|
|
|
536
540
|
Sometimes looking is not enough on its own, because there is more than one
|
|
@@ -584,6 +588,51 @@ by the next agent rather than by a reader:
|
|
|
584
588
|
Same player, same posture as `preview`: referenced from a CDN, never vendored,
|
|
585
589
|
and the file contains only your own art ([NOTICE.md](NOTICE.md)).
|
|
586
590
|
|
|
591
|
+
## Reading a pose you were given โ `rigc pose`
|
|
592
|
+
|
|
593
|
+
```bash
|
|
594
|
+
rigc pose --images parts/ --frame poseA.png [--out pose.json]
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Every command above takes something you authored and tells you about it. This one
|
|
598
|
+
runs the other way: it takes a **picture the user already has** โ one key pose โ
|
|
599
|
+
and reports where each loose part PNG sits in it, so an agent can write those
|
|
600
|
+
coordinates into a rig and a motion **by construction** and spend its effort on the
|
|
601
|
+
part no instrument can measure, the movement between two poses.
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
PLACE torso.png x= 44.4 y= 65.4 rot= 0.0ยฐ scale=1.118 residual=0.0770 unexplained= 19%
|
|
605
|
+
AMBIG arm.png x= 27.1 y= 56.3 rot= -35.2ยฐ scale=1.111 residual=0.0262 unexplained= 2%
|
|
606
|
+
alt 2: x= 61.5 y= 56.3 rot= 35.4ยฐ scale=1.116 residual=0.0279 unexplained= 2%
|
|
607
|
+
PLACE ball.png x= 44.3 y= 104.6 rot= 0.0ยฐ scale=1.144 residual=0.0203 unexplained= 3%
|
|
608
|
+
rotation is a FREE degree of freedom โ the 0ยฐ above is a placeholder
|
|
609
|
+
REFUSE foreign.png no-match: the best placement found has residual 0.4245, above --max-residual 0.25
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
๐จ **Nothing here is a score, and no pass bar attaches to any of it.** `check` and
|
|
613
|
+
`bench` measure a build against a reference, so their numbers mean *how close*. A
|
|
614
|
+
pose frame is not a reference โ it is a **given condition**, and once the spec
|
|
615
|
+
states those coordinates there is nothing left to be close to. The residual says
|
|
616
|
+
how far to trust a placement and where two answers are equally good, which is a
|
|
617
|
+
different job and needs the opposite defaults:
|
|
618
|
+
|
|
619
|
+
- a part that matches nowhere is **refused by name**, with its best guess still in
|
|
620
|
+
the JSON โ a refusal tells you not to trust a number rather than hiding it;
|
|
621
|
+
- two near-equal optima are reported as **both**, flagged `ambiguous`, never
|
|
622
|
+
silently resolved. Two identical limbs look exactly like that;
|
|
623
|
+
- a part whose rotation genuinely does not matter โ a ball โ reports rotation as a
|
|
624
|
+
**free degree of freedom** rather than as a failure;
|
|
625
|
+
- a part the canvas cannot contain at any tested scale, and a part with no material
|
|
626
|
+
in it at all, each get their own named refusal.
|
|
627
|
+
|
|
628
|
+
โ ๏ธ **Residuals degrade under occlusion and there is no depth solver here.** A part
|
|
629
|
+
drawn behind another has the occluder's pixels where its own should be, so its
|
|
630
|
+
residual rises at the *correct* placement; `unexplained` is the share of the part
|
|
631
|
+
that disagrees, and a middling residual beside a high `unexplained` usually means
|
|
632
|
+
*right place, seen through something else*. The output carries its own `caveats`
|
|
633
|
+
block saying so. Fields, coordinate contract and the rest of the limits:
|
|
634
|
+
**[AUTHORING.md ยง11](docs/AUTHORING.md)**.
|
|
635
|
+
|
|
587
636
|
## Run viewer โ watching a *run* instead of reading it
|
|
588
637
|
|
|
589
638
|
๐ **This is the ladder's instrument, not the way to look at your own rig** โ that
|
|
@@ -825,6 +874,7 @@ bun cli.ts render --candidate path/to/spine # PNG frames + a con
|
|
|
825
874
|
bun cli.ts preview --candidate path/to/spine # one .html that plays it
|
|
826
875
|
bun cli.ts vote --candidate path/to/a --candidate path/to/b # one .html that asks which
|
|
827
876
|
bun cli.ts vote --record vote-<id>.json # check the answer into votes.jsonl
|
|
877
|
+
bun cli.ts pose --images path/to/parts --frame poseA.png # read a pose OUT of a picture
|
|
828
878
|
```
|
|
829
879
|
|
|
830
880
|
`validate` on a bare directory checks what it can see. Adding `--cut`/`--cuts` lets
|
|
@@ -837,7 +887,9 @@ archetype policy on top.
|
|
|
837
887
|
[Looking at a rig](#looking-at-a-rig--rigc-render-and-rigc-preview). Run either
|
|
838
888
|
straight after a green `build`, on the same directory `--out` wrote. `vote` is the
|
|
839
889
|
same idea with more than one candidate in the page and an answer coming back โ
|
|
840
|
-
see [Letting someone choose](#letting-someone-choose--rigc-vote).
|
|
890
|
+
see [Letting someone choose](#letting-someone-choose--rigc-vote). `pose` is the
|
|
891
|
+
one command that runs *before* a spec exists rather than after โ see
|
|
892
|
+
[Reading a pose you were given](#reading-a-pose-you-were-given--rigc-pose).
|
|
841
893
|
|
|
842
894
|
## Checks
|
|
843
895
|
|
|
@@ -934,7 +986,7 @@ nothing substantive executed exits 2 rather than printing green.
|
|
|
934
986
|
|
|
935
987
|
```
|
|
936
988
|
tsconfig.json type-check config (noEmit); eslint.config.js โ the no-any gate
|
|
937
|
-
cli.ts build / validate / explain / diff / check / bench / render / preview / vote
|
|
989
|
+
cli.ts build / validate / explain / diff / check / bench / render / preview / vote / pose
|
|
938
990
|
selftest.ts the validator's own negative controls, and diff's and check's
|
|
939
991
|
fixtures/ public.ts โ the three synthetic cuts the selftest breaks
|
|
940
992
|
src/
|
|
@@ -951,6 +1003,9 @@ src/
|
|
|
951
1003
|
stand between a saved vote and the ledger
|
|
952
1004
|
check.ts a candidate against rendered frames โ pixels and per-slot drift,
|
|
953
1005
|
and it never opens the reference skeleton
|
|
1006
|
+
pose.ts the other direction: loose part PNGs against ONE pose frame, and
|
|
1007
|
+
where each part sits in it. An entry instrument โ it reads a given
|
|
1008
|
+
condition into spec coordinates and grades nothing
|
|
954
1009
|
ladder.ts which example is which rung, and which file in it is the reference
|
|
955
1010
|
timelines.ts the 4.3 timeline catalogue and its walker (shared, pure JSON)
|
|
956
1011
|
mesh.ts ring and ribbon mesh builders, weighted-vertex encoding
|
package/cli.ts
CHANGED
|
@@ -54,6 +54,15 @@ import { diffLines, diffSkeletons, sectionFigures, type DiffReport } from './src
|
|
|
54
54
|
import { copyAtlasImages } from './src/emit.ts';
|
|
55
55
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
56
56
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
57
|
+
import {
|
|
58
|
+
DEFAULT_MAX_RESIDUAL,
|
|
59
|
+
DEFAULT_SCALE_MAX,
|
|
60
|
+
DEFAULT_SCALE_MIN,
|
|
61
|
+
estimatePose,
|
|
62
|
+
PoseError,
|
|
63
|
+
poseLines,
|
|
64
|
+
type PoseOptions,
|
|
65
|
+
} from './src/pose.ts';
|
|
57
66
|
import { buildPreview, PLAYER_LINE, type PreviewPage } from './src/preview.ts';
|
|
58
67
|
import {
|
|
59
68
|
atlasPageNames,
|
|
@@ -773,6 +782,71 @@ function cmdPreview(flags: Record<string, string>): void {
|
|
|
773
782
|
console.log(`rigc: wrote ${out} (${(html.length / 1024).toFixed(1)} KiB โ open it in a browser)`);
|
|
774
783
|
}
|
|
775
784
|
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
// reading a given condition โ pose
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
//
|
|
789
|
+
// โญ Every other command here takes a spec and looks at what came out. This one
|
|
790
|
+
// runs the other way: it takes a PICTURE the user already has โ a key pose โ and
|
|
791
|
+
// reads spec coordinates out of it, so an agent can state those poses in a rig and
|
|
792
|
+
// a motion by construction and spend its loops on the part nobody can measure, the
|
|
793
|
+
// movement between them.
|
|
794
|
+
//
|
|
795
|
+
// ๐ซ It grades nothing, and the distinction is load-bearing rather than modest.
|
|
796
|
+
// `check` and `bench` compare a build against a reference and their numbers mean
|
|
797
|
+
// "how close"; a pose frame is not a reference, it is an INPUT, and once the spec
|
|
798
|
+
// states it there is nothing left to be close to. So the residual here is a trust
|
|
799
|
+
// signal โ how much of the frame this placement actually explains โ and the only
|
|
800
|
+
// threshold in `src/pose.ts` is the one that decides whether to print an answer at
|
|
801
|
+
// all, which the caller can move.
|
|
802
|
+
//
|
|
803
|
+
// rigc pose --images parts/ --frame poseA.png [--out pose.json]
|
|
804
|
+
|
|
805
|
+
const DEFAULT_POSE_OUT = 'pose.json';
|
|
806
|
+
|
|
807
|
+
/** `--scale 0.5,2` / `--rotation -30,30` โ a pair of numbers, low first. */
|
|
808
|
+
function readRange(flags: Record<string, string>, key: string): { low: number; high: number } | undefined {
|
|
809
|
+
const raw = flags[key];
|
|
810
|
+
if (raw === undefined) return undefined;
|
|
811
|
+
const parts = raw.split(',').map((s) => Number(s.trim()));
|
|
812
|
+
if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n))) {
|
|
813
|
+
throw new UsageError(`--${key} takes two numbers: <min>,<max>`);
|
|
814
|
+
}
|
|
815
|
+
if (parts[1] < parts[0]) throw new UsageError(`--${key} ${JSON.stringify(raw)}: the minimum must not exceed the maximum`);
|
|
816
|
+
return { low: parts[0], high: parts[1] };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function cmdPose(flags: Record<string, string>): void {
|
|
820
|
+
if (flags.images === undefined) throw new UsageError('pose needs --images <dir> โ the directory the loose part PNGs are in');
|
|
821
|
+
if (flags.frame === undefined) throw new UsageError('pose needs --frame <path> โ one pose frame to read the placements out of');
|
|
822
|
+
const options: PoseOptions = { imagesDir: flags.images, framePath: flags.frame };
|
|
823
|
+
const scale = readRange(flags, 'scale');
|
|
824
|
+
if (scale) {
|
|
825
|
+
if (scale.low <= 0) throw new UsageError('--scale minimum must be greater than zero');
|
|
826
|
+
options.scale = { min: scale.low, max: scale.high };
|
|
827
|
+
}
|
|
828
|
+
const rotation = readRange(flags, 'rotation');
|
|
829
|
+
if (rotation) {
|
|
830
|
+
if (rotation.high - rotation.low > 360) throw new UsageError('--rotation cannot span more than a full turn');
|
|
831
|
+
options.rotation = { minDeg: rotation.low, maxDeg: rotation.high };
|
|
832
|
+
}
|
|
833
|
+
if (flags['max-residual'] !== undefined) {
|
|
834
|
+
const value = Number(flags['max-residual']);
|
|
835
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) throw new UsageError('--max-residual must be a number in (0, 1]');
|
|
836
|
+
options.maxResidual = value;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
console.log('rigc pose');
|
|
840
|
+
const report = estimatePose(options);
|
|
841
|
+
for (const line of poseLines(report)) console.log(line);
|
|
842
|
+
|
|
843
|
+
// Same `--out` shape as `preview` and `vote`: one file, and a directory means
|
|
844
|
+
// "the default name in here" rather than a report written over a directory.
|
|
845
|
+
const target = resolve(flags.out ?? DEFAULT_POSE_OUT);
|
|
846
|
+
const out = existsSync(target) && statSync(target).isDirectory() ? join(target, DEFAULT_POSE_OUT) : target;
|
|
847
|
+
writeJson(out, report);
|
|
848
|
+
}
|
|
849
|
+
|
|
776
850
|
// ---------------------------------------------------------------------------
|
|
777
851
|
// choosing between results โ vote
|
|
778
852
|
// ---------------------------------------------------------------------------
|
|
@@ -1390,6 +1464,12 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1390
1464
|
as: 'the candidate animation to play, when it is named differently from the frame set',
|
|
1391
1465
|
'all-frames': 'print every frame, not just the worst by MAE',
|
|
1392
1466
|
json: 'also write the whole report to this path',
|
|
1467
|
+
frame: 'one pose frame โ a picture of the pose to read the part placements out of',
|
|
1468
|
+
scale: `the scale window to search, as frame pixels per part pixel (default \`${DEFAULT_SCALE_MIN},${DEFAULT_SCALE_MAX}\`)`,
|
|
1469
|
+
rotation: 'the rotation window to search, in screen degrees (default `-180,180`, a full turn)',
|
|
1470
|
+
'max-residual':
|
|
1471
|
+
`above this residual a placement is refused by name instead of reported flat (default ${DEFAULT_MAX_RESIDUAL}); ` +
|
|
1472
|
+
'it is a reporting threshold, not a pass bar',
|
|
1393
1473
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
1394
1474
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
1395
1475
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
@@ -1417,6 +1497,10 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1417
1497
|
framing: 'per-shot|shared',
|
|
1418
1498
|
as: '<name>',
|
|
1419
1499
|
json: '<out>',
|
|
1500
|
+
frame: '<path>',
|
|
1501
|
+
scale: '<min,max>',
|
|
1502
|
+
rotation: '<min,max>',
|
|
1503
|
+
'max-residual': '<0..1>',
|
|
1420
1504
|
animation: '<name>',
|
|
1421
1505
|
max: '<px>',
|
|
1422
1506
|
record: '<result.json>',
|
|
@@ -1505,6 +1589,20 @@ const COMMANDS: CommandDoc[] = [
|
|
|
1505
1589
|
},
|
|
1506
1590
|
},
|
|
1507
1591
|
},
|
|
1592
|
+
{
|
|
1593
|
+
name: 'pose',
|
|
1594
|
+
usage: [
|
|
1595
|
+
`rigc pose --images <dir> --frame <path> [--scale ${DEFAULT_SCALE_MIN},${DEFAULT_SCALE_MAX}] [--rotation -180,180] [--out ${DEFAULT_POSE_OUT}]`,
|
|
1596
|
+
],
|
|
1597
|
+
flags: ['images', 'frame', 'scale', 'rotation', 'max-residual', 'out'],
|
|
1598
|
+
overrides: {
|
|
1599
|
+
images: { value: '<dir>', meaning: 'the loose part PNGs to place; every `.png` in it is a part, in name order' },
|
|
1600
|
+
out: {
|
|
1601
|
+
value: '<file>',
|
|
1602
|
+
meaning: `the .json report to write (default \`${DEFAULT_POSE_OUT}\`); a directory means "the default name in here"`,
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1508
1606
|
{
|
|
1509
1607
|
name: 'vote',
|
|
1510
1608
|
usage: [
|
|
@@ -1582,6 +1680,16 @@ const USAGE = [
|
|
|
1582
1680
|
'draws with rigc\'s own rasteriser; preview embeds the artifact in a page that plays',
|
|
1583
1681
|
'it in the official Spine Web Player, which is also the interop proof.',
|
|
1584
1682
|
'',
|
|
1683
|
+
'pose runs the other way round from everything above: it reads a picture you already',
|
|
1684
|
+
'have โ one key pose โ and reports where each loose part PNG sits in it (x, y, rotation,',
|
|
1685
|
+
'scale) so an agent can state those poses in a spec by construction:',
|
|
1686
|
+
' rigc pose --images parts/ --frame poseA.png pose.json, one entry per part',
|
|
1687
|
+
'It grades nothing and no pass bar attaches to its numbers. The residual is a trust',
|
|
1688
|
+
'signal, and where two placements are equally good it reports BOTH rather than picking โ',
|
|
1689
|
+
'two identical limbs look exactly like that. A part that matches nowhere, a part the',
|
|
1690
|
+
'canvas cannot contain and a part whose rotation is a free degree of freedom are each',
|
|
1691
|
+
'named as such. See `rigc pose --help`.',
|
|
1692
|
+
'',
|
|
1585
1693
|
'vote is the same page with two to four builds in it and an answer coming back:',
|
|
1586
1694
|
' rigc vote --candidate <build A> --candidate <build B> ballot.html, panes labelled A and B',
|
|
1587
1695
|
' rigc vote --record vote-<id>.json --ballot ballot.html check it, append it to votes.jsonl',
|
|
@@ -1626,6 +1734,7 @@ try {
|
|
|
1626
1734
|
else if (command === 'bench') cmdBench(flags, positional);
|
|
1627
1735
|
else if (command === 'render') cmdRender(flags);
|
|
1628
1736
|
else if (command === 'preview') cmdPreview(flags);
|
|
1737
|
+
else if (command === 'pose') cmdPose(flags);
|
|
1629
1738
|
else if (command === 'vote') cmdVote(flags, lists.candidate ?? []);
|
|
1630
1739
|
} catch (err) {
|
|
1631
1740
|
if (err instanceof UsageError) {
|
|
@@ -1646,5 +1755,12 @@ try {
|
|
|
1646
1755
|
console.error(`rigc check error: ${err.message}`);
|
|
1647
1756
|
process.exit(1);
|
|
1648
1757
|
}
|
|
1758
|
+
// Like a usage error in kind โ a missing directory, an unreadable frame โ but
|
|
1759
|
+
// its messages name a path and a reason, and reprinting the whole usage under
|
|
1760
|
+
// them buries that.
|
|
1761
|
+
if (err instanceof PoseError) {
|
|
1762
|
+
console.error(`rigc pose: ${err.message}`);
|
|
1763
|
+
process.exit(2);
|
|
1764
|
+
}
|
|
1649
1765
|
throw err;
|
|
1650
1766
|
}
|
package/docs/AUTHORING.md
CHANGED
|
@@ -70,6 +70,12 @@ can see where Spine stops.
|
|
|
70
70
|
```bash
|
|
71
71
|
bun install # once
|
|
72
72
|
|
|
73
|
+
# โฆbut if what you were HANDED is a picture of the pose rather than numbers, read
|
|
74
|
+
# the numbers out of it first. This one runs BEFORE the loop, not inside it:
|
|
75
|
+
bun cli.ts pose --images path/to/parts --frame path/to/poseA.png --out poseA.json
|
|
76
|
+
# โณ one entry per part PNG: where it sits, how confident that is, and where two
|
|
77
|
+
# answers are equally good โ ยง11
|
|
78
|
+
|
|
73
79
|
bun cli.ts build \
|
|
74
80
|
--rig path/to/my.rig.json \
|
|
75
81
|
--motion path/to/my.motion.json \
|
|
@@ -127,7 +133,7 @@ What the flags mean:
|
|
|
127
133
|
| `--motion` | the motion spec โ time |
|
|
128
134
|
| `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
|
|
129
135
|
| `--copy-images` | `build` only: also copies every referenced page PNG into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own. Default is unchanged โ page paths still point at the source art (issue #217) |
|
|
130
|
-
| `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
|
|
136
|
+
| `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** โ every `.png` in it is a part, in name order |
|
|
131
137
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
132
138
|
| `--profile` | `spine` = the 22 validity rules (**the default**) ยท `spine-html` = all 36, opt-in |
|
|
133
139
|
| `--candidate` | `check`, `bench`, `render`, `preview` and `vote` only: a **compiled** artifact โ the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** โ repeat it 2โ4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
|
|
@@ -136,6 +142,10 @@ What the flags mean:
|
|
|
136
142
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
137
143
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
138
144
|
| `--again` | `vote --record` only: record a second vote on a ballot the ledger already has. Without it a repeat is refused by name rather than doubled |
|
|
145
|
+
| `--frame` | `pose` only: one pose frame โ the picture to read part placements out of. One frame per call; several key poses are several calls, and correlating them is yours (ยง11) |
|
|
146
|
+
| `--scale` | `pose` only: the scale window to search, as **frame pixels per part pixel**, `<min>,<max>` (default `0.5,2`). The report states what it searched, and a window that does not contain the truth does not reliably refuse โ ยง11 |
|
|
147
|
+
| `--rotation` | `pose` only: the rotation window to search, in screen degrees, `<min>,<max>` (default `-180,180`, a full turn). Narrow it when you know the art is upright |
|
|
148
|
+
| `--max-residual` | `pose` only: above this residual a placement is **refused by name** instead of reported flat (default `0.25`). A reporting threshold, not a pass bar โ the placement is still in the JSON |
|
|
139
149
|
|
|
140
150
|
`render` also takes `--fps <n>` (the rate it samples at, default 12 โ the same
|
|
141
151
|
protocol rate the reference frames use) and `--max <px>` (the long side of a
|
|
@@ -164,6 +174,7 @@ bun cli.ts render --candidate path/to/spine [--animation โฆ] [--fps 12] [--ma
|
|
|
164
174
|
bun cli.ts preview --candidate path/to/spine [--animation โฆ] [--out preview.html]
|
|
165
175
|
bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
|
|
166
176
|
bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
|
|
177
|
+
bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
167
178
|
```
|
|
168
179
|
|
|
169
180
|
- **`explain`** is the one to reach for when a rig compiles but looks wrong. It
|
|
@@ -197,6 +208,14 @@ bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger vot
|
|
|
197
208
|
double-clicking it is also the interop proof โ what plays there was played by
|
|
198
209
|
Esoteric Software's runtime, not by rigc's. The player is loaded from a CDN
|
|
199
210
|
rather than copied into the file, so the first open needs a network.
|
|
211
|
+
- ๐ **`pose` is the only command here that reads an INPUT rather than a result.**
|
|
212
|
+
Everything else takes a spec or a build and tells you something about it; `pose`
|
|
213
|
+
takes a picture the user already has โ a key pose โ and reports where each loose
|
|
214
|
+
part PNG sits in it, so you can write those coordinates into a rig and a motion
|
|
215
|
+
**by construction**. Nothing about it grades anything: at that point the pose is
|
|
216
|
+
a given condition, not a target, and the residual it reports is how far to trust
|
|
217
|
+
a placement rather than how good the placement is. Reach for it the moment a
|
|
218
|
+
request arrives as *"make it go from this picture to this one"*. **ยง11.**
|
|
200
219
|
- ๐ณ๏ธ **`vote` is `preview` with more than one candidate in it and an answer
|
|
201
220
|
coming back**, and it is the one step of this loop you cannot run yourself.
|
|
202
221
|
Reach for it where the instruments have run out: two builds that `check` and
|
|
@@ -3053,3 +3072,119 @@ answer read off the exports:
|
|
|
3053
3072
|
|
|
3054
3073
|
If one of those turns out to matter, it belongs in the run's `log.md` as something
|
|
3055
3074
|
the frames had to teach you โ not here.
|
|
3075
|
+
|
|
3076
|
+
---
|
|
3077
|
+
|
|
3078
|
+
## 11. Reading a pose you were given โ `rigc pose`
|
|
3079
|
+
|
|
3080
|
+
```bash
|
|
3081
|
+
bun cli.ts pose --images path/to/parts --frame path/to/poseA.png [--out pose.json]
|
|
3082
|
+
```
|
|
3083
|
+
|
|
3084
|
+
Every other command in this guide takes something you wrote and tells you about
|
|
3085
|
+
it. This one runs the other way: it takes a **picture the user already has** and
|
|
3086
|
+
reports, for each loose part PNG, where that part sits in it โ `x`, `y`,
|
|
3087
|
+
`rotationDeg`, `scale` โ plus a residual saying how well the placed part explains
|
|
3088
|
+
the frame's pixels underneath it.
|
|
3089
|
+
|
|
3090
|
+
Reach for it when a request arrives as *"make it go from this picture to this
|
|
3091
|
+
one"*. Read both frames, write the two key poses into the rig and the motion from
|
|
3092
|
+
the numbers it gives you, and spend your loops on the part no instrument can
|
|
3093
|
+
measure: the movement between them.
|
|
3094
|
+
|
|
3095
|
+
๐ **The recipe that consumes this report is [MOTION.md](MOTION.md)** โ how these
|
|
3096
|
+
placements become a rig and a motion, what goes between two key poses when nothing
|
|
3097
|
+
was given for the in-betweens, and how to spread candidates so a `vote` ballot
|
|
3098
|
+
informs.
|
|
3099
|
+
|
|
3100
|
+
### 11.1 It measures an input, so nothing here is a score
|
|
3101
|
+
|
|
3102
|
+
๐จ **No number in this report has a pass bar, and none of them is a grade.** The
|
|
3103
|
+
distinction is not modesty, it decides how to read the output. `check` and `bench`
|
|
3104
|
+
compare a build against a reference, so their numbers mean *how close*. A pose
|
|
3105
|
+
frame is not a reference โ it is a **given condition**, and once the spec states
|
|
3106
|
+
those coordinates there is nothing left to be close to. The residual exists so you
|
|
3107
|
+
know **how far to trust each placement** and **where two answers are equally
|
|
3108
|
+
good**.
|
|
3109
|
+
|
|
3110
|
+
That is why the command's output is shaped as help rather than as a verdict:
|
|
3111
|
+
|
|
3112
|
+
- a placement that matches nowhere is **refused by name** and its best guess is
|
|
3113
|
+
still printed, because a refusal tells you not to trust a number rather than
|
|
3114
|
+
hiding it;
|
|
3115
|
+
- two near-equal optima are reported as **both**, flagged `ambiguous`, never
|
|
3116
|
+
silently resolved;
|
|
3117
|
+
- a part whose rotation genuinely does not matter is reported as having a **free
|
|
3118
|
+
degree of freedom**, not as a failure.
|
|
3119
|
+
|
|
3120
|
+
### 11.2 The coordinate contract
|
|
3121
|
+
|
|
3122
|
+
Placements are in the **frame's own pixels: y down, origin top-left** โ the same
|
|
3123
|
+
convention a cut manifest uses, and the `space` field of every report repeats it.
|
|
3124
|
+
|
|
3125
|
+
- `x`, `y` โ where the part image's own centre, `(width / 2, height / 2)`, lands.
|
|
3126
|
+
- `rotationDeg` โ **screen** degrees, positive turning **clockwise** on screen.
|
|
3127
|
+
- `scale` โ uniform, as **frame pixels per part pixel**.
|
|
3128
|
+
|
|
3129
|
+
Reconstruct a part pixel `p` as `centre + scale ยท R(rotationDeg) ยท (p โ (width/2,
|
|
3130
|
+
height/2))`. To get to Spine's y-up, counter-clockwise world, use the two
|
|
3131
|
+
conversions that already exist and open-code neither:
|
|
3132
|
+
`screenToSpineDegrees(rotationDeg)` and `cropToSpineY(y, frameHeight)`
|
|
3133
|
+
([`src/transform.ts`](../src/transform.ts)).
|
|
3134
|
+
|
|
3135
|
+
### 11.3 The fields, and what each one is for
|
|
3136
|
+
|
|
3137
|
+
Per part:
|
|
3138
|
+
|
|
3139
|
+
| Field | Meaning |
|
|
3140
|
+
| --- | --- |
|
|
3141
|
+
| `part`, `path`, `width`, `height` | the PNG, by the name every message uses |
|
|
3142
|
+
| `placement` | the best placement found โ `null` **only** for `empty-part` and `larger-than-canvas`, where nothing was searched |
|
|
3143
|
+
| `alternates` | other optima worth reporting, best first. Non-empty means the answer was not unique |
|
|
3144
|
+
| `ambiguous` | at least one alternate is inside the ambiguity margin. **Choose with something this instrument cannot see** โ anatomy, the other frame, or `rigc vote` |
|
|
3145
|
+
| `rotationFree` | the part is self-similar under rotation, so `rotationDeg` is a placeholder and the value is yours |
|
|
3146
|
+
| `rotationSelfSimilarity` | the number `rotationFree` is a threshold on. A part just over the line is worth a look |
|
|
3147
|
+
| `refusal` | `{ reason, detail }` or `null`. Reasons: `no-match`, `larger-than-canvas`, `empty-part` |
|
|
3148
|
+
| `coarse` | the grid this part was actually searched on. A handful of cells means the part is small relative to the frame and the first pass had little to go on |
|
|
3149
|
+
| `notes` | the same facts in prose, in the order they were found |
|
|
3150
|
+
|
|
3151
|
+
Per placement:
|
|
3152
|
+
|
|
3153
|
+
| Field | Meaning |
|
|
3154
|
+
| --- | --- |
|
|
3155
|
+
| `residual` | alpha-weighted mean absolute colour error over the part's own footprint, `0..1`. **Lower is better explained** โ that is all it means |
|
|
3156
|
+
| `unexplained` | the share of the part's material that disagrees with the frame at this placement. **Read this next to the residual** โ see ยง11.4 |
|
|
3157
|
+
| `offCanvas` | the share of the part's material that falls outside the frame at this placement |
|
|
3158
|
+
| `footprint` | frame pixels of material the placement accounts for; the tie-break between two equal residuals |
|
|
3159
|
+
| `bbox` | the axis-aligned box the placed part occupies, in frame pixels |
|
|
3160
|
+
|
|
3161
|
+
The report also carries `frame.background` (how the picture's empty space was
|
|
3162
|
+
identified โ a flat colour, transparency, or `unknown`), `search` (every window
|
|
3163
|
+
and threshold that was applied), and `caveats`.
|
|
3164
|
+
|
|
3165
|
+
### 11.4 What it cannot see โ read this before using the numbers
|
|
3166
|
+
|
|
3167
|
+
- โ ๏ธ **Residuals degrade under occlusion, and there is no depth solver here.** A
|
|
3168
|
+
part drawn *behind* another has the occluder's pixels where its own should be, so
|
|
3169
|
+
its residual rises **at the correct placement**. `unexplained` separates the two
|
|
3170
|
+
readings: a middling residual with a high `unexplained` usually means *right
|
|
3171
|
+
place, seen through something else*, not *wrong place*. The only robustness in
|
|
3172
|
+
the objective is that it is measured on the part's **own alpha footprint** โ a
|
|
3173
|
+
part is never charged for pixels of the picture it does not claim. Weigh
|
|
3174
|
+
accordingly; do not treat either number as a verdict.
|
|
3175
|
+
- โ ๏ธ **A search window that does not contain the truth does not reliably refuse.**
|
|
3176
|
+
A part shrunk inside the region it came from still explains those pixels, so the
|
|
3177
|
+
answer is the best placement available *inside* `--scale` / `--rotation` and its
|
|
3178
|
+
residual can look reasonable. This is why the window is a reported field: if the
|
|
3179
|
+
numbers surprise you, check `search` before you trust them.
|
|
3180
|
+
- โ ๏ธ **A frame whose border has no dominant colour reports `background.unknown`.**
|
|
3181
|
+
Every pixel then counts as material, the silhouette signal is gone, and the
|
|
3182
|
+
residual is colour agreement alone. The report says so rather than being quietly
|
|
3183
|
+
weaker.
|
|
3184
|
+
- **An `ambiguous` part is genuinely ambiguous.** Two identical limbs, a part that
|
|
3185
|
+
fits its own silhouette at more than one angle, and a shape whose interior is one
|
|
3186
|
+
flat colour all look like this. The instrument has run out; that is what
|
|
3187
|
+
`rigc vote` is for.
|
|
3188
|
+
- **One frame per call.** Several key poses are several calls, and correlating A
|
|
3189
|
+
with B โ which placement of a repeated part belongs to which limb, across two
|
|
3190
|
+
frames โ is the authoring job, not this tool's.
|