spine-rigc 0.22.2 → 0.23.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 +64 -1
- package/cli.ts +205 -11
- package/docs/AUTHORING.md +357 -54
- package/docs/INGEST.md +185 -41
- package/docs/SPEC_COVERAGE.md +13 -1
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +57 -13
- package/src/check.ts +83 -1
- package/src/compile.ts +284 -58
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1078 -0
- package/src/render.ts +104 -10
- package/src/rig.ts +92 -8
- package/src/types.ts +39 -7
- package/src/validate.ts +205 -27
- package/tools/editor_roundtrip.ts +172 -21
package/src/compile.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { parseJsonWithPosition } from './json-position.ts';
|
|
|
41
41
|
import { nearMisses } from './keys.ts';
|
|
42
42
|
import { parseMotionSpec } from './motion.ts';
|
|
43
43
|
import {
|
|
44
|
+
declaresNoStage,
|
|
44
45
|
parseRigSpec,
|
|
45
46
|
RIG_FROM_PROPERTIES,
|
|
46
47
|
RIG_PATH_POSITION_MODES,
|
|
@@ -1363,6 +1364,13 @@ function resolveFromAtlas(
|
|
|
1363
1364
|
* own, only a rectangle of somebody's page. `extractRegion` lifts the drawing
|
|
1364
1365
|
* back out, so the generator sees the same grid either way and its output does
|
|
1365
1366
|
* not depend on how the art was delivered.
|
|
1367
|
+
*
|
|
1368
|
+
* ⭐ *Either way* was not true for a pack that turns a region until issue #570:
|
|
1369
|
+
* `extractRegion` refused a `rotate: 90`, so how the art was delivered decided
|
|
1370
|
+
* whether a measurement could be taken at all — on a foreign pack, where turning
|
|
1371
|
+
* is the norm rather than the exception. It now transcribes the runtime's own
|
|
1372
|
+
* mapping for all four rotations, which is what makes the sentence above a
|
|
1373
|
+
* description rather than an aspiration.
|
|
1366
1374
|
*/
|
|
1367
1375
|
function partPlate(img: CompiledImage): Plate {
|
|
1368
1376
|
const page = readPlate(img.absPath);
|
|
@@ -1433,15 +1441,34 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1433
1441
|
// The stage. The rig may state it outright (a foreign skeleton has no crop);
|
|
1434
1442
|
// otherwise the manifest's crop is it. With neither there is nothing to
|
|
1435
1443
|
// measure a full-frame mesh against, so the compile stops rather than guess.
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1444
|
+
//
|
|
1445
|
+
// ⭐ The third state is the rig saying there is no stage at all — `width` and
|
|
1446
|
+
// `height` stated `null` (issue #578). That is a claim rather than a silence,
|
|
1447
|
+
// so it beats the manifest's crop the way a stated number already does, and
|
|
1448
|
+
// the header below emits none of the four fields. Omitting them is still the
|
|
1449
|
+
// refusal underneath: a transcriber who has no stage to copy can now say so,
|
|
1450
|
+
// and one who simply has not looked still cannot.
|
|
1451
|
+
const noStage = declaresNoStage(rig.skeleton);
|
|
1452
|
+
const stageWidth = noStage ? undefined : (rig.skeleton?.width ?? manifest?.crop.w);
|
|
1453
|
+
const stageHeight = noStage ? undefined : (rig.skeleton?.height ?? manifest?.crop.h);
|
|
1454
|
+
if (!noStage && (stageWidth === undefined || stageHeight === undefined)) {
|
|
1439
1455
|
throw new CompileError(
|
|
1440
|
-
'no stage size: give the rig spec a `skeleton.width`/`skeleton.height`, or compile against a cut manifest whose `crop` states them'
|
|
1456
|
+
'no stage size: give the rig spec a `skeleton.width`/`skeleton.height`, or compile against a cut manifest whose `crop` states them, ' +
|
|
1457
|
+
'or state `"width": null, "height": null` for a skeleton that declares no stage',
|
|
1441
1458
|
);
|
|
1442
1459
|
}
|
|
1443
|
-
/**
|
|
1444
|
-
|
|
1460
|
+
/**
|
|
1461
|
+
* Crop height, for the y-down -> y-up flip. Only manifest data uses it, and
|
|
1462
|
+
* `cropPointOf` refuses every bone that asks for manifest data when there is
|
|
1463
|
+
* no manifest — so the two states that can be read here both have one, or a
|
|
1464
|
+
* stage.
|
|
1465
|
+
*
|
|
1466
|
+
* ⚠️ `NaN` for the third, unreachable state (no manifest and a rig that
|
|
1467
|
+
* declares no stage) rather than 0. A plausible number is the failure this
|
|
1468
|
+
* compiler exists to refuse; if the guard above it ever moved, a NaN
|
|
1469
|
+
* coordinate is a named failure and an origin-flipped bone is not.
|
|
1470
|
+
*/
|
|
1471
|
+
const cropH = manifest?.crop.h ?? stageHeight ?? Number.NaN;
|
|
1445
1472
|
const imagesDir = opts.imagesDir !== undefined ? resolve(opts.imagesDir) : resolve(dirname(rigPath), rig.images ?? '.');
|
|
1446
1473
|
|
|
1447
1474
|
// -- 1. gather images ------------------------------------------------------
|
|
@@ -1806,16 +1833,29 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1806
1833
|
for (const rigSlot of rig.slots) {
|
|
1807
1834
|
const part = partBySlot.get(rigSlot.name);
|
|
1808
1835
|
const names = slotAttachments.get(rigSlot.name) ?? rigAttachmentNames.get(rigSlot.name) ?? [];
|
|
1809
|
-
|
|
1836
|
+
// 🔑 A slot nothing fills is EMITTED, empty — it is not dropped (issue #575).
|
|
1837
|
+
// A `continue` stood here instead, and what it bought was the format's own
|
|
1838
|
+
// silence: the emitted array came back a slot short with no line saying
|
|
1839
|
+
// which, and every slot after it moved down one. That index is what a
|
|
1840
|
+
// `drawOrder` key's offsets are counted against and what an index-keyed
|
|
1841
|
+
// consumer splits on, so the drop is not a smaller file, it is a different
|
|
1842
|
+
// rig. Two production exports declaring 53 and 61 slots built green at 51
|
|
1843
|
+
// and 57 and read 0.962 / 0.934 against the file they were transcribed from.
|
|
1844
|
+
//
|
|
1845
|
+
// The shape emitted here is the editor's own: `SkeletonJson`'s slot reader
|
|
1846
|
+
// takes `attachment` with a `null` default, so a slot with no `attachment`
|
|
1847
|
+
// key is a slot that shows nothing — 34 of the 52 slots in the official
|
|
1848
|
+
// `spineboy-pro` export omit the key.
|
|
1849
|
+
const empty = names.length === 0;
|
|
1810
1850
|
|
|
1811
1851
|
const setup = motion.setup?.[rigSlot.name];
|
|
1812
1852
|
// ⚠️ The entry's SHAPE is `parseMotionSpec`'s now (issue #307), and it had to
|
|
1813
|
-
// move: this loop
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1853
|
+
// move: this loop used to `continue` past a slot with no attachments before
|
|
1854
|
+
// reaching here, so the #293 shapes — `"lid_l": null` and, far worse,
|
|
1855
|
+
// `"lid_l": "plate"`, which reads `.attachment` off a string as `undefined`
|
|
1856
|
+
// and hides the slot in silence — stayed GREEN for exactly the slots a
|
|
1857
|
+
// reader is most likely to be halfway through wiring up. Everything from
|
|
1858
|
+
// here down is the half that needs the rig in front of it.
|
|
1819
1859
|
if (setup !== undefined && rigSlot.attachment !== undefined) {
|
|
1820
1860
|
throw new CompileError(
|
|
1821
1861
|
`slot "${rigSlot.name}" has a setup attachment in the rig spec AND in the motion spec; the setup pose has one author`,
|
|
@@ -1824,6 +1864,11 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1824
1864
|
let setupAttachment: string | null;
|
|
1825
1865
|
if (setup !== undefined) setupAttachment = setup.attachment ?? null;
|
|
1826
1866
|
else if (rigSlot.attachment !== undefined) setupAttachment = rigSlot.attachment;
|
|
1867
|
+
// Nothing fills the slot, so there is nothing to choose between and nothing
|
|
1868
|
+
// to guess: the setup pose of an empty slot is "show nothing", which is the
|
|
1869
|
+
// one value the format can express for it. The refusal below stays exactly
|
|
1870
|
+
// where it was for a slot that HAS attachments and states no setup pose.
|
|
1871
|
+
else if (empty) setupAttachment = null;
|
|
1827
1872
|
else {
|
|
1828
1873
|
throw new CompileError(
|
|
1829
1874
|
`no setup pose for slot "${rigSlot.name}": give the motion spec a \`setup\` entry or the rig slot an \`attachment\` — the compiler will not guess one`,
|
|
@@ -1831,7 +1876,11 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1831
1876
|
}
|
|
1832
1877
|
if (setupAttachment !== null && !names.includes(setupAttachment)) {
|
|
1833
1878
|
throw new CompileError(
|
|
1834
|
-
|
|
1879
|
+
empty
|
|
1880
|
+
? `the setup pose shows attachment "${setupAttachment}" on slot "${rigSlot.name}", which no skin and no ` +
|
|
1881
|
+
'manifest part fills — the slot is emitted empty, so there is no such attachment to show. Give the ' +
|
|
1882
|
+
'slot an attachment, or state the setup pose as null'
|
|
1883
|
+
: `setup attachment "${setupAttachment}" for slot "${rigSlot.name}" is not one of [${names.join(', ')}]`,
|
|
1835
1884
|
);
|
|
1836
1885
|
}
|
|
1837
1886
|
if (setup?.color && rigSlot.color !== undefined) {
|
|
@@ -1844,6 +1893,11 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
1844
1893
|
if (rigSlot.dark !== undefined) slot.dark = rigSlot.dark;
|
|
1845
1894
|
if (rigSlot.blend !== undefined) slot.blend = rigSlot.blend;
|
|
1846
1895
|
slots.push(slot);
|
|
1896
|
+
// ...and nothing below this line has anything to build. An empty entry in a
|
|
1897
|
+
// skin's attachment table would be rigc writing a key the editor does not,
|
|
1898
|
+
// so the skins array is left exactly as it was before #575 for every slot
|
|
1899
|
+
// that IS filled, and gains nothing for one that is not.
|
|
1900
|
+
if (empty) continue;
|
|
1847
1901
|
|
|
1848
1902
|
if (part) {
|
|
1849
1903
|
const perSlot: Record<string, SpineAttachment> = {};
|
|
@@ -2288,13 +2342,19 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
2288
2342
|
});
|
|
2289
2343
|
|
|
2290
2344
|
// -- 6. assemble -----------------------------------------------------------
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
};
|
|
2345
|
+
//
|
|
2346
|
+
// The stage is four fields or none of them. `x`/`y` are the origin of the box
|
|
2347
|
+
// `width`/`height` give an extent to, so a header carrying an origin for a box
|
|
2348
|
+
// it does not declare would be a shape no export has — and the key ORDER here
|
|
2349
|
+
// is the editor's own, which is what keeps a staged build byte-identical to
|
|
2350
|
+
// what it emitted before the stage could be declared absent (issue #578).
|
|
2351
|
+
const header: SpineSkeletonJson['skeleton'] = { spine: SPINE_VERSION };
|
|
2352
|
+
if (stageWidth !== undefined && stageHeight !== undefined) {
|
|
2353
|
+
header.x = rig.skeleton?.x ?? 0;
|
|
2354
|
+
header.y = rig.skeleton?.y ?? 0;
|
|
2355
|
+
header.width = stageWidth;
|
|
2356
|
+
header.height = stageHeight;
|
|
2357
|
+
}
|
|
2298
2358
|
if (rig.skeleton?.fps !== undefined) header.fps = rig.skeleton.fps;
|
|
2299
2359
|
if (rig.skeleton?.referenceScale !== undefined) header.referenceScale = rig.skeleton.referenceScale;
|
|
2300
2360
|
const imagesPath = skeletonImagesPath(rig.skeleton?.images, opts, outDir, partDirs);
|
|
@@ -2796,6 +2856,39 @@ interface AttachmentContext {
|
|
|
2796
2856
|
slotNames: Set<string>;
|
|
2797
2857
|
}
|
|
2798
2858
|
|
|
2859
|
+
/**
|
|
2860
|
+
* The seven `type` values `readAttachment` has a branch for (`:540-651`), and
|
|
2861
|
+
* the five rigc emits.
|
|
2862
|
+
*
|
|
2863
|
+
* ⚠️ The lists are separate because the refusals are separate. A `point` is a
|
|
2864
|
+
* name the format HAS and rigc has not built; `sequence` is not a type at all —
|
|
2865
|
+
* it is a key on a region or a mesh (SPEC_COVERAGE part 1-6) — and telling an
|
|
2866
|
+
* author that it "is in the Spine 4.3 format and rigc does not emit it yet"
|
|
2867
|
+
* promises work that will never be done, on a spelling that is simply wrong.
|
|
2868
|
+
* One message said exactly that about every string it did not recognise.
|
|
2869
|
+
*/
|
|
2870
|
+
const SPINE_ATTACHMENT_TYPES = ['region', 'mesh', 'linkedmesh', 'boundingbox', 'path', 'point', 'clipping'] as const;
|
|
2871
|
+
const EMITTED_ATTACHMENT_TYPES = ['region', 'mesh', 'boundingbox', 'clipping', 'path'] as const;
|
|
2872
|
+
|
|
2873
|
+
/**
|
|
2874
|
+
* What each deferred type is and what it would carry — `docs/SPEC_COVERAGE.md`
|
|
2875
|
+
* part 1-6's two rows, restated where the refusal can print them.
|
|
2876
|
+
*
|
|
2877
|
+
* ⭐ The construct, not just its name. "attachment type X is in the Spine 4.3
|
|
2878
|
+
* format and rigc does not emit it yet" tells an author who already knows what a
|
|
2879
|
+
* linked mesh is that rigc will not do it, and tells an author who does not know
|
|
2880
|
+
* nothing at all — and the second is the reader this repository writes for.
|
|
2881
|
+
*/
|
|
2882
|
+
const DEFERRED_ATTACHMENTS: Record<string, string> = {
|
|
2883
|
+
linkedmesh:
|
|
2884
|
+
'a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then ' +
|
|
2885
|
+
'"source" (the attachment it links to, and the key that MAKES it linked), "slot" and "skin" naming where ' +
|
|
2886
|
+
'that source lives, and "timelines" (default true) for whether it follows the source\'s deform keys',
|
|
2887
|
+
point:
|
|
2888
|
+
'a position and an angle with no geometry at all — "x", "y", "rotation" and "color", posed by its bone and ' +
|
|
2889
|
+
'drawn by nothing; what reads it is game code asking where a muzzle or a hand is',
|
|
2890
|
+
};
|
|
2891
|
+
|
|
2799
2892
|
/**
|
|
2800
2893
|
* Build one attachment a rig spec authored, as opposed to one a manifest part
|
|
2801
2894
|
* produced.
|
|
@@ -2804,6 +2897,18 @@ interface AttachmentContext {
|
|
|
2804
2897
|
* attachment type it does not know is to return null and drop it
|
|
2805
2898
|
* (`SkeletonJson.ts:653`), so passing an unimplemented type through would produce
|
|
2806
2899
|
* a skeleton missing an attachment nobody was told about.
|
|
2900
|
+
*
|
|
2901
|
+
* 🚨 **`?? 'region'` is not the parser's default, and that gap was the one real
|
|
2902
|
+
* fall-through here** (issue #577). `getValue(map, "type", "region")` returns the
|
|
2903
|
+
* default only when the key is **missing** (`SkeletonJson.ts:527`, `getValue` at
|
|
2904
|
+
* `:1390`), so `"type": null` is a type the parser HAS and matches no case: the
|
|
2905
|
+
* switch falls off the end, `readAttachment` returns null, and the attachment
|
|
2906
|
+
* disappears. `att.type ?? 'region'` read that same map as a region and compiled
|
|
2907
|
+
* one, which is the compiler inventing a value the spec did not state. Measured
|
|
2908
|
+
* before the repair: `{"type": null, "image": "marker.png"}` compiled and gated
|
|
2909
|
+
* green; `{"type": null}` was refused as *a region needs width and height*, which
|
|
2910
|
+
* is the message the card for this issue quoted and the reason it read as a
|
|
2911
|
+
* `linkedmesh` fault. `"type": "linkedmesh"` itself was always refused by name.
|
|
2807
2912
|
*/
|
|
2808
2913
|
function buildRigAttachment(
|
|
2809
2914
|
att: RigAttachment,
|
|
@@ -2811,18 +2916,55 @@ function buildRigAttachment(
|
|
|
2811
2916
|
where: string,
|
|
2812
2917
|
ctx: AttachmentContext,
|
|
2813
2918
|
): SpineAttachment {
|
|
2814
|
-
const
|
|
2919
|
+
const stated = (att as { type?: unknown }).type;
|
|
2920
|
+
if (stated !== undefined && typeof stated !== 'string') {
|
|
2921
|
+
throw new CompileError(
|
|
2922
|
+
`${where}: "type" is ${JSON.stringify(stated) ?? String(stated)}, which is not a name. An attachment's type ` +
|
|
2923
|
+
`is one of ${SPINE_ATTACHMENT_TYPES.join(', ')}, or the key is absent and reads as "region". ` +
|
|
2924
|
+
'PRESENT-and-null is not absent: `getValue(map, "type", "region")` takes the default only when the key is ' +
|
|
2925
|
+
'missing (`SkeletonJson.ts:527`), so this map matches no case, `readAttachment` returns null (`:653`), and ' +
|
|
2926
|
+
'the attachment is dropped from the skeleton without a word. Remove the key, or name a type.',
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
const type = stated ?? 'region';
|
|
2930
|
+
// A linked mesh in the format's OTHER spelling. `type: "mesh"` and `type:
|
|
2931
|
+
// "linkedmesh"` share one branch and the `source` key is what decides between
|
|
2932
|
+
// them (`:568-569`, `:582`; SPEC_COVERAGE part 1-6) — so a mesh carrying
|
|
2933
|
+
// `source` is a linked mesh whatever its `type` says, and refusing it as "two
|
|
2934
|
+
// keys this compiler does not read: source, skin" sent the author to delete
|
|
2935
|
+
// the one key that made it linked.
|
|
2936
|
+
if (type === 'mesh' && (att as { source?: unknown }).source !== undefined) {
|
|
2937
|
+
throw new NotImplementedError(deferredAttachmentRefusal('linkedmesh', where, ' (a mesh carrying "source" is one)'));
|
|
2938
|
+
}
|
|
2815
2939
|
if (type === 'region') return buildRigRegion(att as RigRegionAttachment, placeholder, where, ctx);
|
|
2816
2940
|
if (type === 'mesh') return buildRigMesh(att as RigMeshAttachment, placeholder, where, ctx);
|
|
2817
2941
|
if (type === 'boundingbox') return buildRigBoundingBox(att as RigBoundingBoxAttachment, where, ctx);
|
|
2818
2942
|
if (type === 'clipping') return buildRigClipping(att as RigClippingAttachment, where, ctx);
|
|
2819
2943
|
if (type === 'path') return buildRigPath(att as RigPathAttachment, where, ctx);
|
|
2820
|
-
throw new NotImplementedError(
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2944
|
+
if (DEFERRED_ATTACHMENTS[type] !== undefined) throw new NotImplementedError(deferredAttachmentRefusal(type, where, ''));
|
|
2945
|
+
const near = nearMisses(type, SPINE_ATTACHMENT_TYPES);
|
|
2946
|
+
throw new CompileError(
|
|
2947
|
+
`${where}: attachment type "${type}" is not one of the ${SPINE_ATTACHMENT_TYPES.length} the Spine 4.3 format ` +
|
|
2948
|
+
`defines (${SPINE_ATTACHMENT_TYPES.join(', ')}). ` +
|
|
2949
|
+
(near.length ? `Did you mean ${near.map((n) => JSON.stringify(n)).join(' or ')}? ` : '') +
|
|
2950
|
+
'The parser matches no case for it, `readAttachment` returns null (`SkeletonJson.ts:653`), and the ' +
|
|
2951
|
+
'attachment is dropped from the skeleton without a word.' +
|
|
2952
|
+
// The one near-miss worth naming outright, because it is a real word in
|
|
2953
|
+
// this format standing one level up from where it was written.
|
|
2954
|
+
(type === 'sequence'
|
|
2955
|
+
? ' "sequence" is a KEY on a region or a mesh rather than a type of its own (docs/SPEC_COVERAGE.md part 1-6).'
|
|
2956
|
+
: ''),
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
/** The refusal for a type the format has and rigc has not built. */
|
|
2961
|
+
function deferredAttachmentRefusal(type: string, where: string, how: string): string {
|
|
2962
|
+
return (
|
|
2963
|
+
`${where}: this attachment is a "${type}"${how} — ${DEFERRED_ATTACHMENTS[type]}. ` +
|
|
2964
|
+
`rigc does not emit it yet, deliberately: it emits ${EMITTED_ATTACHMENT_TYPES.join(', ')}, and neither a ` +
|
|
2965
|
+
'point nor a linked mesh appears anywhere in the benchmark corpus (docs/SPEC_COVERAGE.md parts 3-1 and 4-2), ' +
|
|
2966
|
+
'so neither is on the ladder\'s critical path. docs/SPEC_COVERAGE.md part 1-6 is the row this sentence reads ' +
|
|
2967
|
+
'from, and it is what an implementation would have to carry.'
|
|
2826
2968
|
);
|
|
2827
2969
|
}
|
|
2828
2970
|
|
|
@@ -3191,6 +3333,45 @@ function atlasedImage(image: string, where: string, ctx: AttachmentContext): Com
|
|
|
3191
3333
|
);
|
|
3192
3334
|
}
|
|
3193
3335
|
|
|
3336
|
+
/**
|
|
3337
|
+
* The atlas region this attachment resolves its art from — **one rule for every
|
|
3338
|
+
* attachment kind that has art**, which is region and mesh (the parser's own two
|
|
3339
|
+
* `getValue(map, "path", name)` sites, `:541` and `:570`).
|
|
3340
|
+
*
|
|
3341
|
+
* `path` defaults to the attachment's NAME, not to the placeholder, so a
|
|
3342
|
+
* placeholder called anything other than its PNG's basename resolves a region no
|
|
3343
|
+
* atlas has. Stating it is therefore not decoration: without it the loader
|
|
3344
|
+
* throws `Region not found in atlas`, which `A00_ROUNDTRIP_PARSE` reports in the
|
|
3345
|
+
* parser's own words.
|
|
3346
|
+
*
|
|
3347
|
+
* 🚨 The tree answered this in three different ways until issue #577, and
|
|
3348
|
+
* `docs/AUTHORING.md` §3.4 documented only one of them — *"rigc sets it for you
|
|
3349
|
+
* when the PNG basename differs from the placeholder"*. A region derived it, the
|
|
3350
|
+
* `contour` and `grid` generators derived it (one of them with a comment reading
|
|
3351
|
+
* "Same rule a region attachment follows"), and an authored mesh and the
|
|
3352
|
+
* `ring`/`ribbon` generators wrote `path` only when the spec stated one.
|
|
3353
|
+
* Measured on the probe rig: a region with `image: hair_short.png` under
|
|
3354
|
+
* placeholder `hair` gated green with `"path": "hair_short"`; the same image on
|
|
3355
|
+
* an authored mesh emitted no `path` and failed `A00_ROUNDTRIP_PARSE: threw:
|
|
3356
|
+
* Region not found in atlas: hair (attachment: hair)`. The asymmetry had already
|
|
3357
|
+
* been paid for by hand — `selftest.ts`'s own `TIMELINE_RIG` restates
|
|
3358
|
+
* `path: "block"` and `path: "marker"` on two authored meshes for no other
|
|
3359
|
+
* reason.
|
|
3360
|
+
*
|
|
3361
|
+
* ⭐ Deriving is the reading that was already written down, in the doc and in
|
|
3362
|
+
* two of the five emit sites. The alternative — document the asymmetry and
|
|
3363
|
+
* refuse a mesh whose basename differs without a `path` — was rejected because
|
|
3364
|
+
* it makes a hand-kept exception out of a rule the format applies to both kinds
|
|
3365
|
+
* through one line of parser, and it would have had to contradict §3.4 rather
|
|
3366
|
+
* than satisfy it.
|
|
3367
|
+
*/
|
|
3368
|
+
function attachmentPath(att: { path?: string; image?: string }, placeholder: string): string | undefined {
|
|
3369
|
+
if (att.path !== undefined) return att.path;
|
|
3370
|
+
if (att.image === undefined) return undefined;
|
|
3371
|
+
const region = basename(att.image, '.png');
|
|
3372
|
+
return region === placeholder ? undefined : region;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3194
3375
|
function buildRigRegion(
|
|
3195
3376
|
att: RigRegionAttachment,
|
|
3196
3377
|
placeholder: string,
|
|
@@ -3237,9 +3418,8 @@ function buildRigRegion(
|
|
|
3237
3418
|
);
|
|
3238
3419
|
}
|
|
3239
3420
|
const out: SpineRegionAttachment = { width: r6(width), height: r6(height) };
|
|
3240
|
-
const
|
|
3241
|
-
if (
|
|
3242
|
-
else if (region !== undefined && region !== placeholder) out.path = region;
|
|
3421
|
+
const path = attachmentPath(att, placeholder);
|
|
3422
|
+
if (path !== undefined) out.path = path;
|
|
3243
3423
|
if (att.x !== undefined) out.x = r6(att.x);
|
|
3244
3424
|
if (att.y !== undefined) out.y = r6(att.y);
|
|
3245
3425
|
if (att.rotation !== undefined) out.rotation = r6(att.rotation);
|
|
@@ -3300,6 +3480,16 @@ function encodeNamedWeights(weights: RigMeshBinding[][], where: string, ctx: Att
|
|
|
3300
3480
|
* a part with no art at all (0 of 0 pixels is not a percentage). Neither is an
|
|
3301
3481
|
* error here — a mesh with no image is ordinary data, and an all-transparent part
|
|
3302
3482
|
* is somebody else's assertion to make.
|
|
3483
|
+
*
|
|
3484
|
+
* 🚨 The asymmetry was a promise this function could not keep, and nothing here
|
|
3485
|
+
* said so (issue #570). `partPlate` three frames down called `extractRegion`,
|
|
3486
|
+
* which refused a region a foreign pack had turned — so an authored mesh naming
|
|
3487
|
+
* an `image` under `--atlas-in` ENDED the build, by a refusal raised inside a
|
|
3488
|
+
* measurement, on 36 of 42 atlases of the pack the card was filed from. The
|
|
3489
|
+
* repair was to make the refusal unnecessary rather than to catch it: reading a
|
|
3490
|
+
* turned region is a transcription of `MeshAttachment.computeUVs` and is now
|
|
3491
|
+
* what `extractRegion` does, so there is no unmeasurable case left for this
|
|
3492
|
+
* function to report and no catch here to keep reachable.
|
|
3303
3493
|
*/
|
|
3304
3494
|
function measureAuthoredFit(att: RigMeshAttachment, ctx: AttachmentContext): MeshFitReport | null {
|
|
3305
3495
|
if (att.image === undefined || att.uvs === undefined || att.triangles === undefined) return null;
|
|
@@ -3466,7 +3656,8 @@ function buildRigMesh(
|
|
|
3466
3656
|
width: r6(width),
|
|
3467
3657
|
height: r6(height),
|
|
3468
3658
|
};
|
|
3469
|
-
|
|
3659
|
+
const path = attachmentPath(att, placeholder);
|
|
3660
|
+
if (path !== undefined) out.path = path;
|
|
3470
3661
|
if (att.color !== undefined) out.color = att.color;
|
|
3471
3662
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
3472
3663
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
@@ -3557,7 +3748,8 @@ function buildGeneratedMesh(
|
|
|
3557
3748
|
width: r6(w),
|
|
3558
3749
|
height: r6(h),
|
|
3559
3750
|
};
|
|
3560
|
-
|
|
3751
|
+
const path = attachmentPath(att, placeholder);
|
|
3752
|
+
if (path !== undefined) out.path = path;
|
|
3561
3753
|
if (att.color !== undefined) out.color = att.color;
|
|
3562
3754
|
return out;
|
|
3563
3755
|
}
|
|
@@ -4035,8 +4227,8 @@ function buildGridAttachment(
|
|
|
4035
4227
|
width: r6(w),
|
|
4036
4228
|
height: r6(h),
|
|
4037
4229
|
};
|
|
4038
|
-
|
|
4039
|
-
|
|
4230
|
+
const path = attachmentPath(att, placeholder);
|
|
4231
|
+
if (path !== undefined) out.path = path;
|
|
4040
4232
|
if (att.color !== undefined) out.color = att.color;
|
|
4041
4233
|
return out;
|
|
4042
4234
|
}
|
|
@@ -4174,9 +4366,11 @@ function buildContourAttachment(
|
|
|
4174
4366
|
};
|
|
4175
4367
|
// Same rule a region attachment follows: the atlas region is the PNG's
|
|
4176
4368
|
// basename, so a placeholder named anything else needs `path` written down or
|
|
4177
|
-
// the loader resolves nothing.
|
|
4178
|
-
|
|
4179
|
-
|
|
4369
|
+
// the loader resolves nothing. Stated once in `attachmentPath` since #577 —
|
|
4370
|
+
// this comment used to be the rule's only statement, beside four emit sites
|
|
4371
|
+
// that disagreed with it.
|
|
4372
|
+
const path = attachmentPath(att, placeholder);
|
|
4373
|
+
if (path !== undefined) out.path = path;
|
|
4180
4374
|
if (att.color !== undefined) out.color = att.color;
|
|
4181
4375
|
return out;
|
|
4182
4376
|
}
|
|
@@ -5647,12 +5841,15 @@ function deformGeometryOf(
|
|
|
5647
5841
|
* pair too long, or aimed at the wrong attachment, loses its tail and
|
|
5648
5842
|
* deforms part of the mesh correctly. That is the worst possible failure
|
|
5649
5843
|
* shape: it looks almost right.
|
|
5650
|
-
* 2.
|
|
5651
|
-
*
|
|
5652
|
-
* 3. **`fromVertex` where a vertex is not one pair.** See below.
|
|
5653
|
-
* 4. **A key that carries both a run and no room for one**, or a non-finite
|
|
5844
|
+
* 2. **`fromVertex` where a vertex is not one pair.** See below.
|
|
5845
|
+
* 3. **A key that carries both a run and no room for one**, or a non-finite
|
|
5654
5846
|
* offset — a NaN in the deform array propagates into world vertices.
|
|
5655
5847
|
*
|
|
5848
|
+
* ⛔ What is **not** refused, and was until issue #576: an odd `offset` or an odd
|
|
5849
|
+
* run length. Both are raw copies at raw indices in both readers, both are what a
|
|
5850
|
+
* trimmed editor delta looks like, and neither has a second spelling — see the
|
|
5851
|
+
* clause in the loop below and the one in `deformStart`.
|
|
5852
|
+
*
|
|
5656
5853
|
* `fromVertex` is rigc's own field and the reason it exists is issue #89's
|
|
5657
5854
|
* observation: a deform key is the only key in the format whose meaning depends
|
|
5658
5855
|
* on the attachment it is attached to, and an author reasons in vertices while
|
|
@@ -5811,11 +6008,31 @@ function compileDeformTrack(
|
|
|
5811
6008
|
'or omit it entirely for "back to the setup pose"',
|
|
5812
6009
|
);
|
|
5813
6010
|
}
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
6011
|
+
// ⛔ No parity clause here, and its absence is the rule (issue #576).
|
|
6012
|
+
//
|
|
6013
|
+
// A run is copied, not decoded. `SkeletonJson`'s deform branch does
|
|
6014
|
+
// `Utils.arrayCopy(verticesValue, 0, deform, start, verticesValue.length)`
|
|
6015
|
+
// with `start` the key's own `offset`, and `SkeletonBinary` reads a count and
|
|
6016
|
+
// a start and fills `for (let v = start; v < end; v++) deform[v] = ...`.
|
|
6017
|
+
// Neither has any pair arithmetic to be misaligned against, so an ODD run is
|
|
6018
|
+
// legal, deterministic data: it writes the x and y of one vertex and the x of
|
|
6019
|
+
// the next, and that next y stays at its setup value.
|
|
6020
|
+
//
|
|
6021
|
+
// 🔒 The reason this cannot be "refused in THIS spec anyway" is that there is
|
|
6022
|
+
// no other spelling of it. Padding a `0` to make the run even is a different
|
|
6023
|
+
// animation wherever the setup y it lands on is non-zero, so a spec that
|
|
6024
|
+
// refuses an odd run is a spec no transcription of such a file can be written
|
|
6025
|
+
// in — and one is in this repository's own example corpus (`spineboy-pro`,
|
|
6026
|
+
// `hoverboard` / `hoverboard-board`: `offset: 1` and 147 numbers into a
|
|
6027
|
+
// 148-long array, the whole delta minus the leading zero the editor trimmed).
|
|
6028
|
+
//
|
|
6029
|
+
// What replaces it is the bound the runtime really has, below: the run has to
|
|
6030
|
+
// FIT. That one is the quiet defect — a copy past the end of a `Float32Array`
|
|
6031
|
+
// is a no-op in JavaScript — and it is unaffected by where a run starts or
|
|
6032
|
+
// how long it is. `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` measures the same
|
|
6033
|
+
// bound on the emitted file and has had no parity clause since issue #262;
|
|
6034
|
+
// until this was removed the two halves of rigc disagreed about what the
|
|
6035
|
+
// format holds, and the half that refused was the authoring half.
|
|
5819
6036
|
for (const n of run) {
|
|
5820
6037
|
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
5821
6038
|
throw new CompileError(`${where} (t=${key.t}): "vertices" holds a non-finite value ${JSON.stringify(n)}`);
|
|
@@ -5884,10 +6101,10 @@ function expandDeformToInfluences(displacements: number[], geometry: DeformGeome
|
|
|
5884
6101
|
/**
|
|
5885
6102
|
* Where in the deform array this key's run begins.
|
|
5886
6103
|
*
|
|
5887
|
-
* `offset` is that index outright
|
|
5888
|
-
*
|
|
5889
|
-
*
|
|
5890
|
-
* vertices of a weighted one.
|
|
6104
|
+
* `offset` is that index outright — any index the array holds, odd ones included
|
|
6105
|
+
* (issue #576). `fromVertex` is a vertex index, and turning one into the other is
|
|
6106
|
+
* exact only where a vertex occupies exactly one pair — which is every vertex of
|
|
6107
|
+
* an unweighted attachment and only the single-bone vertices of a weighted one.
|
|
5891
6108
|
*/
|
|
5892
6109
|
function deformStart(
|
|
5893
6110
|
key: MotionDeformTrack['keys'][number],
|
|
@@ -5901,12 +6118,16 @@ function deformStart(
|
|
|
5901
6118
|
`${where} (t=${key.t}): offset is ${JSON.stringify(key.offset)}; it is an index into the deform array, so a whole number ≥ 0`,
|
|
5902
6119
|
);
|
|
5903
6120
|
}
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
6121
|
+
// ⛔ An odd `offset` is not refused either, and it went the same way as the
|
|
6122
|
+
// run's own length (issue #576). The refusal that stood here read an odd
|
|
6123
|
+
// start as a `fromVertex` typed into the wrong field — a real mistake, but
|
|
6124
|
+
// this caught exactly the half of it whose index happens to be odd:
|
|
6125
|
+
// `offset: 4` meant as vertex 4 is the same mistake, lands on vertex 2, and
|
|
6126
|
+
// was always accepted. What it did refuse was every faithful transcription of
|
|
6127
|
+
// a trimmed editor run, one of which ships in `examples/` — `spineboy-pro`'s
|
|
6128
|
+
// `hoverboard-board` starts at 1. A rule that filters one parity of a
|
|
6129
|
+
// confusion it cannot see, at the price of a construct the format holds, is
|
|
6130
|
+
// the wrong instrument; `A35` dropped the same clause in issue #262.
|
|
5910
6131
|
return key.offset;
|
|
5911
6132
|
}
|
|
5912
6133
|
if (key.fromVertex === undefined) return 0;
|
|
@@ -5914,7 +6135,12 @@ function deformStart(
|
|
|
5914
6135
|
if (!Number.isInteger(from) || from < 0) {
|
|
5915
6136
|
throw new CompileError(`${where} (t=${key.t}): fromVertex is ${JSON.stringify(from)}; it is a vertex index, so a whole number ≥ 0`);
|
|
5916
6137
|
}
|
|
5917
|
-
|
|
6138
|
+
// ⌈⌉ rather than ÷, because an odd run REACHES a last vertex without covering
|
|
6139
|
+
// it: its final number is that vertex's x and the y beside it stays at setup
|
|
6140
|
+
// (issue #576). The bound below is about which vertices the run reaches, so the
|
|
6141
|
+
// half-reached one counts — and on a weighted attachment its influence count is
|
|
6142
|
+
// checked with the rest.
|
|
6143
|
+
const covered = Math.ceil(runLength / 2);
|
|
5918
6144
|
if (from + covered > geometry.vertexCount) {
|
|
5919
6145
|
throw new CompileError(
|
|
5920
6146
|
`${where} (t=${key.t}): fromVertex ${from} plus ${covered} vertex offset(s) runs to vertex ${from + covered}, ` +
|