spine-rigc 0.27.0 → 0.29.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/cli.ts +159 -10
- package/docs/AUTHORING.md +374 -86
- package/docs/INGEST.md +54 -2
- package/docs/SPEC_COVERAGE.md +1 -1
- package/package.json +1 -1
- package/src/compile.ts +307 -282
- package/src/diff.ts +235 -9
- package/src/ingest.ts +68 -13
- package/src/pose.ts +195 -21
- package/src/rig.ts +32 -21
- package/src/timelines.ts +67 -13
- package/src/types.ts +12 -6
- package/src/validate.ts +288 -13
- package/tools/editor_roundtrip.ts +1 -1
package/cli.ts
CHANGED
|
@@ -66,10 +66,24 @@ import {
|
|
|
66
66
|
type DeformKeyMeasure,
|
|
67
67
|
type DeformSpan,
|
|
68
68
|
} from './src/deformmeasure.ts';
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
diffLines,
|
|
71
|
+
diffSkeletons,
|
|
72
|
+
reportedFigures,
|
|
73
|
+
sectionFigures,
|
|
74
|
+
type DiffAnimationPair,
|
|
75
|
+
type DiffReport,
|
|
76
|
+
} from './src/diff.ts';
|
|
70
77
|
import { ingest, IngestError, IngestSpecRefused, INGEST_GUTTERS, type IngestFinding, type IngestStage } from './src/ingest.ts';
|
|
71
78
|
import { copyAtlasPages } from './src/emit.ts';
|
|
72
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
DEFAULT_PADDING,
|
|
81
|
+
DEFAULT_PAGE_SIZE,
|
|
82
|
+
packAtlas,
|
|
83
|
+
pageFootprint,
|
|
84
|
+
parseAtlasText,
|
|
85
|
+
type AtlasRegion,
|
|
86
|
+
} from './src/atlas.ts';
|
|
73
87
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
74
88
|
import { KEY_TIME_EPSILON } from './src/timelines.ts';
|
|
75
89
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
@@ -218,13 +232,16 @@ const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images',
|
|
|
218
232
|
/**
|
|
219
233
|
* The flags a command is allowed to spell more than once.
|
|
220
234
|
*
|
|
221
|
-
*
|
|
222
|
-
* candidates
|
|
235
|
+
* `vote --candidate` is, because a ballot is *by definition* several
|
|
236
|
+
* candidates, and `diff --as` is, because a skeleton has as many shots as it
|
|
237
|
+
* has and one pairing per flag is the only spelling that keeps each pair a pair
|
|
238
|
+
* (issue #720). Everywhere else a repeat is a mistake and is refused: `check
|
|
223
239
|
* --candidate a --candidate b` used to take `b` silently, which is a report
|
|
224
240
|
* about a rig the caller did not think they were asking about.
|
|
225
241
|
*/
|
|
226
242
|
const REPEATABLE_FLAGS: Record<string, ReadonlySet<string>> = {
|
|
227
243
|
vote: new Set(['candidate']),
|
|
244
|
+
diff: new Set(['as']),
|
|
228
245
|
};
|
|
229
246
|
|
|
230
247
|
/**
|
|
@@ -1155,6 +1172,45 @@ function dropLine(dropped: DroppedState): string {
|
|
|
1155
1172
|
return ` DROP ${dropped.slot}/${dropped.state}: ${droppedStateReason(dropped)} (state not emitted)`;
|
|
1156
1173
|
}
|
|
1157
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* The rectangle a region occupies **on its page**, for a line that has already
|
|
1177
|
+
* said where the region is — and the empty string where the page rectangle is
|
|
1178
|
+
* the one `bounds:` already states.
|
|
1179
|
+
*
|
|
1180
|
+
* ## The fact no surface an author reads carried (issue #718)
|
|
1181
|
+
*
|
|
1182
|
+
* The atlas line beside this clause prints the DRAWING's size, because that is
|
|
1183
|
+
* what an attachment's width and height mean. A packer that turned the drawing a
|
|
1184
|
+
* quarter to fit it wrote `bounds:` in the drawing's orientation too. So an
|
|
1185
|
+
* author holding the pack and the build report had neither end of the rectangle
|
|
1186
|
+
* they have to cut out of the page to measure a part against a rendered frame —
|
|
1187
|
+
* and the one place rigc printed it was `A06`'s overlap text, reachable only
|
|
1188
|
+
* under `--profile spine-html`. The knowledge was in the tree the whole time:
|
|
1189
|
+
* `pageFootprint` has derived this rectangle for every reader of it since issue
|
|
1190
|
+
* #579, and nothing an author reads said it.
|
|
1191
|
+
*
|
|
1192
|
+
* ⚠️ **The condition is `pageFootprint`'s own answer, not a second reading of
|
|
1193
|
+
* `degrees`.** Re-spelling that predicate here is the exact duplication #579 was
|
|
1194
|
+
* filed on — four readers derived this rectangle and two derived it wrongly — so
|
|
1195
|
+
* the clause asks the function whether its answer differs from the `bounds:`
|
|
1196
|
+
* line, and prints only then.
|
|
1197
|
+
*
|
|
1198
|
+
* 🔸 A consequence worth stating rather than leaving to be discovered: a region
|
|
1199
|
+
* whose KEPT rectangle is square is silent here, because a quarter turn leaves
|
|
1200
|
+
* its footprint the same two numbers and there is nothing the pack does not
|
|
1201
|
+
* already say. The general rule — `bounds` is the unturned size, the footprint
|
|
1202
|
+
* is its transpose at `rotate: 90` and `rotate: 270`, and which way to turn the
|
|
1203
|
+
* rectangle to recover the drawing — belongs to an author's own reading and is
|
|
1204
|
+
* stated in `docs/AUTHORING.md` §0.2, which holds for every region including
|
|
1205
|
+
* that one.
|
|
1206
|
+
*/
|
|
1207
|
+
function pageRectangle(region: AtlasRegion): string {
|
|
1208
|
+
const foot = pageFootprint(region);
|
|
1209
|
+
return foot.width === region.width && foot.height === region.height
|
|
1210
|
+
? ''
|
|
1211
|
+
: `, occupies ${foot.width}x${foot.height}`;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1158
1214
|
function cmdBuild(flags: Record<string, string>): void {
|
|
1159
1215
|
const { label, opts } = resolveCut(flags);
|
|
1160
1216
|
const profile = readProfile(flags);
|
|
@@ -1208,13 +1264,23 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1208
1264
|
// that declares a `scale:` also says so and shows the texels it was read
|
|
1209
1265
|
// from: the size on the left is the DRAWING's and the rectangle is the
|
|
1210
1266
|
// pack's, and issue #267 is the report that printed the second as the first.
|
|
1267
|
+
//
|
|
1268
|
+
// `pageRectangle` closes the line's last silence (issue #718), and it is
|
|
1269
|
+
// placed LAST rather than beside the turn it follows from, which is where
|
|
1270
|
+
// the card put it. The two clauses collide nowhere else, and the collision
|
|
1271
|
+
// is real: `scale 0.5 (373x106 texels)` is itself a size, so
|
|
1272
|
+
// `rotate 90, occupies 106x373 scale 0.5 (…)` reads as though the footprint
|
|
1273
|
+
// were the scaled quantity. As a trailing clause of the whole location
|
|
1274
|
+
// phrase it is unambiguous with a `scale:` line and identical to the card's
|
|
1275
|
+
// wording without one, which is every pack that has no `scale:` to state.
|
|
1211
1276
|
const where =
|
|
1212
1277
|
img.atlas === undefined
|
|
1213
1278
|
? img.page
|
|
1214
1279
|
: `${img.page} @ ${img.atlas.x},${img.atlas.y}${img.atlas.degrees ? ` rotate ${img.atlas.degrees}` : ''}` +
|
|
1215
1280
|
(img.atlasScale === undefined
|
|
1216
1281
|
? ''
|
|
1217
|
-
: ` scale ${img.atlasScale} (${img.atlas.originalWidth}x${img.atlas.originalHeight} texels)`)
|
|
1282
|
+
: ` scale ${img.atlasScale} (${img.atlas.originalWidth}x${img.atlas.originalHeight} texels)`) +
|
|
1283
|
+
pageRectangle(img.atlas);
|
|
1218
1284
|
console.log(` .. ${img.region.padEnd(24)} ${img.width}x${img.height} <- ${where}`);
|
|
1219
1285
|
}
|
|
1220
1286
|
for (const d of result.droppedStates) console.log(dropLine(d));
|
|
@@ -1403,7 +1469,69 @@ function cmdValidate(flags: Record<string, string>, positional: string[]): void
|
|
|
1403
1469
|
console.log('rigc: green');
|
|
1404
1470
|
}
|
|
1405
1471
|
|
|
1406
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* `--as <candidate>=<reference>`, one pair per occurrence.
|
|
1474
|
+
*
|
|
1475
|
+
* ⚠️ Spelled with a pair where `check --as <name>` takes one name, and the
|
|
1476
|
+
* difference is in what the two commands have on the other side. `check`
|
|
1477
|
+
* measures against a rendered frame SET, which already carries the reference
|
|
1478
|
+
* animation's name in its own directory, so one name closes the gap. `diff` has
|
|
1479
|
+
* two skeletons and either may have its own vocabulary, so one name says which
|
|
1480
|
+
* shot on which side and leaves the other unanswered. The direction — candidate
|
|
1481
|
+
* first — is the one `bonedist`'s correspondence file already writes its
|
|
1482
|
+
* `animations` map in.
|
|
1483
|
+
*
|
|
1484
|
+
* Every refusal here is a UsageError because every one of them is about the
|
|
1485
|
+
* flag's own value, and each names what it read: a value with no `=`, an empty
|
|
1486
|
+
* side, a name repeated on either side, and a name no animation on that side
|
|
1487
|
+
* answers to. ⛔ The last of those is a refusal rather than a dropped pair for
|
|
1488
|
+
* the reason a miss is refused by name everywhere else in this tool — a typo
|
|
1489
|
+
* that quietly measured less would be a report about a pairing the caller did
|
|
1490
|
+
* not ask for.
|
|
1491
|
+
*/
|
|
1492
|
+
function readAnimationPairs(values: string[], candidate: unknown, reference: unknown): DiffAnimationPair[] {
|
|
1493
|
+
const animationsOf = (root: unknown): string[] => {
|
|
1494
|
+
const anims = (root as { animations?: unknown } | null)?.animations;
|
|
1495
|
+
return typeof anims === 'object' && anims !== null && !Array.isArray(anims) ? Object.keys(anims) : [];
|
|
1496
|
+
};
|
|
1497
|
+
const have = { candidate: animationsOf(candidate), reference: animationsOf(reference) };
|
|
1498
|
+
const pairs: DiffAnimationPair[] = [];
|
|
1499
|
+
for (const value of values) {
|
|
1500
|
+
const at = value.indexOf('=');
|
|
1501
|
+
if (at < 0) {
|
|
1502
|
+
throw new UsageError(
|
|
1503
|
+
`--as ${JSON.stringify(value)} is not a pair. It takes <candidate>=<reference> — two animation names joined ` +
|
|
1504
|
+
'by `=`, because diff compares two skeletons and either may have its own name for the shot. The candidate ' +
|
|
1505
|
+
`has [${have.candidate.join(', ') || 'none'}] and the reference has [${have.reference.join(', ') || 'none'}].`,
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const pair = { candidate: value.slice(0, at), reference: value.slice(at + 1) };
|
|
1509
|
+
if (pair.candidate === '' || pair.reference === '') {
|
|
1510
|
+
throw new UsageError(
|
|
1511
|
+
`--as ${JSON.stringify(value)} leaves the ${pair.candidate === '' ? 'candidate' : 'reference'} side empty; ` +
|
|
1512
|
+
'it takes <candidate>=<reference>, a name on each side',
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
for (const side of ['candidate', 'reference'] as const) {
|
|
1516
|
+
if (!have[side].includes(pair[side])) {
|
|
1517
|
+
throw new UsageError(
|
|
1518
|
+
`--as ${JSON.stringify(value)} names no ${side} animation: the ${side} has ` +
|
|
1519
|
+
`[${have[side].join(', ') || 'none'}] and not ${JSON.stringify(pair[side])}`,
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (pairs.some((p) => p[side] === pair[side])) {
|
|
1523
|
+
throw new UsageError(
|
|
1524
|
+
`--as pairs the ${side} animation ${JSON.stringify(pair[side])} twice; each animation may be in one pair, ` +
|
|
1525
|
+
'or the block would compare one shot against two',
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
pairs.push(pair);
|
|
1530
|
+
}
|
|
1531
|
+
return pairs;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function cmdDiff(flags: Record<string, string>, lists: Record<string, string[]>, positional: string[]): void {
|
|
1407
1535
|
const [candidate, reference] = positional;
|
|
1408
1536
|
if (!candidate || !reference) throw new UsageError('diff takes two paths: <candidate.json> <reference.json>');
|
|
1409
1537
|
const candidatePath = resolve(candidate);
|
|
@@ -1411,7 +1539,10 @@ function cmdDiff(flags: Record<string, string>, positional: string[]): void {
|
|
|
1411
1539
|
for (const path of [candidatePath, referencePath]) {
|
|
1412
1540
|
if (!existsSync(path)) throw new UsageError(`nothing at ${path}`);
|
|
1413
1541
|
}
|
|
1414
|
-
const
|
|
1542
|
+
const candidateJson = readJsonFile(candidatePath);
|
|
1543
|
+
const referenceJson = readJsonFile(referencePath);
|
|
1544
|
+
const animationPairs = readAnimationPairs(lists.as ?? [], candidateJson, referenceJson);
|
|
1545
|
+
const report = diffSkeletons(candidateJson, referenceJson, { animationPairs });
|
|
1415
1546
|
console.log('rigc diff');
|
|
1416
1547
|
for (const line of diffLines(report, { candidate: candidatePath, reference: referencePath })) console.log(line);
|
|
1417
1548
|
if (flags.json !== undefined) {
|
|
@@ -3266,8 +3397,26 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3266
3397
|
},
|
|
3267
3398
|
{
|
|
3268
3399
|
name: 'diff',
|
|
3269
|
-
usage: ['rigc diff <candidate.json> <reference.json> [--json <out>]'],
|
|
3270
|
-
flags: ['json'],
|
|
3400
|
+
usage: ['rigc diff <candidate.json> <reference.json> [--as <candidate>=<reference>]… [--json <out>]'],
|
|
3401
|
+
flags: ['as', 'json'],
|
|
3402
|
+
overrides: {
|
|
3403
|
+
as: {
|
|
3404
|
+
value: '<candidate>=<reference>',
|
|
3405
|
+
meaning:
|
|
3406
|
+
'pair a candidate animation with a reference one, so the name-agnostic `animations` block can be ' +
|
|
3407
|
+
'measured over shots the two files call different things. Repeatable, one pair each. An INPUT and never ' +
|
|
3408
|
+
'derived: two skeletons cannot say which of their shots are the same shot. Without it the block appears ' +
|
|
3409
|
+
'only when each side has exactly one animation, which pairs by position, and is otherwise absent rather ' +
|
|
3410
|
+
'than guessed',
|
|
3411
|
+
},
|
|
3412
|
+
},
|
|
3413
|
+
notes: [
|
|
3414
|
+
'the `animations` block reads two figures once something has paired the shots, exactly as',
|
|
3415
|
+
'`bones` and `slots` do: name-matched, where `names` lives, and name-agnostic over the pair.',
|
|
3416
|
+
'A candidate that followed a brief withholding the animation name reads `count` 1/1 and 0.000',
|
|
3417
|
+
'on every other name-matched measure — including `duration` and `key_counts` it may have got',
|
|
3418
|
+
'exactly right — so read the pair and not the section mean.',
|
|
3419
|
+
],
|
|
3271
3420
|
},
|
|
3272
3421
|
{
|
|
3273
3422
|
name: 'check',
|
|
@@ -3562,7 +3711,7 @@ try {
|
|
|
3562
3711
|
else if (command === 'ingest') cmdIngest(flags, positional);
|
|
3563
3712
|
else if (command === 'validate') cmdValidate(flags, positional);
|
|
3564
3713
|
else if (command === 'explain') cmdExplain(flags);
|
|
3565
|
-
else if (command === 'diff') cmdDiff(flags, positional);
|
|
3714
|
+
else if (command === 'diff') cmdDiff(flags, lists, positional);
|
|
3566
3715
|
else if (command === 'check') cmdCheck(flags);
|
|
3567
3716
|
else if (command === 'bench') cmdBench(flags, positional);
|
|
3568
3717
|
else if (command === 'bonedist') cmdBoneDist(flags);
|