spine-rigc 0.10.0 → 0.12.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 +31 -1
- package/cli.ts +234 -4
- package/docs/AUTHORING.md +288 -21
- package/docs/INGEST.md +7 -0
- package/docs/MOTION.md +10 -0
- package/package.json +1 -1
- package/src/chainfit.ts +2028 -0
- package/src/compile.ts +126 -7
- package/src/mesh.ts +183 -22
- package/src/pose.ts +58 -32
- package/src/render.ts +1 -1
- package/src/types.ts +14 -3
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ parser and a list of named assertions all come back green.
|
|
|
36
36
|
| a compiled rig | `rigc preview` | one self-contained `.html` that plays it in Spine's own web player |
|
|
37
37
|
| two to four compiled rigs | `rigc vote` | one ballot page a human picks from, and the answer checked into a ledger |
|
|
38
38
|
| a picture of a key pose | `rigc pose` | where each loose part PNG sits in it, in spec coordinates — the movement between two poses is then yours to key ([docs/MOTION.md](docs/MOTION.md)) |
|
|
39
|
+
| the same picture, and a rig | `rigc chainfit` | the parts `pose` refuses because something is drawn over them — read through the candidate's own draw order and hierarchy, with the share of each part the answer was measured on |
|
|
39
40
|
|
|
40
41
|
Everything in that table needs Bun and this package: no clone, no reference art, no
|
|
41
42
|
art pipeline, no server.
|
|
@@ -353,6 +354,16 @@ self-contained `.html` that plays it in Spine's own web player; **`vote`** puts
|
|
|
353
354
|
four candidates in one page and takes a human's answer back. Reach for them the moment
|
|
354
355
|
a rig compiles green, because green says nothing at all about the picture.
|
|
355
356
|
|
|
357
|
+
<p align="center">
|
|
358
|
+
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-keypose.gif" alt="Two key poses read back by rigc pose, two candidate in-between motions compiled from them, and a rigc vote ballot picking one" width="600" />
|
|
359
|
+
</p>
|
|
360
|
+
|
|
361
|
+
<p align="center"><em>The whole loop on one character: two key poses are the given
|
|
362
|
+
conditions, <code>rigc pose</code> reads where every part sits in each picture, two
|
|
363
|
+
candidate in-betweenings are compiled from the same pair — and a real
|
|
364
|
+
<code>rigc vote</code> ballot picks the winner, because the movement between the poses
|
|
365
|
+
is the one thing no instrument here will grade.</em></p>
|
|
366
|
+
|
|
356
367
|
Three properties of `vote` are worth stating, because they are what make its ledger
|
|
357
368
|
usable by the next agent rather than by a reader: **a tie is a recorded outcome, not a
|
|
358
369
|
missing one** — `both-unacceptable` is the tie that means *propose again*, and it is
|
|
@@ -371,7 +382,25 @@ it, so those coordinates go into the rig and the motion **by construction** and
|
|
|
371
382
|
effort goes into the part no instrument can measure: the movement between two poses.
|
|
372
383
|
A part that matches nowhere is refused by name, two near-equal placements are reported
|
|
373
384
|
as both, and nothing it prints is a score. Fields, the coordinate contract and the
|
|
374
|
-
limits: [AUTHORING.md §11](docs/AUTHORING.md).
|
|
385
|
+
limits: [AUTHORING.md §11](docs/AUTHORING.md). The parts it refuses because
|
|
386
|
+
something is drawn over them are `rigc chainfit`'s, once a candidate exists —
|
|
387
|
+
[§12](docs/AUTHORING.md).
|
|
388
|
+
|
|
389
|
+
## The gallery — four complete rigs over art that ships with them
|
|
390
|
+
|
|
391
|
+
Each directory in [`gallery/`](https://github.com/firejune/rigc/tree/main/gallery) is
|
|
392
|
+
one rig spec, one motion spec and the PNGs they name, small enough to read in one
|
|
393
|
+
sitting. Each stars a single feature, so *how do I do X* has a working answer rather
|
|
394
|
+
than a field table, and each README carries the frame rate it was authored at, what
|
|
395
|
+
was verified, and what writing it cost. Repository material: a clone and
|
|
396
|
+
`bun install` runs them.
|
|
397
|
+
|
|
398
|
+
| Example | Stars | What it is |
|
|
399
|
+
| --- | --- | --- |
|
|
400
|
+
| [`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk) | `ik` constraints + **`ik` timelines** | Two two-bone leg chains solved to foot targets — the planted leg nailed down, the swinging one let go at the top of its lift |
|
|
401
|
+
| [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) | **`deform` timelines** | A ball squashed about its contact point and stretched along its travel, from two affine transforms written out in the README |
|
|
402
|
+
| [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex) | **`contour` meshes** | A swallow-tailed banner and a serrated leaf: four meshes traced off their own alpha, waved by bone timelines and rippled by a `deform` |
|
|
403
|
+
| [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
|
|
375
404
|
|
|
376
405
|
## Commands
|
|
377
406
|
|
|
@@ -390,6 +419,7 @@ commands take it and what its default is.
|
|
|
390
419
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it |
|
|
391
420
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
392
421
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
422
|
+
| `chainfit --candidate <dir> --images <dir> --frame <png>` | reads the parts `pose` refuses, through the candidate's own draw order and hierarchy: masked residuals over **visible** pixels, one hinge per child instead of four degrees of freedom, and the `rotate` key value each answer implies |
|
|
393
423
|
| `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score |
|
|
394
424
|
| `check --candidate <dir> --frames <dir>` | the candidate against reference pictures — the only instrument here that can see a *wrong animation* |
|
|
395
425
|
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
package/cli.ts
CHANGED
|
@@ -72,6 +72,18 @@ import {
|
|
|
72
72
|
poseLines,
|
|
73
73
|
type PoseOptions,
|
|
74
74
|
} from './src/pose.ts';
|
|
75
|
+
import {
|
|
76
|
+
ANCHOR_MAX_RESIDUAL,
|
|
77
|
+
ANCHOR_MAX_UNEXPLAINED,
|
|
78
|
+
chainFitLines,
|
|
79
|
+
ChainFitError,
|
|
80
|
+
DEFAULT_HINGE_MAX,
|
|
81
|
+
DEFAULT_HINGE_MIN,
|
|
82
|
+
DEFAULT_MIN_VISIBLE,
|
|
83
|
+
DEFAULT_PASSES,
|
|
84
|
+
estimateChainFit,
|
|
85
|
+
type ChainFitOptions,
|
|
86
|
+
} from './src/chainfit.ts';
|
|
75
87
|
import { buildPreview, PLAYER_LINE, type PreviewPage } from './src/preview.ts';
|
|
76
88
|
import {
|
|
77
89
|
atlasPageNames,
|
|
@@ -382,10 +394,41 @@ const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> =
|
|
|
382
394
|
authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
|
|
383
395
|
};
|
|
384
396
|
|
|
385
|
-
/**
|
|
397
|
+
/**
|
|
398
|
+
* What a mesh measured about its own fit against the art it names, or nothing
|
|
399
|
+
* for a mesh with no art to measure against.
|
|
400
|
+
*
|
|
401
|
+
* Printed for authored geometry as well as for a `contour` (issue #277): the
|
|
402
|
+
* figure is a measurement between the emitted triangles and the PNG, so it means
|
|
403
|
+
* the same thing whoever drew the vertices, and the silence was the defect —
|
|
404
|
+
* an octagon rim placed on a round part's silhouette clips its own ink outline
|
|
405
|
+
* at 94.31% and used to print nothing at all.
|
|
406
|
+
*
|
|
407
|
+
* The hole is appended only when there is one, so the common line is unchanged.
|
|
408
|
+
* It is the one figure in the report that a hole moves: `coverage` and
|
|
409
|
+
* `overshoot` are both measured against the FILLED silhouette, so spanning an
|
|
410
|
+
* interior hole is neither missing coverage nor reaching past anything, and an
|
|
411
|
+
* unintentional hole — a gap in the art, a stroke that failed to join — bought
|
|
412
|
+
* fill over transparent pixels with nothing anywhere saying so (issue #275).
|
|
413
|
+
*/
|
|
386
414
|
function meshFit(m: CompileResult['meshes'][number]): string {
|
|
387
415
|
if (m.coverage === undefined) return '';
|
|
388
|
-
|
|
416
|
+
const hole = m.holePixels ? `, enclosing ${m.holePixels}px of hole` : '';
|
|
417
|
+
return ` covers ${(m.coverage * 100).toFixed(2)}% of the art, reaching ${m.overshoot?.toFixed(2) ?? '?'}px past it${hole}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The triangle budget a `MESH` line is read against: the rig's, or nothing.
|
|
422
|
+
*
|
|
423
|
+
* 📐 It used to be the literal `80`, which was nobody's budget — the rig quoted
|
|
424
|
+
* in issue #275 declared 64, `A13_MESH_BUDGET` measured against that 64
|
|
425
|
+
* correctly, and the line an author actually reads printed 80. Under the default
|
|
426
|
+
* `--profile spine` `A13` is `PROF`, so the printed number is the only budget
|
|
427
|
+
* figure in the output and it has to be the declared one. A rig that declares
|
|
428
|
+
* none says so in the same words `A13` SKIPs in, rather than being given a wall.
|
|
429
|
+
*/
|
|
430
|
+
function meshBudget(rig: CompileResult['rig']): string {
|
|
431
|
+
return rig.meshTriangleBudget === null ? '(no budget declared)' : `(budget ${rig.meshTriangleBudget})`;
|
|
389
432
|
}
|
|
390
433
|
|
|
391
434
|
/**
|
|
@@ -471,7 +514,7 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
471
514
|
for (const m of result.meshes) {
|
|
472
515
|
console.log(
|
|
473
516
|
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
474
|
-
|
|
517
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}`,
|
|
475
518
|
);
|
|
476
519
|
}
|
|
477
520
|
for (const ph of result.physics) {
|
|
@@ -1029,6 +1072,100 @@ function cmdPose(flags: Record<string, string>): void {
|
|
|
1029
1072
|
writeJson(out, report);
|
|
1030
1073
|
}
|
|
1031
1074
|
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
// reading the half a picture hides — chainfit
|
|
1077
|
+
// ---------------------------------------------------------------------------
|
|
1078
|
+
//
|
|
1079
|
+
// ⭐ `pose` above reads a picture with nothing but the loose parts, and refuses
|
|
1080
|
+
// the parts another part is drawn over — a residual measured through an occluder
|
|
1081
|
+
// rises AT the correct placement, so the honest answer is a refusal. This reads
|
|
1082
|
+
// those, and the whole difference is that it is also given the CANDIDATE: with a
|
|
1083
|
+
// draw order the covered pixels can be excluded from a part's objective instead
|
|
1084
|
+
// of charged to it, and with a hierarchy a child of a placed bone has one degree
|
|
1085
|
+
// of freedom — the hinge about its own pivot — where `pose` has four.
|
|
1086
|
+
//
|
|
1087
|
+
// 🚫 Same phase and the same framing as `pose`: it reads a given condition into
|
|
1088
|
+
// spec coordinates and grades nothing. Every residual is a trust signal, every
|
|
1089
|
+
// threshold is reported, and `visibleShare` is how much of the part the number
|
|
1090
|
+
// was even computed on.
|
|
1091
|
+
//
|
|
1092
|
+
// rigc chainfit --candidate <dir> --images <dir> --frame poseA.png [--anchor pose.json]
|
|
1093
|
+
|
|
1094
|
+
const DEFAULT_CHAINFIT_OUT = 'chainfit.json';
|
|
1095
|
+
|
|
1096
|
+
function cmdChainFit(flags: Record<string, string>): void {
|
|
1097
|
+
if (flags.candidate === undefined) {
|
|
1098
|
+
throw new UsageError('chainfit needs --candidate <dir | skeleton.json> — the compiled rig to read the frame through');
|
|
1099
|
+
}
|
|
1100
|
+
if (flags.images === undefined) {
|
|
1101
|
+
throw new UsageError("chainfit needs --images <dir> — where the candidate's attachment image names resolve to PNGs");
|
|
1102
|
+
}
|
|
1103
|
+
if (flags.frame === undefined) throw new UsageError('chainfit needs --frame <path> — one pose frame to read the placements out of');
|
|
1104
|
+
// Refused rather than ignored. Every other --candidate command takes --atlas, so
|
|
1105
|
+
// passing it here is a reasonable thing to try — and a flag that silently does
|
|
1106
|
+
// nothing is worse than one that says why it cannot.
|
|
1107
|
+
if (flags.atlas !== undefined) {
|
|
1108
|
+
throw new UsageError(
|
|
1109
|
+
'chainfit reads no atlas: the part art comes from --images, one PNG per attachment image name, and the ' +
|
|
1110
|
+
'skeleton is all it needs of the candidate. Drop --atlas',
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
const options: ChainFitOptions = {
|
|
1114
|
+
candidatePath: flags.candidate,
|
|
1115
|
+
imagesDir: flags.images,
|
|
1116
|
+
framePath: flags.frame,
|
|
1117
|
+
};
|
|
1118
|
+
if (flags.anchor !== undefined) options.anchorPath = flags.anchor;
|
|
1119
|
+
const hinge = readRange(flags, 'hinge');
|
|
1120
|
+
if (hinge) {
|
|
1121
|
+
if (hinge.high - hinge.low > 360) throw new UsageError('--hinge cannot span more than a full turn');
|
|
1122
|
+
options.hinge = { minDeg: hinge.low, maxDeg: hinge.high };
|
|
1123
|
+
}
|
|
1124
|
+
if (flags.stretch !== undefined) {
|
|
1125
|
+
const value = Number(flags.stretch);
|
|
1126
|
+
if (!Number.isFinite(value) || value < 1) throw new UsageError('--stretch must be a ratio of 1 or more, e.g. 1.25');
|
|
1127
|
+
options.stretch = value;
|
|
1128
|
+
}
|
|
1129
|
+
if (flags['min-visible'] !== undefined) {
|
|
1130
|
+
const value = Number(flags['min-visible']);
|
|
1131
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new UsageError('--min-visible must be a number in [0, 1]');
|
|
1132
|
+
options.minVisible = value;
|
|
1133
|
+
}
|
|
1134
|
+
if (flags['max-residual'] !== undefined) {
|
|
1135
|
+
const value = Number(flags['max-residual']);
|
|
1136
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) throw new UsageError('--max-residual must be a number in (0, 1]');
|
|
1137
|
+
options.maxResidual = value;
|
|
1138
|
+
}
|
|
1139
|
+
if (flags.passes !== undefined) {
|
|
1140
|
+
const value = Number(flags.passes);
|
|
1141
|
+
if (!Number.isInteger(value) || value < 1 || value > 8) throw new UsageError('--passes must be a whole number in 1..8');
|
|
1142
|
+
options.passes = value;
|
|
1143
|
+
}
|
|
1144
|
+
if (flags['anchor-residual'] !== undefined) {
|
|
1145
|
+
const value = Number(flags['anchor-residual']);
|
|
1146
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) throw new UsageError('--anchor-residual must be a number in (0, 1]');
|
|
1147
|
+
options.anchorMaxResidual = value;
|
|
1148
|
+
}
|
|
1149
|
+
const scale = readRange(flags, 'scale');
|
|
1150
|
+
if (scale) {
|
|
1151
|
+
if (scale.low <= 0) throw new UsageError('--scale minimum must be greater than zero');
|
|
1152
|
+
options.scale = { min: scale.low, max: scale.high };
|
|
1153
|
+
}
|
|
1154
|
+
const rotation = readRange(flags, 'rotation');
|
|
1155
|
+
if (rotation) {
|
|
1156
|
+
if (rotation.high - rotation.low > 360) throw new UsageError('--rotation cannot span more than a full turn');
|
|
1157
|
+
options.rotation = { minDeg: rotation.low, maxDeg: rotation.high };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
console.log('rigc chainfit');
|
|
1161
|
+
const report = estimateChainFit(options);
|
|
1162
|
+
for (const line of chainFitLines(report)) console.log(line);
|
|
1163
|
+
|
|
1164
|
+
const target = resolve(flags.out ?? DEFAULT_CHAINFIT_OUT);
|
|
1165
|
+
const out = existsSync(target) && statSync(target).isDirectory() ? join(target, DEFAULT_CHAINFIT_OUT) : target;
|
|
1166
|
+
writeJson(out, report);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1032
1169
|
// ---------------------------------------------------------------------------
|
|
1033
1170
|
// choosing between results — vote
|
|
1034
1171
|
// ---------------------------------------------------------------------------
|
|
@@ -1763,7 +1900,8 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
1763
1900
|
for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
|
|
1764
1901
|
for (const m of result.meshes) {
|
|
1765
1902
|
console.log(
|
|
1766
|
-
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles
|
|
1903
|
+
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
1904
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}`,
|
|
1767
1905
|
);
|
|
1768
1906
|
}
|
|
1769
1907
|
}
|
|
@@ -1838,6 +1976,27 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1838
1976
|
'max-residual':
|
|
1839
1977
|
`above this residual a placement is refused by name instead of reported flat (default ${DEFAULT_MAX_RESIDUAL}); ` +
|
|
1840
1978
|
'it is a reporting threshold, not a pass bar',
|
|
1979
|
+
anchor:
|
|
1980
|
+
'a `rigc pose` report for THIS frame, whose confident placements become the anchors the chains hang off ' +
|
|
1981
|
+
'(default: run that pass internally over exactly the parts the candidate draws)',
|
|
1982
|
+
hinge:
|
|
1983
|
+
`the window each child bone's local rotation is searched over, in Spine degrees about its setup value ` +
|
|
1984
|
+
`(default \`${DEFAULT_HINGE_MIN},${DEFAULT_HINGE_MAX}\`, a full turn — one degree of freedom is cheap enough not ` +
|
|
1985
|
+
'to risk a window that does not contain the truth)',
|
|
1986
|
+
stretch:
|
|
1987
|
+
'also search a uniform scale on every bone, over this ratio either way (e.g. 1.25). Without it the stretch ' +
|
|
1988
|
+
"degree of freedom is searched only where the candidate's own animations key a `scale` timeline, because a rig " +
|
|
1989
|
+
'that never scales a bone is a rig saying that bone does not stretch',
|
|
1990
|
+
'min-visible':
|
|
1991
|
+
`below this share of a part surviving the parts drawn over it, the placement is refused by name instead of ` +
|
|
1992
|
+
`reported flat (default ${DEFAULT_MIN_VISIBLE}); the best one found is still printed, and it is a reporting ` +
|
|
1993
|
+
'threshold, not a pass bar',
|
|
1994
|
+
passes:
|
|
1995
|
+
`how many times the occluder masks are rebuilt from the answers and the fit rerun (default ${DEFAULT_PASSES}); ` +
|
|
1996
|
+
"pass 1 freezes each part's visible set where the RIG predicts it, later passes where the last one landed",
|
|
1997
|
+
'anchor-residual':
|
|
1998
|
+
`the residual a \`pose\` placement must be within to anchor a chain (default ${ANCHOR_MAX_RESIDUAL}, with ` +
|
|
1999
|
+
`unexplained ≤ ${ANCHOR_MAX_UNEXPLAINED} and unambiguous — the 2026-09-03 measurement run's own clean-frame criterion)`,
|
|
1841
2000
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
1842
2001
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
1843
2002
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
@@ -1876,6 +2035,12 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1876
2035
|
scale: '<min,max>',
|
|
1877
2036
|
rotation: '<min,max>',
|
|
1878
2037
|
'max-residual': '<0..1>',
|
|
2038
|
+
anchor: '<pose.json>',
|
|
2039
|
+
hinge: '<min,max>',
|
|
2040
|
+
stretch: '<ratio>',
|
|
2041
|
+
'min-visible': '<0..1>',
|
|
2042
|
+
passes: '<n>',
|
|
2043
|
+
'anchor-residual': '<0..1>',
|
|
1879
2044
|
animation: '<name>',
|
|
1880
2045
|
max: '<px>',
|
|
1881
2046
|
record: '<result.json>',
|
|
@@ -2012,6 +2177,50 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2012
2177
|
},
|
|
2013
2178
|
},
|
|
2014
2179
|
},
|
|
2180
|
+
{
|
|
2181
|
+
name: 'chainfit',
|
|
2182
|
+
usage: [
|
|
2183
|
+
`rigc chainfit --candidate <dir | skeleton.json> --images <dir> --frame <path> [--anchor pose.json] [--out ${DEFAULT_CHAINFIT_OUT}]`,
|
|
2184
|
+
],
|
|
2185
|
+
flags: [
|
|
2186
|
+
'candidate',
|
|
2187
|
+
'images',
|
|
2188
|
+
'frame',
|
|
2189
|
+
'anchor',
|
|
2190
|
+
'hinge',
|
|
2191
|
+
'stretch',
|
|
2192
|
+
'min-visible',
|
|
2193
|
+
'max-residual',
|
|
2194
|
+
'passes',
|
|
2195
|
+
'anchor-residual',
|
|
2196
|
+
'scale',
|
|
2197
|
+
'rotation',
|
|
2198
|
+
'out',
|
|
2199
|
+
],
|
|
2200
|
+
overrides: {
|
|
2201
|
+
images: {
|
|
2202
|
+
value: '<dir>',
|
|
2203
|
+
meaning:
|
|
2204
|
+
"where each attachment's image name resolves to a loose PNG. ⚠️ NOT a part list the way `pose --images` " +
|
|
2205
|
+
'is one — the candidate decides what the parts are, so extra PNGs in here are simply unused and a name ' +
|
|
2206
|
+
'the directory lacks is refused by name',
|
|
2207
|
+
},
|
|
2208
|
+
scale: {
|
|
2209
|
+
meaning:
|
|
2210
|
+
'the scale window the INTERNAL anchor pass searches, as frame pixels per part pixel (default ' +
|
|
2211
|
+
`\`${DEFAULT_SCALE_MIN},${DEFAULT_SCALE_MAX}\`). Refused together with --anchor, which means there is no internal pass`,
|
|
2212
|
+
},
|
|
2213
|
+
rotation: {
|
|
2214
|
+
meaning:
|
|
2215
|
+
'the rotation window the INTERNAL anchor pass searches, in screen degrees (default `-180,180`). Refused ' +
|
|
2216
|
+
'together with --anchor — the chains\' own window is --hinge',
|
|
2217
|
+
},
|
|
2218
|
+
out: {
|
|
2219
|
+
value: '<file>',
|
|
2220
|
+
meaning: `the .json report to write (default \`${DEFAULT_CHAINFIT_OUT}\`); a directory means "the default name in here"`,
|
|
2221
|
+
},
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2015
2224
|
{
|
|
2016
2225
|
name: 'vote',
|
|
2017
2226
|
usage: [
|
|
@@ -2099,6 +2308,19 @@ const USAGE = [
|
|
|
2099
2308
|
'canvas cannot contain and a part whose rotation is a free degree of freedom are each',
|
|
2100
2309
|
'named as such. See `rigc pose --help`.',
|
|
2101
2310
|
'',
|
|
2311
|
+
'chainfit reads the half of that picture pose refuses. It is the same question with',
|
|
2312
|
+
'one more input — the candidate rig — and that input buys two things: draw order, so',
|
|
2313
|
+
'the pixels another part covers are EXCLUDED from a part\'s residual instead of',
|
|
2314
|
+
'charged to it, and hierarchy, so a child of a placed bone is searched over one hinge',
|
|
2315
|
+
'instead of four degrees of freedom:',
|
|
2316
|
+
' rigc chainfit --candidate build/ --images parts/ --frame poseA.png',
|
|
2317
|
+
'Every residual is over the part\'s VISIBLE pixels and comes with the `visibleShare` it',
|
|
2318
|
+
'was computed on, so a mostly-hidden answer carries its own uncertainty. It grades',
|
|
2319
|
+
'nothing either: a part too far behind the others is refused by the visibility floor,',
|
|
2320
|
+
'a limb with no trusted part on it or above it is refused `no-anchor`, and two hinge',
|
|
2321
|
+
'answers that explain the picture equally well are both reported. See',
|
|
2322
|
+
'`rigc chainfit --help`.',
|
|
2323
|
+
'',
|
|
2102
2324
|
'vote is the same page with two to four builds in it and an answer coming back:',
|
|
2103
2325
|
' rigc vote --candidate <build A> --candidate <build B> ballot.html, panes labelled A and B',
|
|
2104
2326
|
' rigc vote --record vote-<id>.json --ballot ballot.html check it, append it to votes.jsonl',
|
|
@@ -2145,6 +2367,7 @@ try {
|
|
|
2145
2367
|
else if (command === 'render') cmdRender(flags);
|
|
2146
2368
|
else if (command === 'preview') cmdPreview(flags);
|
|
2147
2369
|
else if (command === 'pose') cmdPose(flags);
|
|
2370
|
+
else if (command === 'chainfit') cmdChainFit(flags);
|
|
2148
2371
|
else if (command === 'vote') cmdVote(flags, lists.candidate ?? []);
|
|
2149
2372
|
} catch (err) {
|
|
2150
2373
|
if (err instanceof UsageError) {
|
|
@@ -2176,5 +2399,12 @@ try {
|
|
|
2176
2399
|
console.error(`rigc pose: ${err.message}`);
|
|
2177
2400
|
process.exit(2);
|
|
2178
2401
|
}
|
|
2402
|
+
// Same kind as a PoseError, and printed the same way for the same reason: the
|
|
2403
|
+
// messages name a path, a bone or an attachment, and reprinting the whole
|
|
2404
|
+
// usage under them buries the one line that says what to change.
|
|
2405
|
+
if (err instanceof ChainFitError) {
|
|
2406
|
+
console.error(`rigc chainfit: ${err.message}`);
|
|
2407
|
+
process.exit(2);
|
|
2408
|
+
}
|
|
2179
2409
|
throw err;
|
|
2180
2410
|
}
|