spine-rigc 0.35.1 → 0.36.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/docs/AUTHORING.md +125 -97
- package/docs/FACE.md +3 -1
- package/docs/INGEST.md +10 -13
- package/docs/SPEC_COVERAGE.md +15 -14
- package/package.json +1 -1
- package/src/compile.ts +133 -218
- package/src/diff.ts +59 -0
- package/src/ingest.ts +26 -148
- package/src/rig.ts +48 -8
- package/src/timelines.ts +23 -8
- package/src/types.ts +12 -12
- package/src/validate.ts +9 -3
- package/tools/editor_roundtrip.ts +13 -4
package/src/compile.ts
CHANGED
|
@@ -319,13 +319,14 @@ export function editorNamesInOrder(names: readonly string[], collection: 'animat
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
* The skin the editor keeps at index 0 whatever its name sorts as
|
|
322
|
+
* The skin the editor keeps at index 0 whatever its name sorts as — and the one
|
|
323
|
+
* skin that may not share a placeholder with a named skin
|
|
324
|
+
* (`refuseDefaultSkinContest`).
|
|
323
325
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* and has no business spelling the name a second time (issue #746).
|
|
326
|
+
* Not exported since #796: `ingest` read it to know which contested entries got
|
|
327
|
+
* no composed name, and nothing is composed any more.
|
|
327
328
|
*/
|
|
328
|
-
|
|
329
|
+
const DEFAULT_SKIN = 'default';
|
|
329
330
|
|
|
330
331
|
/**
|
|
331
332
|
* The order the emitted `skins` array is written in: **`default` first, then the
|
|
@@ -2433,12 +2434,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2433
2434
|
// `parseRigSpec` has refused a sequence on any kind but these three and
|
|
2434
2435
|
// proved its shape, so what is left is whether its frames exist. The stem
|
|
2435
2436
|
// is the region `path` the attachment resolves through, which with no
|
|
2436
|
-
// `path` stated is the
|
|
2437
|
-
//
|
|
2437
|
+
// `path` stated is the attachment's NAME — its stated `name`, else the
|
|
2438
|
+
// placeholder (`path = getValue(map, "path", name)`, issue #796).
|
|
2438
2439
|
const sequence = (att as RigRegionAttachment).sequence;
|
|
2439
2440
|
if (sequence !== undefined) {
|
|
2440
2441
|
addSequenceFrames(
|
|
2441
|
-
(att as RigRegionAttachment).path ?? placeholder,
|
|
2442
|
+
(att as RigRegionAttachment).path ?? (att as RigRegionAttachment).name ?? placeholder,
|
|
2442
2443
|
sequence,
|
|
2443
2444
|
`skin "${skinName}" slot "${slotName}" attachment "${placeholder}"`,
|
|
2444
2445
|
);
|
|
@@ -2447,11 +2448,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2447
2448
|
rigAttachmentNames.set(slotName, names);
|
|
2448
2449
|
}
|
|
2449
2450
|
}
|
|
2450
|
-
//
|
|
2451
|
-
//
|
|
2452
|
-
//
|
|
2453
|
-
//
|
|
2454
|
-
|
|
2451
|
+
// The one contested shape the editor cannot hold — a placeholder the default
|
|
2452
|
+
// skin shares with a named skin — refused here, off the normalised skin
|
|
2453
|
+
// table, before anything is built. Every other contested placeholder is
|
|
2454
|
+
// emitted exactly as the spec states it (issue #796).
|
|
2455
|
+
refuseDefaultSkinContests(skinNames, skinParts);
|
|
2455
2456
|
|
|
2456
2457
|
// -- 2. atlas --------------------------------------------------------------
|
|
2457
2458
|
//
|
|
@@ -2677,7 +2678,6 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2677
2678
|
const placeholders = skinParts.get(skinName)!.attachments[rigSlot.name];
|
|
2678
2679
|
if (!placeholders) continue;
|
|
2679
2680
|
const perSlot: Record<string, SpineAttachment> = {};
|
|
2680
|
-
const shared = contested.get(rigSlot.name);
|
|
2681
2681
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
2682
2682
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
2683
2683
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
@@ -2694,15 +2694,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2694
2694
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
2695
2695
|
links: pendingLinks,
|
|
2696
2696
|
});
|
|
2697
|
-
// The name is put on AFTER the builder rather than inside it:
|
|
2698
|
-
// builders write
|
|
2699
|
-
// to be remembered in
|
|
2700
|
-
//
|
|
2701
|
-
|
|
2702
|
-
// reaches here: it is refused above (issue #567), because the editor
|
|
2703
|
-
// holds no such shape in either spelling.
|
|
2704
|
-
const composed = composeSkinAttachmentName(skinName, placeholder, shared?.has(placeholder) === true);
|
|
2705
|
-
perSlot[placeholder] = composed === null ? built : nameSkinAttachment(built, composed, placeholder);
|
|
2697
|
+
// The name is put on AFTER the builder rather than inside it: six
|
|
2698
|
+
// builders write six shapes, the rule is one rule, and a rule that has
|
|
2699
|
+
// to be remembered in six places is a rule that will be kept in five.
|
|
2700
|
+
// It is the spec's own `name`, or nothing (issue #796).
|
|
2701
|
+
perSlot[placeholder] = withStatedName(built, att);
|
|
2706
2702
|
}
|
|
2707
2703
|
tableFor(skinName)[rigSlot.name] = perSlot;
|
|
2708
2704
|
}
|
|
@@ -3497,49 +3493,63 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
|
|
|
3497
3493
|
}
|
|
3498
3494
|
|
|
3499
3495
|
// ---------------------------------------------------------------------------
|
|
3500
|
-
// attachment names,
|
|
3496
|
+
// attachment names, and the one placeholder shape the editor cannot hold
|
|
3501
3497
|
// ---------------------------------------------------------------------------
|
|
3502
3498
|
|
|
3503
3499
|
/**
|
|
3504
|
-
*
|
|
3505
|
-
*
|
|
3506
|
-
*
|
|
3507
|
-
*
|
|
3508
|
-
*
|
|
3509
|
-
* `
|
|
3510
|
-
*
|
|
3511
|
-
*
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3514
|
-
*
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3500
|
+
* An attachment as the builder made it, with the spec's own `name` put on it —
|
|
3501
|
+
* or unchanged, where the spec states none.
|
|
3502
|
+
*
|
|
3503
|
+
* ## rigc derives no name (issue #796)
|
|
3504
|
+
*
|
|
3505
|
+
* `readAttachment` reads `name = getValue(map, "name", placeholder)`
|
|
3506
|
+
* (`SkeletonJson.js:526`), so an entry that states no `name` is named by its
|
|
3507
|
+
* placeholder and one that states a name is named by it: the runtime's
|
|
3508
|
+
* `Attachment.name`, which a consumer reads off `slot.attachment.name`. That is
|
|
3509
|
+
* the whole of what the field does. Nothing in the format resolves by it — a
|
|
3510
|
+
* skin's table, a slot's setup `attachment`, an attachment or deform timeline and
|
|
3511
|
+
* a linked mesh's `source` are keyed by the placeholder (`:415-418`, `:433`,
|
|
3512
|
+
* `:1140`) — so the field is emitted exactly when the spec states it, verbatim,
|
|
3513
|
+
* and never composed.
|
|
3514
|
+
*
|
|
3515
|
+
* 🚨 **It used to be composed, and the composition was measured wrong on
|
|
3516
|
+
* production data.** Issue #541 (landed by #552) wrote `"<skin>/<placeholder>"`
|
|
3517
|
+
* for every placeholder more than one skin fills, on the reading that *"the
|
|
3518
|
+
* editor requires attachment names to be unique — because a linked mesh resolves
|
|
3519
|
+
* its parent by name"*. Both halves fail a measurement:
|
|
3520
|
+
*
|
|
3521
|
+
* - **A linked mesh resolves its source by skin, slot and KEY.** Measured through
|
|
3522
|
+
* spine-core 4.3.13: two skins each fill placeholder `C` with a different mesh
|
|
3523
|
+
* and no `name`, both load named `C`, and a link in the second skin with
|
|
3524
|
+
* `source: "C", skin: <second>` binds the SECOND skin's mesh; the same link
|
|
3525
|
+
* with `skin: <first>` binds the first. A link whose `source` spells its
|
|
3526
|
+
* source's stated name rather than its key throws `Source mesh not found`.
|
|
3527
|
+
* - **The editor imports the uncomposed shape.** A production rig whose
|
|
3528
|
+
* placeholders are each filled by two named skins, meshes and linked meshes
|
|
3529
|
+
* among them, imported through Spine 4.3.26 with every composed name stripped
|
|
3530
|
+
* — exit 0 and a project written (#796). It is the shape that editor exported
|
|
3531
|
+
* in the first place.
|
|
3532
|
+
*
|
|
3533
|
+
* What #541 actually bisected was a rig whose DEFAULT skin filled the contested
|
|
3534
|
+
* placeholder beside the named ones (`default` + one named skin was its smallest
|
|
3535
|
+
* refusing variant), and that shape is refused here on its own measurement —
|
|
3536
|
+
* `refuseDefaultSkinContest` — which never depended on the composition.
|
|
3537
|
+
*
|
|
3538
|
+
* ⚠️ `path` is not touched. It stays the builder's — stated, or derived from
|
|
3539
|
+
* `image` against the name the attachment will carry (`attachmentPath`) — and
|
|
3540
|
+
* its default is the parser's, `path = getValue(map, "path", name)`
|
|
3541
|
+
* (`:529`, `:560`), so a stated name with no `path` resolves the region that
|
|
3542
|
+
* name spells.
|
|
3543
|
+
*
|
|
3544
|
+
* 🔸 `name` is written FIRST: the editor writes it ahead of `type` on every
|
|
3545
|
+
* named attachment of the production set #796 was measured on (all of them
|
|
3546
|
+
* meshes), and it is the key the parser reads first. No region, polygon or link
|
|
3547
|
+
* with a name was in that set, so for those types the position is the same
|
|
3548
|
+
* choice rather than a second measurement.
|
|
3518
3549
|
*/
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
return `${skinName}${SKIN_ATTACHMENT_SEPARATOR}${placeholder}`;
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
/**
|
|
3526
|
-
* The `name` one skin's entry for a placeholder is emitted with, or `null` for
|
|
3527
|
-
* the entries that carry no `name` field at all.
|
|
3528
|
-
*
|
|
3529
|
-
* Stated once, and called by both the emit and `contestedPlaceholders`'
|
|
3530
|
-
* collision walk, because two readings of one rule is how issue #567 happened.
|
|
3531
|
-
* By the time either caller runs, a contested placeholder the **default** skin
|
|
3532
|
-
* fills has already been refused — see `refuseDefaultSkinContest` — so every
|
|
3533
|
-
* entry this composes for is a named skin's.
|
|
3534
|
-
*
|
|
3535
|
-
* 🔒 A third caller reads it from outside: `ingest` compares the name a source
|
|
3536
|
-
* states against the one this returns, and reports the rename where the two
|
|
3537
|
-
* differ (issue #746). It calls this rather than `skinAttachmentName` so that
|
|
3538
|
-
* WHETHER a name is composed is read off the same line as WHAT it is — the
|
|
3539
|
-
* separator and the contest test are each stated once, here.
|
|
3540
|
-
*/
|
|
3541
|
-
export function composeSkinAttachmentName(skinName: string, placeholder: string, contested: boolean): string | null {
|
|
3542
|
-
return contested ? skinAttachmentName(skinName, placeholder) : null;
|
|
3550
|
+
function withStatedName(att: SpineAttachment, stated: RigAttachment): SpineAttachment {
|
|
3551
|
+
const name = (stated as { name?: string }).name;
|
|
3552
|
+
return name === undefined ? att : ({ name, ...att } as SpineAttachment);
|
|
3543
3553
|
}
|
|
3544
3554
|
|
|
3545
3555
|
/**
|
|
@@ -3590,12 +3600,13 @@ export function composeSkinAttachmentName(skinName: string, placeholder: string,
|
|
|
3590
3600
|
* other. There is no third spelling to find, so this is a `CompileError` and not
|
|
3591
3601
|
* a scheme, in the shape issue #543 used: refuse by name and say what to do.
|
|
3592
3602
|
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
*
|
|
3597
|
-
*
|
|
3598
|
-
*
|
|
3603
|
+
* 🔒 **Why it outlived the composition it was written beside (#796).** Both
|
|
3604
|
+
* trips are about the default skin's entry, and neither needs a name to be
|
|
3605
|
+
* composed for anything to fail: trip 8 is the uncomposed spelling, and trip 7
|
|
3606
|
+
* is exactly what a spec stating a `name` on that entry would now emit. So the
|
|
3607
|
+
* refusal stands on its own two measurements, and a stated `name` does not lift
|
|
3608
|
+
* it. What it never covered — two or more NAMED skins filling one placeholder —
|
|
3609
|
+
* is the shape the editor exports itself, and it is emitted as stated.
|
|
3599
3610
|
*/
|
|
3600
3611
|
function refuseDefaultSkinContest(slotName: string, placeholder: string, skins: readonly string[]): never {
|
|
3601
3612
|
const named = skins.filter((skin) => skin !== DEFAULT_SKIN);
|
|
@@ -3603,78 +3614,28 @@ function refuseDefaultSkinContest(slotName: string, placeholder: string, skins:
|
|
|
3603
3614
|
`slot "${slotName}": placeholder "${placeholder}" is filled by the "${DEFAULT_SKIN}" skin AND by ` +
|
|
3604
3615
|
`${named.length === 1 ? 'skin' : 'skins'} ${named.map((skin) => `"${skin}"`).join(', ')}, and the Spine ` +
|
|
3605
3616
|
'editor has no way to hold that. Measured on 4.3.26 in both spellings: give the default skin\'s attachment a ' +
|
|
3606
|
-
|
|
3607
|
-
`
|
|
3608
|
-
'
|
|
3617
|
+
'name of its own and the editor re-keys it by that name on export, so the slot\'s setup attachment ' +
|
|
3618
|
+
`"${placeholder}" resolves in no default-skin key and the default skin draws nothing; leave it as the ` +
|
|
3619
|
+
'placeholder and the import is refused outright with ' +
|
|
3609
3620
|
`"Multiple attachments have the same name: ${placeholder} ${placeholder}", because a default-skin attachment ` +
|
|
3610
3621
|
"hangs on the slot beside the named skins' placeholder of that name. Move the default skin's entry for this " +
|
|
3611
3622
|
`slot into a named skin — call it "base" — so every skin filling "${placeholder}" is a named one. Two or more ` +
|
|
3612
|
-
'named skins sharing a placeholder is the shape the editor
|
|
3613
|
-
'(#541, #552).',
|
|
3623
|
+
'named skins sharing a placeholder is the shape the editor exports itself, and it needs no `name` (#796).',
|
|
3614
3624
|
);
|
|
3615
3625
|
}
|
|
3616
3626
|
|
|
3617
3627
|
/**
|
|
3618
|
-
*
|
|
3619
|
-
*
|
|
3620
|
-
*
|
|
3621
|
-
*
|
|
3622
|
-
*
|
|
3623
|
-
*
|
|
3624
|
-
*
|
|
3625
|
-
*
|
|
3626
|
-
*
|
|
3627
|
-
* editor refuses the whole import, and says exactly why:
|
|
3628
|
-
*
|
|
3629
|
-
* ERROR: Unable to import skeleton.
|
|
3630
|
-
* [error] Error reading skeleton: skins
|
|
3631
|
-
* Cause: [error] Error reading attachment: patch (MOw)
|
|
3632
|
-
* Cause: [error] Multiple attachments have the same name: patch patch
|
|
3633
|
-
*
|
|
3634
|
-
* Bisected on the emitted file: four skins REFUSED, deform timelines removed
|
|
3635
|
-
* REFUSED, `default` + one skin REFUSED, `default` alone IMPORTS, the second skin
|
|
3636
|
-
* given a distinct placeholder IMPORTS, and the same placeholder with **each
|
|
3637
|
-
* entry given its own `name`** IMPORTS — all four skins. So it is neither the
|
|
3638
|
-
* skin count nor the timelines; it is one name over several attachments.
|
|
3639
|
-
*
|
|
3640
|
-
* ## Only the contested pairs are named, and the default skin may not contest
|
|
3641
|
-
*
|
|
3642
|
-
* A placeholder one skin fills keeps the emitted shape it has always had: no
|
|
3643
|
-
* `name`, no `path` it did not already carry. Every rig in this tree declares
|
|
3644
|
-
* exactly one skin, so **no emitted byte in the tree moves** — and a
|
|
3645
|
-
* multi-skin rig whose skins use distinct placeholders does not move either,
|
|
3646
|
-
* because nothing there is ambiguous to begin with.
|
|
3647
|
-
*
|
|
3648
|
-
* ⚠️ A contested placeholder the **default** skin fills is refused before any
|
|
3649
|
-
* of this runs — `refuseDefaultSkinContest`, issue #567 — because two editor
|
|
3650
|
-
* round trips showed the editor holds no such shape in either spelling. So
|
|
3651
|
-
* every entry the walk below composes for belongs to a named skin, and the
|
|
3652
|
-
* emitted name comes off `composeSkinAttachmentName`, which this function calls
|
|
3653
|
-
* rather than restates: the emit and the refusal disagreeing about one name is
|
|
3654
|
-
* the defect both of them exist to prevent.
|
|
3655
|
-
*
|
|
3656
|
-
* ⚠️ The scope of the editor's uniqueness rule is **not** skeleton-wide, and the
|
|
3657
|
-
* corpus proves it rather than a hypothesis doing so: `spineboy-pro.json`, which
|
|
3658
|
-
* the editor wrote, gives the name `head` to a region in slot `head` and to a
|
|
3659
|
-
* bounding box in slot `head-bb`, and names one `hoverglow-small` across eight
|
|
3660
|
-
* slots. What #541 refused was one slot. Composing from the skin makes the names
|
|
3661
|
-
* unique within the slot, which satisfies that scope and every narrower one;
|
|
3662
|
-
* nothing here claims to know which of them the editor actually applies, and an
|
|
3663
|
-
* assertion that policed the emitted artifact would have to.
|
|
3664
|
-
*
|
|
3665
|
-
* ## Why a composed name is not the compiler inventing a value
|
|
3666
|
-
*
|
|
3667
|
-
* rigc has always decided this attachment's name — it decided it was the
|
|
3668
|
-
* placeholder, silently, and that decision is the defect. What changes is the
|
|
3669
|
-
* derivation, not who makes it, and the new one is a function of two names the
|
|
3670
|
-
* spec wrote. Nothing is read off the art, and `path` — the field that says
|
|
3671
|
-
* which texture to draw — stays exactly what the spec stated or what the
|
|
3672
|
-
* attachment already resolved to.
|
|
3628
|
+
* Walk every `(slot, placeholder)` pair more than one skin fills, and refuse the
|
|
3629
|
+
* one shape among them the editor has no representation for: a pair the
|
|
3630
|
+
* **default** skin is one of the fillers of (`refuseDefaultSkinContest`).
|
|
3631
|
+
*
|
|
3632
|
+
* Every other contested pair is legal as it stands and is emitted exactly as
|
|
3633
|
+
* the spec states it — the same placeholder key in each skin, each entry named
|
|
3634
|
+
* by its own `name` or by that key (issue #796). This function decides nothing
|
|
3635
|
+
* about names; until #796 it also composed and collision-checked
|
|
3636
|
+
* `<skin>/<placeholder>`, and both went with the composition.
|
|
3673
3637
|
*/
|
|
3674
|
-
function
|
|
3675
|
-
skinNames: readonly string[],
|
|
3676
|
-
skinParts: Map<string, RigSkinParts>,
|
|
3677
|
-
): Map<string, Set<string>> {
|
|
3638
|
+
function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<string, RigSkinParts>): void {
|
|
3678
3639
|
/** slot -> placeholder -> the skins that fill it, in declaration order. */
|
|
3679
3640
|
const fillers = new Map<string, Map<string, string[]>>();
|
|
3680
3641
|
for (const skinName of skinNames) {
|
|
@@ -3686,82 +3647,11 @@ function contestedPlaceholders(
|
|
|
3686
3647
|
fillers.set(slotName, perSlot);
|
|
3687
3648
|
}
|
|
3688
3649
|
}
|
|
3689
|
-
const contested = new Map<string, Set<string>>();
|
|
3690
|
-
const collisions: string[] = [];
|
|
3691
3650
|
for (const [slotName, perSlot] of fillers) {
|
|
3692
|
-
const shared = new Set([...perSlot].filter(([, skins]) => skins.length > 1).map(([placeholder]) => placeholder));
|
|
3693
|
-
// 🚨 Before anything is composed: a contested placeholder the DEFAULT skin
|
|
3694
|
-
// fills has no representation in the editor at all, in either spelling
|
|
3695
|
-
// (issue #567, round trips 7 and 8). It is refused here rather than emitted,
|
|
3696
|
-
// and the refusal comes first because renaming cannot repair it — the
|
|
3697
|
-
// remedy is a different rig, not a different string.
|
|
3698
|
-
for (const placeholder of shared) {
|
|
3699
|
-
const skins = perSlot.get(placeholder)!;
|
|
3700
|
-
if (skins.includes(DEFAULT_SKIN)) refuseDefaultSkinContest(slotName, placeholder, skins);
|
|
3701
|
-
}
|
|
3702
|
-
if (shared.size) contested.set(slotName, shared);
|
|
3703
|
-
/** Emitted attachment name -> the first entry that claimed it. */
|
|
3704
|
-
const claimed = new Map<string, string>();
|
|
3705
3651
|
for (const [placeholder, skins] of perSlot) {
|
|
3706
|
-
|
|
3707
|
-
// The emitted name, read off the one function that decides it — so the
|
|
3708
|
-
// refusal and the emit cannot drift into two readings. An UNCONTESTED
|
|
3709
|
-
// entry is claimed under its bare placeholder, the default skin's
|
|
3710
|
-
// included: a named skin whose composed name equals it is a collision,
|
|
3711
|
-
// and one this walk sees for the same reason it sees every other.
|
|
3712
|
-
const name = composeSkinAttachmentName(skinName, placeholder, shared.has(placeholder)) ?? placeholder;
|
|
3713
|
-
const site = `skin "${skinName}" placeholder "${placeholder}"`;
|
|
3714
|
-
const taken = claimed.get(name);
|
|
3715
|
-
if (taken === undefined) claimed.set(name, site);
|
|
3716
|
-
else collisions.push(`slot "${slotName}": ${taken} and ${site} would both be named "${name}"`);
|
|
3717
|
-
}
|
|
3652
|
+
if (skins.length > 1 && skins.includes(DEFAULT_SKIN)) refuseDefaultSkinContest(slotName, placeholder, skins);
|
|
3718
3653
|
}
|
|
3719
3654
|
}
|
|
3720
|
-
if (collisions.length) {
|
|
3721
|
-
throw new CompileError(
|
|
3722
|
-
`${collisions.length} attachment name collision(s): a placeholder that more than one skin fills is emitted ` +
|
|
3723
|
-
`with the name "<skin>${SKIN_ATTACHMENT_SEPARATOR}<placeholder>", because the Spine editor refuses an import ` +
|
|
3724
|
-
'in which one slot holds two attachments of one name (#541) — and here that composed name is one another ' +
|
|
3725
|
-
'entry in the same slot already answers to. Rename the placeholder or the skin so the two differ. ' +
|
|
3726
|
-
`${collisions.join('. ')}`,
|
|
3727
|
-
);
|
|
3728
|
-
}
|
|
3729
|
-
return contested;
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
/**
|
|
3733
|
-
* Give one attachment its own `name`, and pin the texture `path` that name would
|
|
3734
|
-
* otherwise have taken with it.
|
|
3735
|
-
*
|
|
3736
|
-
* 🚨 The second half is the whole hazard. `readAttachment` reads
|
|
3737
|
-
* `const name = getValue(map, "name", placeholder)` and then
|
|
3738
|
-
* `const path = getValue(map, "path", name)` (`SkeletonJson.ts:526-529`, and
|
|
3739
|
-
* again at `:559` for a mesh) — so `path` defaults to the NAME, not to the
|
|
3740
|
-
* placeholder. Writing a name and leaving `path` alone silently repoints the
|
|
3741
|
-
* attachment's texture lookup at a region no atlas has. Restating `path` at what
|
|
3742
|
-
* the attachment already resolved to makes the name change invisible to
|
|
3743
|
-
* everything but the editor's own uniqueness rule, which is the only thing it is
|
|
3744
|
-
* for.
|
|
3745
|
-
*
|
|
3746
|
-
* ⚠️ `region` and `mesh` are exactly the two types that read `path`; the polygon
|
|
3747
|
-
* types (`boundingbox`, `clipping`, `path`) have no texture and get the name
|
|
3748
|
-
* alone. The list is the parser's own two `getValue(map, "path", …)` sites
|
|
3749
|
-
* rather than a judgement about which attachments "have art".
|
|
3750
|
-
*/
|
|
3751
|
-
function nameSkinAttachment(att: SpineAttachment, name: string, placeholder: string): SpineAttachment {
|
|
3752
|
-
const kind = (att as { type?: string }).type ?? 'region';
|
|
3753
|
-
// Key order is the parser's reading order — `name`, then `path`, then the rest
|
|
3754
|
-
// as the builder wrote it — for the same reason every other emitted object
|
|
3755
|
-
// follows it: the file is read by people and diffed against references.
|
|
3756
|
-
//
|
|
3757
|
-
// ⚠️ The three kinds here are the three the parser gives a texture `path` to,
|
|
3758
|
-
// and `linkedmesh` is one of them (`SkeletonJson.ts:541`, `:570` — the mesh
|
|
3759
|
-
// branch is shared). Leaving it out would write a `name` and no `path`, and
|
|
3760
|
-
// `path` defaults to `name`, so a contested link would resolve the region
|
|
3761
|
-
// "<skin>/<placeholder>", which no atlas holds.
|
|
3762
|
-
if (kind !== 'region' && kind !== 'mesh' && kind !== 'linkedmesh') return { name, ...att };
|
|
3763
|
-
const { path, ...rest } = att as SpineRegionAttachment | SpineMeshAttachment | SpineLinkedMeshAttachment;
|
|
3764
|
-
return { name, path: path ?? placeholder, ...rest } as SpineAttachment;
|
|
3765
3655
|
}
|
|
3766
3656
|
|
|
3767
3657
|
// ---------------------------------------------------------------------------
|
|
@@ -3898,6 +3788,20 @@ function buildRigAttachment(
|
|
|
3898
3788
|
);
|
|
3899
3789
|
}
|
|
3900
3790
|
const type = stated ?? 'region';
|
|
3791
|
+
// `name` is the runtime's `Attachment.name` and, on a type that draws, what
|
|
3792
|
+
// `path` defaults to — so it is written verbatim and has to be a string the
|
|
3793
|
+
// parser can hold as one (issue #796). An empty string is a string: it names
|
|
3794
|
+
// the attachment "" and, with no `path`, asks the atlas for region "", which
|
|
3795
|
+
// `A08` names by region like any other miss.
|
|
3796
|
+
const statedName = (att as { name?: unknown }).name;
|
|
3797
|
+
if (statedName !== undefined && typeof statedName !== 'string') {
|
|
3798
|
+
throw new CompileError(
|
|
3799
|
+
`${where}: "name" is ${JSON.stringify(statedName) ?? String(statedName)}, which is not a string. An ` +
|
|
3800
|
+
"attachment's name is the runtime's `Attachment.name` — `getValue(map, \"name\", placeholder)`, " +
|
|
3801
|
+
'`SkeletonJson.js:526` — and on a region, mesh or linked mesh it is also what `path` defaults to. Write it ' +
|
|
3802
|
+
'as a string, or leave the key out and the attachment is named by its placeholder.',
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3901
3805
|
// A linked mesh in the format's OTHER spelling. `type: "mesh"` and `type:
|
|
3902
3806
|
// "linkedmesh"` share one branch and the `source` key is what decides between
|
|
3903
3807
|
// them (`:568-569`, `:582`; SPEC_COVERAGE part 1-6) — so a mesh carrying
|
|
@@ -4338,11 +4242,15 @@ function atlasedImage(image: string, where: string, ctx: AttachmentContext): Com
|
|
|
4338
4242
|
* through one line of parser, and it would have had to contradict §3.4 rather
|
|
4339
4243
|
* than satisfy it.
|
|
4340
4244
|
*/
|
|
4341
|
-
function attachmentPath(att: { path?: string; image?: string }, placeholder: string): string | undefined {
|
|
4245
|
+
function attachmentPath(att: { name?: string; path?: string; image?: string }, placeholder: string): string | undefined {
|
|
4342
4246
|
if (att.path !== undefined) return att.path;
|
|
4343
4247
|
if (att.image === undefined) return undefined;
|
|
4344
4248
|
const region = basename(att.image, '.png');
|
|
4345
|
-
|
|
4249
|
+
// Against the NAME the attachment will carry, because that is what `path`
|
|
4250
|
+
// defaults to — the stated `name`, else the placeholder (issue #796). Read
|
|
4251
|
+
// against the placeholder alone, an image named after a stated name would
|
|
4252
|
+
// restate it as `path`, a field the source never wrote.
|
|
4253
|
+
return region === (att.name ?? placeholder) ? undefined : region;
|
|
4346
4254
|
}
|
|
4347
4255
|
|
|
4348
4256
|
/**
|
|
@@ -4364,7 +4272,7 @@ function attachmentPath(att: { path?: string; image?: string }, placeholder: str
|
|
|
4364
4272
|
* keys where `buildRigLinkedMesh` does — because the analogy to a mesh is
|
|
4365
4273
|
* obvious and it is not a measurement.
|
|
4366
4274
|
*/
|
|
4367
|
-
function meshTextureKeys(att: { path?: string; image?: string; color?: string }, placeholder: string): Pick<SpineMeshAttachment, 'path' | 'color'> {
|
|
4275
|
+
function meshTextureKeys(att: { name?: string; path?: string; image?: string; color?: string }, placeholder: string): Pick<SpineMeshAttachment, 'path' | 'color'> {
|
|
4368
4276
|
const out: Pick<SpineMeshAttachment, 'path' | 'color'> = {};
|
|
4369
4277
|
const path = attachmentPath(att, placeholder);
|
|
4370
4278
|
if (path !== undefined) out.path = path;
|
|
@@ -4408,8 +4316,22 @@ function emitSequence(seq: RigSequence): SpineSequence {
|
|
|
4408
4316
|
* measures the same: that is the one number the frames state. Frames of
|
|
4409
4317
|
* different sizes state several, and picking one — the first, the setup frame,
|
|
4410
4318
|
* the largest — would be the compiler choosing a value the spec did not.
|
|
4411
|
-
*
|
|
4412
|
-
*
|
|
4319
|
+
*
|
|
4320
|
+
* ⭐ A STATED size is emitted as stated and compared with no frame, on either
|
|
4321
|
+
* route. It is the quad, and the frames are what is drawn into it:
|
|
4322
|
+
* `computeUVs` scales each region by `width / region.originalWidth`, so a frame
|
|
4323
|
+
* of another size is drawn at the attachment's size, filling it (issue #795,
|
|
4324
|
+
* measured on spine-core 4.3.13: frames of 40, 60 and 80 under a stated 40 give
|
|
4325
|
+
* every frame the same ±20 quad, each with its own region's UVs). That is the
|
|
4326
|
+
* shape an editor exports whenever a series mixes image sizes — it writes the
|
|
4327
|
+
* setup frame's size and leaves the others as they are.
|
|
4328
|
+
*
|
|
4329
|
+
* ⚠️ The single-region rule in `buildRigRegion` does not transfer, though it
|
|
4330
|
+
* reads the same. There the attachment has one region, the editor always writes
|
|
4331
|
+
* that region's own size, and a spec stating another is contradicting the pack
|
|
4332
|
+
* it is resolved against. Here the attachment has `count` regions and one size,
|
|
4333
|
+
* so every frame but the setup frame differing from it is the ordinary case, and
|
|
4334
|
+
* refusing it refused correct editor exports.
|
|
4413
4335
|
*/
|
|
4414
4336
|
function sequenceFrameSize(
|
|
4415
4337
|
frames: readonly CompiledImage[],
|
|
@@ -4420,14 +4342,6 @@ function sequenceFrameSize(
|
|
|
4420
4342
|
for (const field of ['width', 'height'] as const) {
|
|
4421
4343
|
const sizes = [...new Set(frames.map((img) => img[field]))];
|
|
4422
4344
|
if (stated[field] !== undefined) {
|
|
4423
|
-
const packed = frames.find((img) => img.atlas !== undefined && img[field] !== stated[field]);
|
|
4424
|
-
if (packed !== undefined) {
|
|
4425
|
-
throw new CompileError(
|
|
4426
|
-
`${where}: the spec says ${field} ${stated[field]} and sequence frame "${packed.region}" of the imported ` +
|
|
4427
|
-
`atlas is ${packed[field]}; a packed frame's rectangle is fixed, so the two would produce a quad the ` +
|
|
4428
|
-
'pack cannot fill',
|
|
4429
|
-
);
|
|
4430
|
-
}
|
|
4431
4345
|
out[field] = stated[field];
|
|
4432
4346
|
} else if (sizes.length === 1) {
|
|
4433
4347
|
out[field] = sizes[0];
|
|
@@ -4445,7 +4359,8 @@ function sequenceFrameSize(
|
|
|
4445
4359
|
/** Every frame of `att`'s sequence, already atlased by the gather pass. */
|
|
4446
4360
|
function sequenceFrames(att: { path?: string; sequence?: RigSequence }, placeholder: string, where: string, ctx: AttachmentContext): CompiledImage[] {
|
|
4447
4361
|
const seq = att.sequence!;
|
|
4448
|
-
|
|
4362
|
+
// `path`, else what `path` defaults to: the stated `name`, else the placeholder (issue #796).
|
|
4363
|
+
const stem = att.path ?? (att as { name?: string }).name ?? placeholder;
|
|
4449
4364
|
const frames: CompiledImage[] = [];
|
|
4450
4365
|
for (let i = 0; i < seq.count; i++) {
|
|
4451
4366
|
// By the frame's FULL region name — `atlasedImage` takes a basename, and a
|
package/src/diff.ts
CHANGED
|
@@ -683,6 +683,12 @@ interface AttachmentFact {
|
|
|
683
683
|
edgesPresent: boolean | null;
|
|
684
684
|
/** `<width>x<height>` as stated, or `unstated` — see `attachments.region_size`. */
|
|
685
685
|
size: string;
|
|
686
|
+
/**
|
|
687
|
+
* The name the runtime gives the attachment: its stated `name`, else its
|
|
688
|
+
* placeholder key (`getValue(map, "name", placeholder)`, `SkeletonJson.js:526`)
|
|
689
|
+
* — see `attachments.runtime_name`.
|
|
690
|
+
*/
|
|
691
|
+
runtimeName: string;
|
|
686
692
|
}
|
|
687
693
|
|
|
688
694
|
function attachmentFacts(root: Json): { skins: Set<string>; byKey: Map<string, AttachmentFact> } {
|
|
@@ -718,6 +724,7 @@ function attachmentFacts(root: Json): { skins: Set<string>; byKey: Map<string, A
|
|
|
718
724
|
// "has edges" means.
|
|
719
725
|
edgesPresent: type === 'mesh' ? 'edges' in att : null,
|
|
720
726
|
size: num(att.width) !== null && num(att.height) !== null ? `${num(att.width)}x${num(att.height)}` : 'unstated',
|
|
727
|
+
runtimeName: str(att.name) ?? attName,
|
|
721
728
|
});
|
|
722
729
|
}
|
|
723
730
|
}
|
|
@@ -820,9 +827,61 @@ function diffAttachments(c: Json, r: Json): DiffSection {
|
|
|
820
827
|
bm,
|
|
821
828
|
(x, y) => x.edgesPresent === y.edgesPresent,
|
|
822
829
|
),
|
|
830
|
+
// ── issue #796 ───────────────────────────────────────────────────────────
|
|
831
|
+
//
|
|
832
|
+
// The name the RUNTIME gives each attachment — `name` if stated, else the
|
|
833
|
+
// placeholder key — agreed per skin/slot/placeholder. Every measure above is
|
|
834
|
+
// keyed by the placeholder, so a rebuild that renamed every attachment while
|
|
835
|
+
// keeping every key read 1.000 across this whole section: that is what a
|
|
836
|
+
// rebuild did on two production rigs (a stated name respelled as `path`, and
|
|
837
|
+
// `<skin>/<placeholder>` composed over a name the file never stated), and it
|
|
838
|
+
// took a pose oracle comparing `slot.attachment.name` to see it.
|
|
839
|
+
//
|
|
840
|
+
// 🚫 Reported rather than in the mean, by the same test as `mesh_edges`: a
|
|
841
|
+
// name draws no pixel, so no reading of the frames could decide it. It is
|
|
842
|
+
// still a value a consumer reads — `slot.attachment.name` — which is why it
|
|
843
|
+
// is measured at all.
|
|
844
|
+
//
|
|
845
|
+
// ⚠️ Over the keys BOTH sides hold, not over the larger roster, and that is
|
|
846
|
+
// the one departure from `agreement` here. A key one side lacks is already
|
|
847
|
+
// `attachments.names` and `attachments.count`; scored again here it would
|
|
848
|
+
// move this measure on every rename of a key and every dropped attachment,
|
|
849
|
+
// and "the same key answers to another name" — the only thing nothing else
|
|
850
|
+
// reads — would be one term among many. The denominator this reports is the
|
|
851
|
+
// number of keys compared, so a report over few shared keys says so.
|
|
852
|
+
runtimeNames(a.byKey, b.byKey),
|
|
823
853
|
]);
|
|
824
854
|
}
|
|
825
855
|
|
|
856
|
+
/**
|
|
857
|
+
* `attachments.runtime_name`: of the skin/slot/placeholder keys both sides
|
|
858
|
+
* hold, how many answer to the same runtime name — see the note at its call.
|
|
859
|
+
*/
|
|
860
|
+
function runtimeNames(a: Map<string, AttachmentFact>, b: Map<string, AttachmentFact>): DiffMeasure {
|
|
861
|
+
let shared = 0;
|
|
862
|
+
let agree = 0;
|
|
863
|
+
const differ: string[] = [];
|
|
864
|
+
for (const [key, fact] of a) {
|
|
865
|
+
const other = b.get(key);
|
|
866
|
+
if (other === undefined) continue;
|
|
867
|
+
shared++;
|
|
868
|
+
if (fact.runtimeName === other.runtimeName) agree++;
|
|
869
|
+
else if (differ.length < 3) differ.push(`${key} "${fact.runtimeName}" vs "${other.runtimeName}"`);
|
|
870
|
+
}
|
|
871
|
+
const missed = shared - agree;
|
|
872
|
+
return measure(
|
|
873
|
+
'attachments.runtime_name',
|
|
874
|
+
'each attachment both sides hold answers to the same name at runtime (`name` if stated, else its placeholder)',
|
|
875
|
+
agree,
|
|
876
|
+
shared,
|
|
877
|
+
shared === 0
|
|
878
|
+
? 'no skin/slot/placeholder key is on both sides, so no name was compared'
|
|
879
|
+
: missed === 0
|
|
880
|
+
? undefined
|
|
881
|
+
: `${missed} renamed: ${differ.join('; ')}${missed > differ.length ? `; …and ${missed - differ.length} more` : ''}`,
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
826
885
|
interface ConstraintFact {
|
|
827
886
|
type: string;
|
|
828
887
|
/** Every bone or slot the constraint names, sorted — its wiring. */
|