spine-rigc 2.1.2 → 2.1.3
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 +3 -1
- package/package.json +1 -1
- package/tools/editor_roundtrip.ts +129 -41
package/README.md
CHANGED
|
@@ -699,7 +699,9 @@ declares, or the banner the binary prints about itself — rather than starting
|
|
|
699
699
|
and failing downstream. Both refusals point at `--exported <file>`, which
|
|
700
700
|
measures an export the editor already made and is the half of this tool that
|
|
701
701
|
needs no editor at all. A build whose `skeleton.json` is missing, is not JSON, or
|
|
702
|
-
holds JSON that is not one object is refused by name before the editor starts
|
|
702
|
+
holds JSON that is not one object is refused by name before the editor starts,
|
|
703
|
+
and so is a build with no atlas or an atlas that is a directory, or an
|
|
704
|
+
`--exported` file that is not one JSON object.
|
|
703
705
|
|
|
704
706
|
⛔ **Run the round trip by hand, on a machine that has the editor.** CI has no
|
|
705
707
|
editor, so no automated check runs it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -968,31 +968,95 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
968
968
|
* it was and every line after this prints as it did.
|
|
969
969
|
*/
|
|
970
970
|
function readBuildSkeleton(source: string): { skeleton: Record<string, unknown> } | { refusal: string } {
|
|
971
|
-
|
|
971
|
+
return readSkeletonObject(
|
|
972
|
+
source,
|
|
973
|
+
"the build's skeleton.json",
|
|
972
974
|
'The round trip imports that file into the editor, so it has to be one skeleton JSON object — `skeleton`, ' +
|
|
973
|
-
|
|
974
|
-
|
|
975
|
+
'`bones`, `slots`, … — which is what `rigc build` writes there.',
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* One skeleton file read and parsed, with its bytes — or the sentence the tool
|
|
981
|
+
* refuses it with, naming `what` it is, its path, what was found there and the
|
|
982
|
+
* `required` sentence. The one body behind `readBuildSkeleton` and the
|
|
983
|
+
* `--exported` read (issue #1107), so the two files are refused in one shape.
|
|
984
|
+
*
|
|
985
|
+
* The bytes are handed back because the `--exported` file is written into the
|
|
986
|
+
* candidate from them: the file step 3 measures is then the file this parsed,
|
|
987
|
+
* byte for byte, rather than whatever is at the path by the time step 3 runs.
|
|
988
|
+
*/
|
|
989
|
+
function readSkeletonObject(
|
|
990
|
+
path: string,
|
|
991
|
+
what: string,
|
|
992
|
+
required: string,
|
|
993
|
+
): { skeleton: Record<string, unknown>; bytes: Buffer } | { refusal: string } {
|
|
994
|
+
let bytes: Buffer;
|
|
975
995
|
try {
|
|
976
|
-
|
|
996
|
+
bytes = readFileSync(path);
|
|
977
997
|
} catch (err) {
|
|
978
998
|
const code = (err as NodeJS.ErrnoException).code;
|
|
979
|
-
if (code === 'EISDIR') return { refusal:
|
|
980
|
-
return { refusal:
|
|
999
|
+
if (code === 'EISDIR') return { refusal: `${what} at ${path} is a directory, not a file. ${required}` };
|
|
1000
|
+
return { refusal: `${what} at ${path} could not be read (${code ?? String(err)}). ${required}` };
|
|
981
1001
|
}
|
|
1002
|
+
const text = bytes.toString('utf8');
|
|
982
1003
|
if (text.trim() === '') {
|
|
983
|
-
return { refusal:
|
|
1004
|
+
return { refusal: `${what} at ${path} is empty (${text.length} byte(s), no JSON value in them). ${required}` };
|
|
984
1005
|
}
|
|
985
1006
|
let parsed: unknown;
|
|
986
1007
|
try {
|
|
987
1008
|
parsed = JSON.parse(text);
|
|
988
1009
|
} catch (err) {
|
|
989
1010
|
const said = err instanceof Error ? err.message : String(err);
|
|
990
|
-
return { refusal:
|
|
1011
|
+
return { refusal: `${what} at ${path} is not JSON — the parser stopped with "${said}". ${required}` };
|
|
991
1012
|
}
|
|
992
1013
|
if (!isRecord(parsed)) {
|
|
993
|
-
return { refusal:
|
|
1014
|
+
return { refusal: `${what} at ${path} is JSON, but it holds ${jsonKind(parsed)} rather than an object. ${required}` };
|
|
994
1015
|
}
|
|
995
|
-
return { skeleton: parsed };
|
|
1016
|
+
return { skeleton: parsed, bytes };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* The build's atlas, read once before step 1, or the sentence the tool refuses
|
|
1021
|
+
* it with (issue #1107): its name and the page names it lists.
|
|
1022
|
+
*
|
|
1023
|
+
* 🚨 This read used to sit at step 3, after the editor had imported and
|
|
1024
|
+
* exported. So a build with no atlas was refused by name only after a licensed
|
|
1025
|
+
* editor had spent an import and an export on it, and a directory named
|
|
1026
|
+
* `*.atlas` beside the build threw `EISDIR` there as a stack. Neither needs the
|
|
1027
|
+
* editor to answer: the atlas is rigc's input to step 3, not the editor's.
|
|
1028
|
+
*
|
|
1029
|
+
* ⭐ Every `*.atlas` entry is read, not only the one whose pages are listed,
|
|
1030
|
+
* because step 3 copies every one of them beside the export, and a directory
|
|
1031
|
+
* among them throws at that copy. The first entry is the atlas, as it always was.
|
|
1032
|
+
*
|
|
1033
|
+
* ⚠️ "Read" is all this can mean. The tool's own reader of an atlas is the line
|
|
1034
|
+
* filter below, which accepts any text, so there is no malformed atlas for it to
|
|
1035
|
+
* refuse; what the atlas SAYS is the gate's to judge at step 3 (`A08`, `A17`).
|
|
1036
|
+
*/
|
|
1037
|
+
function readBuildAtlas(build: string): { pageNames: string[] } | { refusal: string } {
|
|
1038
|
+
const atlases = readdirSync(build).filter((f) => f.endsWith('.atlas'));
|
|
1039
|
+
if (atlases.length === 0) return { refusal: `no .atlas in the build directory ${build}` };
|
|
1040
|
+
const required =
|
|
1041
|
+
'Step 3 copies the build\'s atlas and pages beside the export so the export can be loaded and measured, so ' +
|
|
1042
|
+
'every `.atlas` there has to be a file — the one `rigc build` writes.';
|
|
1043
|
+
let first: string | null = null;
|
|
1044
|
+
for (const name of atlases) {
|
|
1045
|
+
const path = join(build, name);
|
|
1046
|
+
try {
|
|
1047
|
+
const text = readFileSync(path, 'utf8');
|
|
1048
|
+
if (first === null) first = text;
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
1051
|
+
if (code === 'EISDIR') return { refusal: `the build's atlas at ${path} is a directory, not a file. ${required}` };
|
|
1052
|
+
return { refusal: `the build's atlas at ${path} could not be read (${code ?? String(err)}). ${required}` };
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const pageNames = (first ?? '')
|
|
1056
|
+
.split('\n')
|
|
1057
|
+
.filter((line) => /\.(png|jpg|jpeg)\s*$/i.test(line.trim()) && !line.startsWith(' ') && !line.startsWith('\t'))
|
|
1058
|
+
.map((line) => line.trim());
|
|
1059
|
+
return { pageNames };
|
|
996
1060
|
}
|
|
997
1061
|
|
|
998
1062
|
/**
|
|
@@ -1111,10 +1175,14 @@ function main(): void {
|
|
|
1111
1175
|
// makes — and `--pack --copy-images` is refused by `cli.ts`, correctly,
|
|
1112
1176
|
// because a packed atlas does not reference loose parts.
|
|
1113
1177
|
//
|
|
1114
|
-
// ⛔ It is checked HERE, before step 1,
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1178
|
+
// ⛔ It is checked HERE, before step 1, because it matters to the editor's
|
|
1179
|
+
// import in step 1. The atlas and an `--exported` file are read before step 1
|
|
1180
|
+
// as well, just below, although nothing before step 3 uses them (issue #1107):
|
|
1181
|
+
// they are rigc's inputs and not the editor's, so reading them first costs the
|
|
1182
|
+
// editor nothing and refuses before a licensed editor is started on a trip
|
|
1183
|
+
// that cannot finish — the reason `readBuildSkeleton` reads the build's
|
|
1184
|
+
// skeleton first (#1090). So the order is: every input rigc reads is read
|
|
1185
|
+
// before the editor starts, and only what the editor writes is read after it.
|
|
1118
1186
|
{
|
|
1119
1187
|
interface ImagesProbe {
|
|
1120
1188
|
skeleton?: { images?: string };
|
|
@@ -1167,6 +1235,48 @@ function main(): void {
|
|
|
1167
1235
|
}
|
|
1168
1236
|
}
|
|
1169
1237
|
|
|
1238
|
+
// The export is a skeleton only; it needs the build's atlas and pages beside
|
|
1239
|
+
// it to be a candidate anything can load — so the atlas is step 3's, and it is
|
|
1240
|
+
// read here, before step 1, for the reason the sentence above gives.
|
|
1241
|
+
//
|
|
1242
|
+
// 🚨 Which is why the build has to be SELF-CONTAINED, and this refuses when it
|
|
1243
|
+
// is not. An ordinary build's atlas names its pages by a relative path back to
|
|
1244
|
+
// the art directory; copy that atlas to a directory at another depth and every
|
|
1245
|
+
// page name resolves to nothing. Found by running this tool — it reported four
|
|
1246
|
+
// `A17_ATLAS_PAGE_FILES_EXIST` failures that were the harness's fault and not
|
|
1247
|
+
// the editor's, which is the worst kind of red: a real assertion, correctly
|
|
1248
|
+
// fired, pointing at the wrong culprit.
|
|
1249
|
+
const atlas = readBuildAtlas(opts.build);
|
|
1250
|
+
if ('refusal' in atlas) return fail(atlas.refusal);
|
|
1251
|
+
const wandering = atlas.pageNames.filter((n) => n.includes('/'));
|
|
1252
|
+
if (wandering.length > 0) {
|
|
1253
|
+
fail(
|
|
1254
|
+
`the build's atlas names its pages by path, not by filename — the first is "${wandering[0]}". ` +
|
|
1255
|
+
'The round trip copies the atlas beside the export, at a different depth, so every one of those ' +
|
|
1256
|
+
`${wandering.length} page name(s) would resolve to nothing and A17 would blame the editor for it. ` +
|
|
1257
|
+
'Rebuild with `--copy-images`, which puts the pages beside the skeleton and names them plainly.',
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// 🔒 The `--exported` file, read and parsed here for the same reason (issue
|
|
1262
|
+
// #1107): one skeleton JSON object, in `readBuildSkeleton`'s shape, or refused
|
|
1263
|
+
// by name. It used to be read first by step 4's `skinsDeclaredBy`, bare, so a
|
|
1264
|
+
// file that was not JSON was gated and diffed by steps 3–4 and then left the
|
|
1265
|
+
// tool as a `SyntaxError` stack. Step 3's candidate is written from these
|
|
1266
|
+
// bytes, so what is measured is what was parsed.
|
|
1267
|
+
let exportedBytes: Buffer | null = null;
|
|
1268
|
+
if (opts.exported !== null) {
|
|
1269
|
+
if (!existsSync(opts.exported)) fail(`no such export: ${opts.exported}`);
|
|
1270
|
+
const got = readSkeletonObject(
|
|
1271
|
+
opts.exported,
|
|
1272
|
+
'the --exported file',
|
|
1273
|
+
'Steps 3-6 measure that file as the editor\'s JSON export of this build, so it has to be one skeleton JSON ' +
|
|
1274
|
+
'object — `skeleton`, `bones`, `slots`, … — which is what the editor\'s `-e json` export writes.',
|
|
1275
|
+
);
|
|
1276
|
+
if ('refusal' in got) return fail(got.refusal);
|
|
1277
|
+
exportedBytes = got.bytes;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1170
1280
|
rmSync(opts.out, { recursive: true, force: true });
|
|
1171
1281
|
mkdirSync(join(opts.out, 'export'), { recursive: true });
|
|
1172
1282
|
mkdirSync(join(opts.out, 'export-cand'), { recursive: true });
|
|
@@ -1177,7 +1287,6 @@ function main(): void {
|
|
|
1177
1287
|
|
|
1178
1288
|
let exportedJson: string;
|
|
1179
1289
|
if (opts.exported !== null) {
|
|
1180
|
-
if (!existsSync(opts.exported)) fail(`no such export: ${opts.exported}`);
|
|
1181
1290
|
exportedJson = opts.exported;
|
|
1182
1291
|
emit(` editor NOT RUN — measuring an export the editor already made: ${opts.exported}`);
|
|
1183
1292
|
emit('');
|
|
@@ -1246,33 +1355,12 @@ function main(): void {
|
|
|
1246
1355
|
exportedJson = join(opts.out, 'export', written[0]);
|
|
1247
1356
|
}
|
|
1248
1357
|
|
|
1249
|
-
// The
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
// 🚨 Which is why the build has to be SELF-CONTAINED, and this refuses when it
|
|
1253
|
-
// is not. An ordinary build's atlas names its pages by a relative path back to
|
|
1254
|
-
// the art directory; copy that atlas to a directory at another depth and every
|
|
1255
|
-
// page name resolves to nothing. Found by running this tool — it reported four
|
|
1256
|
-
// `A17_ATLAS_PAGE_FILES_EXIST` failures that were the harness's fault and not
|
|
1257
|
-
// the editor's, which is the worst kind of red: a real assertion, correctly
|
|
1258
|
-
// fired, pointing at the wrong culprit.
|
|
1358
|
+
// The candidate: the export beside the build's atlas and pages, which were
|
|
1359
|
+
// read before step 1 (see `readBuildAtlas`). An `--exported` file is written
|
|
1360
|
+
// from the bytes parsed there; the editor's own export is copied from disk.
|
|
1259
1361
|
const cand = join(opts.out, 'export-cand');
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
const pageNames = readFileSync(join(opts.build, atlasName!), 'utf8')
|
|
1263
|
-
.split('\n')
|
|
1264
|
-
.filter((line) => /\.(png|jpg|jpeg)\s*$/i.test(line.trim()) && !line.startsWith(' ') && !line.startsWith('\t'))
|
|
1265
|
-
.map((line) => line.trim());
|
|
1266
|
-
const wandering = pageNames.filter((n) => n.includes('/'));
|
|
1267
|
-
if (wandering.length > 0) {
|
|
1268
|
-
fail(
|
|
1269
|
-
`the build's atlas names its pages by path, not by filename — the first is "${wandering[0]}". ` +
|
|
1270
|
-
'The round trip copies the atlas beside the export, at a different depth, so every one of those ' +
|
|
1271
|
-
`${wandering.length} page name(s) would resolve to nothing and A17 would blame the editor for it. ` +
|
|
1272
|
-
'Rebuild with `--copy-images`, which puts the pages beside the skeleton and names them plainly.',
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
copyFileSync(exportedJson, join(cand, 'skeleton.json'));
|
|
1362
|
+
if (exportedBytes !== null) writeFileSync(join(cand, 'skeleton.json'), exportedBytes);
|
|
1363
|
+
else copyFileSync(exportedJson, join(cand, 'skeleton.json'));
|
|
1276
1364
|
for (const f of readdirSync(opts.build)) {
|
|
1277
1365
|
if (f.endsWith('.atlas') || f.endsWith('.png')) copyFileSync(join(opts.build, f), join(cand, f));
|
|
1278
1366
|
}
|