spine-rigc 0.35.0 → 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 +161 -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 +224 -236
- 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 +14 -13
- 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
|
|
@@ -400,7 +401,11 @@ function editorSkinOrder<T extends { name: string }>(skins: readonly T[]): T[] {
|
|
|
400
401
|
* partly sorted:
|
|
401
402
|
*
|
|
402
403
|
* - a pair the comparator leaves open (`editorNameOrder`'s number, separator and
|
|
403
|
-
* folder cases)
|
|
404
|
+
* folder cases). Since #791 a name carrying U+3000 or a full-width digit is
|
|
405
|
+
* not one: the comparator folds both first (`EDITOR_NAME_FOLD`), which is
|
|
406
|
+
* what a production map of 95 slot keys was measured to need, so such a map
|
|
407
|
+
* is sorted where it used to be kept whole — or, with only full-width digits
|
|
408
|
+
* in it, sorted with the digits at their code points;
|
|
404
409
|
* - any slot name holding `/`. The folder rule was measured on skin and
|
|
405
410
|
* animation names, which the editor files in folders; slots it files under
|
|
406
411
|
* bones, and no round trip carried a slot name with a `/` in it.
|
|
@@ -465,10 +470,59 @@ interface ReadName {
|
|
|
465
470
|
skipped: number;
|
|
466
471
|
}
|
|
467
472
|
|
|
473
|
+
/**
|
|
474
|
+
* The characters the editor folds before it compares two names, as
|
|
475
|
+
* `[first, last, to]` code point ranges: each of `first`…`last` is read as
|
|
476
|
+
* `to` plus its offset from `first` (issue #791).
|
|
477
|
+
*
|
|
478
|
+
* 🔬 **Measured on a production set, not on the probes or `examples/`.** One
|
|
479
|
+
* skin's `attachments` map of 95 slot keys, 7 of them carrying U+3000
|
|
480
|
+
* IDEOGRAPHIC SPACE or a full-width digit (U+FF12, U+FF14), came back in an
|
|
481
|
+
* order this comparator reproduces **95 of 95** once U+3000 is read as U+0020
|
|
482
|
+
* and U+FF10–U+FF19 as `0`–`9` — an order neither a plain codepoint sort nor a
|
|
483
|
+
* case-insensitive sort without the fold reproduces. No name in the five probes, the
|
|
484
|
+
* twelve exports or any spec in this tree carries either character, so the set
|
|
485
|
+
* is the only measurement of them there is.
|
|
486
|
+
*
|
|
487
|
+
* ⚠️ **Exactly those two classes, and not `normalize('NFKC')`, although NFKC
|
|
488
|
+
* folds both the same way.** Three readings reproduce the measurement and
|
|
489
|
+
* disagree past it: NFKC (which also turns `fi` into `fi`, `²` and `①` into
|
|
490
|
+
* digits, U+00A0 and U+2000–U+200A into a space, and composes `e` + U+0301
|
|
491
|
+
* into `é`); a classifier that reads every Unicode decimal digit as a digit and
|
|
492
|
+
* every space separator as a space (which also reads `٣` as 3, and does none
|
|
493
|
+
* of NFKC's other folds); and a width fold alone (which also reads `A` as `A`,
|
|
494
|
+
* and folds no other space). What all three agree on is exactly U+3000 and the
|
|
495
|
+
* ten full-width digits — the ten are one class under every reading, so the
|
|
496
|
+
* eight the set did not carry are folded with the two it did — and that
|
|
497
|
+
* intersection is what is applied. Every other character is compared at its own
|
|
498
|
+
* code point as before, and a whitespace character that is not a space is still
|
|
499
|
+
* `leafOrder`'s `separator`.
|
|
500
|
+
*
|
|
501
|
+
* One comparator (#728), so the fold reaches `skins` and `animations` too: an
|
|
502
|
+
* animation name with a full-width digit was compared at the digit's code point,
|
|
503
|
+
* the order the only measurement of such a name contradicts, and a pair that
|
|
504
|
+
* U+3000 decided was refused as `separator` and is now ordered.
|
|
505
|
+
*/
|
|
506
|
+
export const EDITOR_NAME_FOLD: ReadonlyArray<readonly [number, number, number]> = [
|
|
507
|
+
[0x3000, 0x3000, 0x0020],
|
|
508
|
+
[0xff10, 0xff19, 0x0030],
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
/** `name` with every character `EDITOR_NAME_FOLD` covers read as the character it folds to. */
|
|
512
|
+
function foldBeforeComparing(name: string): string {
|
|
513
|
+
let out = '';
|
|
514
|
+
for (const char of name) {
|
|
515
|
+
const code = char.codePointAt(0) ?? 0;
|
|
516
|
+
const range = EDITOR_NAME_FOLD.find(([first, last]) => code >= first && code <= last);
|
|
517
|
+
out += range === undefined ? char : String.fromCodePoint(range[2] + code - range[0]);
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
|
|
468
522
|
function readName(name: string, skip: RegExp): ReadName {
|
|
469
523
|
const chars: string[] = [];
|
|
470
524
|
let skipped = 0;
|
|
471
|
-
for (const char of foldUp(name)) {
|
|
525
|
+
for (const char of foldUp(foldBeforeComparing(name))) {
|
|
472
526
|
if (skip.test(char)) skipped++;
|
|
473
527
|
else chars.push(char);
|
|
474
528
|
}
|
|
@@ -2380,12 +2434,12 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2380
2434
|
// `parseRigSpec` has refused a sequence on any kind but these three and
|
|
2381
2435
|
// proved its shape, so what is left is whether its frames exist. The stem
|
|
2382
2436
|
// is the region `path` the attachment resolves through, which with no
|
|
2383
|
-
// `path` stated is the
|
|
2384
|
-
//
|
|
2437
|
+
// `path` stated is the attachment's NAME — its stated `name`, else the
|
|
2438
|
+
// placeholder (`path = getValue(map, "path", name)`, issue #796).
|
|
2385
2439
|
const sequence = (att as RigRegionAttachment).sequence;
|
|
2386
2440
|
if (sequence !== undefined) {
|
|
2387
2441
|
addSequenceFrames(
|
|
2388
|
-
(att as RigRegionAttachment).path ?? placeholder,
|
|
2442
|
+
(att as RigRegionAttachment).path ?? (att as RigRegionAttachment).name ?? placeholder,
|
|
2389
2443
|
sequence,
|
|
2390
2444
|
`skin "${skinName}" slot "${slotName}" attachment "${placeholder}"`,
|
|
2391
2445
|
);
|
|
@@ -2394,11 +2448,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2394
2448
|
rigAttachmentNames.set(slotName, names);
|
|
2395
2449
|
}
|
|
2396
2450
|
}
|
|
2397
|
-
//
|
|
2398
|
-
//
|
|
2399
|
-
//
|
|
2400
|
-
//
|
|
2401
|
-
|
|
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);
|
|
2402
2456
|
|
|
2403
2457
|
// -- 2. atlas --------------------------------------------------------------
|
|
2404
2458
|
//
|
|
@@ -2624,7 +2678,6 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2624
2678
|
const placeholders = skinParts.get(skinName)!.attachments[rigSlot.name];
|
|
2625
2679
|
if (!placeholders) continue;
|
|
2626
2680
|
const perSlot: Record<string, SpineAttachment> = {};
|
|
2627
|
-
const shared = contested.get(rigSlot.name);
|
|
2628
2681
|
for (const [placeholder, att] of Object.entries(placeholders)) {
|
|
2629
2682
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
2630
2683
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
@@ -2641,15 +2694,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
2641
2694
|
slotNames: new Set(rig.slots.map((s) => s.name)),
|
|
2642
2695
|
links: pendingLinks,
|
|
2643
2696
|
});
|
|
2644
|
-
// The name is put on AFTER the builder rather than inside it:
|
|
2645
|
-
// builders write
|
|
2646
|
-
// to be remembered in
|
|
2647
|
-
//
|
|
2648
|
-
|
|
2649
|
-
// reaches here: it is refused above (issue #567), because the editor
|
|
2650
|
-
// holds no such shape in either spelling.
|
|
2651
|
-
const composed = composeSkinAttachmentName(skinName, placeholder, shared?.has(placeholder) === true);
|
|
2652
|
-
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);
|
|
2653
2702
|
}
|
|
2654
2703
|
tableFor(skinName)[rigSlot.name] = perSlot;
|
|
2655
2704
|
}
|
|
@@ -3444,49 +3493,63 @@ function rotationOf(spec: RigBone, ctx: BoneContext): number | null {
|
|
|
3444
3493
|
}
|
|
3445
3494
|
|
|
3446
3495
|
// ---------------------------------------------------------------------------
|
|
3447
|
-
// attachment names,
|
|
3496
|
+
// attachment names, and the one placeholder shape the editor cannot hold
|
|
3448
3497
|
// ---------------------------------------------------------------------------
|
|
3449
3498
|
|
|
3450
3499
|
/**
|
|
3451
|
-
*
|
|
3452
|
-
*
|
|
3453
|
-
*
|
|
3454
|
-
*
|
|
3455
|
-
*
|
|
3456
|
-
* `
|
|
3457
|
-
*
|
|
3458
|
-
*
|
|
3459
|
-
*
|
|
3460
|
-
*
|
|
3461
|
-
*
|
|
3462
|
-
*
|
|
3463
|
-
*
|
|
3464
|
-
*
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
*
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
3476
|
-
*
|
|
3477
|
-
*
|
|
3478
|
-
*
|
|
3479
|
-
*
|
|
3480
|
-
*
|
|
3481
|
-
*
|
|
3482
|
-
*
|
|
3483
|
-
*
|
|
3484
|
-
*
|
|
3485
|
-
*
|
|
3486
|
-
*
|
|
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.
|
|
3487
3549
|
*/
|
|
3488
|
-
|
|
3489
|
-
|
|
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);
|
|
3490
3553
|
}
|
|
3491
3554
|
|
|
3492
3555
|
/**
|
|
@@ -3537,12 +3600,13 @@ export function composeSkinAttachmentName(skinName: string, placeholder: string,
|
|
|
3537
3600
|
* other. There is no third spelling to find, so this is a `CompileError` and not
|
|
3538
3601
|
* a scheme, in the shape issue #543 used: refuse by name and say what to do.
|
|
3539
3602
|
*
|
|
3540
|
-
*
|
|
3541
|
-
*
|
|
3542
|
-
*
|
|
3543
|
-
*
|
|
3544
|
-
*
|
|
3545
|
-
*
|
|
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.
|
|
3546
3610
|
*/
|
|
3547
3611
|
function refuseDefaultSkinContest(slotName: string, placeholder: string, skins: readonly string[]): never {
|
|
3548
3612
|
const named = skins.filter((skin) => skin !== DEFAULT_SKIN);
|
|
@@ -3550,78 +3614,28 @@ function refuseDefaultSkinContest(slotName: string, placeholder: string, skins:
|
|
|
3550
3614
|
`slot "${slotName}": placeholder "${placeholder}" is filled by the "${DEFAULT_SKIN}" skin AND by ` +
|
|
3551
3615
|
`${named.length === 1 ? 'skin' : 'skins'} ${named.map((skin) => `"${skin}"`).join(', ')}, and the Spine ` +
|
|
3552
3616
|
'editor has no way to hold that. Measured on 4.3.26 in both spellings: give the default skin\'s attachment a ' +
|
|
3553
|
-
|
|
3554
|
-
`
|
|
3555
|
-
'
|
|
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 ' +
|
|
3556
3620
|
`"Multiple attachments have the same name: ${placeholder} ${placeholder}", because a default-skin attachment ` +
|
|
3557
3621
|
"hangs on the slot beside the named skins' placeholder of that name. Move the default skin's entry for this " +
|
|
3558
3622
|
`slot into a named skin — call it "base" — so every skin filling "${placeholder}" is a named one. Two or more ` +
|
|
3559
|
-
'named skins sharing a placeholder is the shape the editor
|
|
3560
|
-
'(#541, #552).',
|
|
3623
|
+
'named skins sharing a placeholder is the shape the editor exports itself, and it needs no `name` (#796).',
|
|
3561
3624
|
);
|
|
3562
3625
|
}
|
|
3563
3626
|
|
|
3564
3627
|
/**
|
|
3565
|
-
*
|
|
3566
|
-
*
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
*
|
|
3572
|
-
*
|
|
3573
|
-
*
|
|
3574
|
-
* editor refuses the whole import, and says exactly why:
|
|
3575
|
-
*
|
|
3576
|
-
* ERROR: Unable to import skeleton.
|
|
3577
|
-
* [error] Error reading skeleton: skins
|
|
3578
|
-
* Cause: [error] Error reading attachment: patch (MOw)
|
|
3579
|
-
* Cause: [error] Multiple attachments have the same name: patch patch
|
|
3580
|
-
*
|
|
3581
|
-
* Bisected on the emitted file: four skins REFUSED, deform timelines removed
|
|
3582
|
-
* REFUSED, `default` + one skin REFUSED, `default` alone IMPORTS, the second skin
|
|
3583
|
-
* given a distinct placeholder IMPORTS, and the same placeholder with **each
|
|
3584
|
-
* entry given its own `name`** IMPORTS — all four skins. So it is neither the
|
|
3585
|
-
* skin count nor the timelines; it is one name over several attachments.
|
|
3586
|
-
*
|
|
3587
|
-
* ## Only the contested pairs are named, and the default skin may not contest
|
|
3588
|
-
*
|
|
3589
|
-
* A placeholder one skin fills keeps the emitted shape it has always had: no
|
|
3590
|
-
* `name`, no `path` it did not already carry. Every rig in this tree declares
|
|
3591
|
-
* exactly one skin, so **no emitted byte in the tree moves** — and a
|
|
3592
|
-
* multi-skin rig whose skins use distinct placeholders does not move either,
|
|
3593
|
-
* because nothing there is ambiguous to begin with.
|
|
3594
|
-
*
|
|
3595
|
-
* ⚠️ A contested placeholder the **default** skin fills is refused before any
|
|
3596
|
-
* of this runs — `refuseDefaultSkinContest`, issue #567 — because two editor
|
|
3597
|
-
* round trips showed the editor holds no such shape in either spelling. So
|
|
3598
|
-
* every entry the walk below composes for belongs to a named skin, and the
|
|
3599
|
-
* emitted name comes off `composeSkinAttachmentName`, which this function calls
|
|
3600
|
-
* rather than restates: the emit and the refusal disagreeing about one name is
|
|
3601
|
-
* the defect both of them exist to prevent.
|
|
3602
|
-
*
|
|
3603
|
-
* ⚠️ The scope of the editor's uniqueness rule is **not** skeleton-wide, and the
|
|
3604
|
-
* corpus proves it rather than a hypothesis doing so: `spineboy-pro.json`, which
|
|
3605
|
-
* the editor wrote, gives the name `head` to a region in slot `head` and to a
|
|
3606
|
-
* bounding box in slot `head-bb`, and names one `hoverglow-small` across eight
|
|
3607
|
-
* slots. What #541 refused was one slot. Composing from the skin makes the names
|
|
3608
|
-
* unique within the slot, which satisfies that scope and every narrower one;
|
|
3609
|
-
* nothing here claims to know which of them the editor actually applies, and an
|
|
3610
|
-
* assertion that policed the emitted artifact would have to.
|
|
3611
|
-
*
|
|
3612
|
-
* ## Why a composed name is not the compiler inventing a value
|
|
3613
|
-
*
|
|
3614
|
-
* rigc has always decided this attachment's name — it decided it was the
|
|
3615
|
-
* placeholder, silently, and that decision is the defect. What changes is the
|
|
3616
|
-
* derivation, not who makes it, and the new one is a function of two names the
|
|
3617
|
-
* spec wrote. Nothing is read off the art, and `path` — the field that says
|
|
3618
|
-
* which texture to draw — stays exactly what the spec stated or what the
|
|
3619
|
-
* 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.
|
|
3620
3637
|
*/
|
|
3621
|
-
function
|
|
3622
|
-
skinNames: readonly string[],
|
|
3623
|
-
skinParts: Map<string, RigSkinParts>,
|
|
3624
|
-
): Map<string, Set<string>> {
|
|
3638
|
+
function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<string, RigSkinParts>): void {
|
|
3625
3639
|
/** slot -> placeholder -> the skins that fill it, in declaration order. */
|
|
3626
3640
|
const fillers = new Map<string, Map<string, string[]>>();
|
|
3627
3641
|
for (const skinName of skinNames) {
|
|
@@ -3633,82 +3647,11 @@ function contestedPlaceholders(
|
|
|
3633
3647
|
fillers.set(slotName, perSlot);
|
|
3634
3648
|
}
|
|
3635
3649
|
}
|
|
3636
|
-
const contested = new Map<string, Set<string>>();
|
|
3637
|
-
const collisions: string[] = [];
|
|
3638
3650
|
for (const [slotName, perSlot] of fillers) {
|
|
3639
|
-
const shared = new Set([...perSlot].filter(([, skins]) => skins.length > 1).map(([placeholder]) => placeholder));
|
|
3640
|
-
// 🚨 Before anything is composed: a contested placeholder the DEFAULT skin
|
|
3641
|
-
// fills has no representation in the editor at all, in either spelling
|
|
3642
|
-
// (issue #567, round trips 7 and 8). It is refused here rather than emitted,
|
|
3643
|
-
// and the refusal comes first because renaming cannot repair it — the
|
|
3644
|
-
// remedy is a different rig, not a different string.
|
|
3645
|
-
for (const placeholder of shared) {
|
|
3646
|
-
const skins = perSlot.get(placeholder)!;
|
|
3647
|
-
if (skins.includes(DEFAULT_SKIN)) refuseDefaultSkinContest(slotName, placeholder, skins);
|
|
3648
|
-
}
|
|
3649
|
-
if (shared.size) contested.set(slotName, shared);
|
|
3650
|
-
/** Emitted attachment name -> the first entry that claimed it. */
|
|
3651
|
-
const claimed = new Map<string, string>();
|
|
3652
3651
|
for (const [placeholder, skins] of perSlot) {
|
|
3653
|
-
|
|
3654
|
-
// The emitted name, read off the one function that decides it — so the
|
|
3655
|
-
// refusal and the emit cannot drift into two readings. An UNCONTESTED
|
|
3656
|
-
// entry is claimed under its bare placeholder, the default skin's
|
|
3657
|
-
// included: a named skin whose composed name equals it is a collision,
|
|
3658
|
-
// and one this walk sees for the same reason it sees every other.
|
|
3659
|
-
const name = composeSkinAttachmentName(skinName, placeholder, shared.has(placeholder)) ?? placeholder;
|
|
3660
|
-
const site = `skin "${skinName}" placeholder "${placeholder}"`;
|
|
3661
|
-
const taken = claimed.get(name);
|
|
3662
|
-
if (taken === undefined) claimed.set(name, site);
|
|
3663
|
-
else collisions.push(`slot "${slotName}": ${taken} and ${site} would both be named "${name}"`);
|
|
3664
|
-
}
|
|
3652
|
+
if (skins.length > 1 && skins.includes(DEFAULT_SKIN)) refuseDefaultSkinContest(slotName, placeholder, skins);
|
|
3665
3653
|
}
|
|
3666
3654
|
}
|
|
3667
|
-
if (collisions.length) {
|
|
3668
|
-
throw new CompileError(
|
|
3669
|
-
`${collisions.length} attachment name collision(s): a placeholder that more than one skin fills is emitted ` +
|
|
3670
|
-
`with the name "<skin>${SKIN_ATTACHMENT_SEPARATOR}<placeholder>", because the Spine editor refuses an import ` +
|
|
3671
|
-
'in which one slot holds two attachments of one name (#541) — and here that composed name is one another ' +
|
|
3672
|
-
'entry in the same slot already answers to. Rename the placeholder or the skin so the two differ. ' +
|
|
3673
|
-
`${collisions.join('. ')}`,
|
|
3674
|
-
);
|
|
3675
|
-
}
|
|
3676
|
-
return contested;
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
/**
|
|
3680
|
-
* Give one attachment its own `name`, and pin the texture `path` that name would
|
|
3681
|
-
* otherwise have taken with it.
|
|
3682
|
-
*
|
|
3683
|
-
* 🚨 The second half is the whole hazard. `readAttachment` reads
|
|
3684
|
-
* `const name = getValue(map, "name", placeholder)` and then
|
|
3685
|
-
* `const path = getValue(map, "path", name)` (`SkeletonJson.ts:526-529`, and
|
|
3686
|
-
* again at `:559` for a mesh) — so `path` defaults to the NAME, not to the
|
|
3687
|
-
* placeholder. Writing a name and leaving `path` alone silently repoints the
|
|
3688
|
-
* attachment's texture lookup at a region no atlas has. Restating `path` at what
|
|
3689
|
-
* the attachment already resolved to makes the name change invisible to
|
|
3690
|
-
* everything but the editor's own uniqueness rule, which is the only thing it is
|
|
3691
|
-
* for.
|
|
3692
|
-
*
|
|
3693
|
-
* ⚠️ `region` and `mesh` are exactly the two types that read `path`; the polygon
|
|
3694
|
-
* types (`boundingbox`, `clipping`, `path`) have no texture and get the name
|
|
3695
|
-
* alone. The list is the parser's own two `getValue(map, "path", …)` sites
|
|
3696
|
-
* rather than a judgement about which attachments "have art".
|
|
3697
|
-
*/
|
|
3698
|
-
function nameSkinAttachment(att: SpineAttachment, name: string, placeholder: string): SpineAttachment {
|
|
3699
|
-
const kind = (att as { type?: string }).type ?? 'region';
|
|
3700
|
-
// Key order is the parser's reading order — `name`, then `path`, then the rest
|
|
3701
|
-
// as the builder wrote it — for the same reason every other emitted object
|
|
3702
|
-
// follows it: the file is read by people and diffed against references.
|
|
3703
|
-
//
|
|
3704
|
-
// ⚠️ The three kinds here are the three the parser gives a texture `path` to,
|
|
3705
|
-
// and `linkedmesh` is one of them (`SkeletonJson.ts:541`, `:570` — the mesh
|
|
3706
|
-
// branch is shared). Leaving it out would write a `name` and no `path`, and
|
|
3707
|
-
// `path` defaults to `name`, so a contested link would resolve the region
|
|
3708
|
-
// "<skin>/<placeholder>", which no atlas holds.
|
|
3709
|
-
if (kind !== 'region' && kind !== 'mesh' && kind !== 'linkedmesh') return { name, ...att };
|
|
3710
|
-
const { path, ...rest } = att as SpineRegionAttachment | SpineMeshAttachment | SpineLinkedMeshAttachment;
|
|
3711
|
-
return { name, path: path ?? placeholder, ...rest } as SpineAttachment;
|
|
3712
3655
|
}
|
|
3713
3656
|
|
|
3714
3657
|
// ---------------------------------------------------------------------------
|
|
@@ -3845,6 +3788,20 @@ function buildRigAttachment(
|
|
|
3845
3788
|
);
|
|
3846
3789
|
}
|
|
3847
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
|
+
}
|
|
3848
3805
|
// A linked mesh in the format's OTHER spelling. `type: "mesh"` and `type:
|
|
3849
3806
|
// "linkedmesh"` share one branch and the `source` key is what decides between
|
|
3850
3807
|
// them (`:568-569`, `:582`; SPEC_COVERAGE part 1-6) — so a mesh carrying
|
|
@@ -4285,11 +4242,42 @@ function atlasedImage(image: string, where: string, ctx: AttachmentContext): Com
|
|
|
4285
4242
|
* through one line of parser, and it would have had to contradict §3.4 rather
|
|
4286
4243
|
* than satisfy it.
|
|
4287
4244
|
*/
|
|
4288
|
-
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 {
|
|
4289
4246
|
if (att.path !== undefined) return att.path;
|
|
4290
4247
|
if (att.image === undefined) return undefined;
|
|
4291
4248
|
const region = basename(att.image, '.png');
|
|
4292
|
-
|
|
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;
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
/**
|
|
4257
|
+
* A mesh attachment's `path` and `color`, in that order — the two keys every
|
|
4258
|
+
* mesh constructor spreads right after `type` (issue #791).
|
|
4259
|
+
*
|
|
4260
|
+
* 🔬 The editor writes a mesh `type, path, color, uvs, …`: right after `type`,
|
|
4261
|
+
* `path` before `color` — measured on a production set (#791), not on
|
|
4262
|
+
* `examples/`. There 18 meshes whose `path` differs from their name wrote it
|
|
4263
|
+
* second and 1 mesh with a `color` wrote that second; no object carried both, so
|
|
4264
|
+
* `path` before `color` is the order the two measured positions leave, not a
|
|
4265
|
+
* third measurement. The twelve exports carry neither key on a mesh, which is
|
|
4266
|
+
* why `EDITOR_KEY_ORDER`'s `mesh attachment` row does not list them: an unlisted
|
|
4267
|
+
* key keeps its constructor's index, so the position is stated HERE, and the
|
|
4268
|
+
* row stays what the public exports derive.
|
|
4269
|
+
*
|
|
4270
|
+
* ⚠️ Not a linked mesh's, and not a region's. Neither was in the measured set —
|
|
4271
|
+
* a region's `path` stays where `buildRigRegion` puts it, a linked mesh's
|
|
4272
|
+
* keys where `buildRigLinkedMesh` does — because the analogy to a mesh is
|
|
4273
|
+
* obvious and it is not a measurement.
|
|
4274
|
+
*/
|
|
4275
|
+
function meshTextureKeys(att: { name?: string; path?: string; image?: string; color?: string }, placeholder: string): Pick<SpineMeshAttachment, 'path' | 'color'> {
|
|
4276
|
+
const out: Pick<SpineMeshAttachment, 'path' | 'color'> = {};
|
|
4277
|
+
const path = attachmentPath(att, placeholder);
|
|
4278
|
+
if (path !== undefined) out.path = path;
|
|
4279
|
+
if (att.color !== undefined) out.color = att.color;
|
|
4280
|
+
return out;
|
|
4293
4281
|
}
|
|
4294
4282
|
|
|
4295
4283
|
/**
|
|
@@ -4328,8 +4316,22 @@ function emitSequence(seq: RigSequence): SpineSequence {
|
|
|
4328
4316
|
* measures the same: that is the one number the frames state. Frames of
|
|
4329
4317
|
* different sizes state several, and picking one — the first, the setup frame,
|
|
4330
4318
|
* the largest — would be the compiler choosing a value the spec did not.
|
|
4331
|
-
*
|
|
4332
|
-
*
|
|
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.
|
|
4333
4335
|
*/
|
|
4334
4336
|
function sequenceFrameSize(
|
|
4335
4337
|
frames: readonly CompiledImage[],
|
|
@@ -4340,14 +4342,6 @@ function sequenceFrameSize(
|
|
|
4340
4342
|
for (const field of ['width', 'height'] as const) {
|
|
4341
4343
|
const sizes = [...new Set(frames.map((img) => img[field]))];
|
|
4342
4344
|
if (stated[field] !== undefined) {
|
|
4343
|
-
const packed = frames.find((img) => img.atlas !== undefined && img[field] !== stated[field]);
|
|
4344
|
-
if (packed !== undefined) {
|
|
4345
|
-
throw new CompileError(
|
|
4346
|
-
`${where}: the spec says ${field} ${stated[field]} and sequence frame "${packed.region}" of the imported ` +
|
|
4347
|
-
`atlas is ${packed[field]}; a packed frame's rectangle is fixed, so the two would produce a quad the ` +
|
|
4348
|
-
'pack cannot fill',
|
|
4349
|
-
);
|
|
4350
|
-
}
|
|
4351
4345
|
out[field] = stated[field];
|
|
4352
4346
|
} else if (sizes.length === 1) {
|
|
4353
4347
|
out[field] = sizes[0];
|
|
@@ -4365,7 +4359,8 @@ function sequenceFrameSize(
|
|
|
4365
4359
|
/** Every frame of `att`'s sequence, already atlased by the gather pass. */
|
|
4366
4360
|
function sequenceFrames(att: { path?: string; sequence?: RigSequence }, placeholder: string, where: string, ctx: AttachmentContext): CompiledImage[] {
|
|
4367
4361
|
const seq = att.sequence!;
|
|
4368
|
-
|
|
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;
|
|
4369
4364
|
const frames: CompiledImage[] = [];
|
|
4370
4365
|
for (let i = 0; i < seq.count; i++) {
|
|
4371
4366
|
// By the frame's FULL region name — `atlasedImage` takes a basename, and a
|
|
@@ -4707,6 +4702,7 @@ function buildRigMesh(
|
|
|
4707
4702
|
}
|
|
4708
4703
|
const out: SpineMeshAttachment = {
|
|
4709
4704
|
type: 'mesh',
|
|
4705
|
+
...meshTextureKeys(att, placeholder),
|
|
4710
4706
|
uvs: att.uvs.map(f32),
|
|
4711
4707
|
triangles: att.triangles,
|
|
4712
4708
|
vertices,
|
|
@@ -4715,9 +4711,6 @@ function buildRigMesh(
|
|
|
4715
4711
|
width: f32(width),
|
|
4716
4712
|
height: f32(height),
|
|
4717
4713
|
};
|
|
4718
|
-
const path = attachmentPath(att, placeholder);
|
|
4719
|
-
if (path !== undefined) out.path = path;
|
|
4720
|
-
if (att.color !== undefined) out.color = att.color;
|
|
4721
4714
|
if (att.sequence !== undefined) out.sequence = emitSequence(att.sequence);
|
|
4722
4715
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
4723
4716
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
@@ -4998,6 +4991,7 @@ function buildGeneratedMesh(
|
|
|
4998
4991
|
});
|
|
4999
4992
|
const out: SpineMeshAttachment = {
|
|
5000
4993
|
type: 'mesh',
|
|
4994
|
+
...meshTextureKeys(att, placeholder),
|
|
5001
4995
|
uvs: geometry.uvs.map(f32),
|
|
5002
4996
|
triangles: geometry.triangles,
|
|
5003
4997
|
vertices: vertices.map(f32),
|
|
@@ -5005,9 +4999,6 @@ function buildGeneratedMesh(
|
|
|
5005
4999
|
width: f32(w),
|
|
5006
5000
|
height: f32(h),
|
|
5007
5001
|
};
|
|
5008
|
-
const path = attachmentPath(att, placeholder);
|
|
5009
|
-
if (path !== undefined) out.path = path;
|
|
5010
|
-
if (att.color !== undefined) out.color = att.color;
|
|
5011
5002
|
return out;
|
|
5012
5003
|
}
|
|
5013
5004
|
|
|
@@ -5565,6 +5556,7 @@ function buildGridAttachment(
|
|
|
5565
5556
|
});
|
|
5566
5557
|
const out: SpineMeshAttachment = {
|
|
5567
5558
|
type: 'mesh',
|
|
5559
|
+
...meshTextureKeys(att, placeholder),
|
|
5568
5560
|
uvs: geometry.uvs.map(f32),
|
|
5569
5561
|
triangles: geometry.triangles,
|
|
5570
5562
|
vertices: vertices.map(f32),
|
|
@@ -5572,9 +5564,6 @@ function buildGridAttachment(
|
|
|
5572
5564
|
width: f32(w),
|
|
5573
5565
|
height: f32(h),
|
|
5574
5566
|
};
|
|
5575
|
-
const path = attachmentPath(att, placeholder);
|
|
5576
|
-
if (path !== undefined) out.path = path;
|
|
5577
|
-
if (att.color !== undefined) out.color = att.color;
|
|
5578
5567
|
return out;
|
|
5579
5568
|
}
|
|
5580
5569
|
|
|
@@ -5726,8 +5715,15 @@ function buildContourAttachment(
|
|
|
5726
5715
|
holePixels: geometry.contour?.holePixels,
|
|
5727
5716
|
depth: depth?.summary,
|
|
5728
5717
|
});
|
|
5718
|
+
// Same rule a region attachment follows: the atlas region is the PNG's
|
|
5719
|
+
// basename, so a placeholder named anything else needs `path` written down or
|
|
5720
|
+
// the loader resolves nothing. Stated once in `attachmentPath` since #577 —
|
|
5721
|
+
// this comment used to be the rule's only statement, beside four emit sites
|
|
5722
|
+
// that disagreed with it — and spread right after `type` by
|
|
5723
|
+
// `meshTextureKeys` since #791.
|
|
5729
5724
|
const out: SpineMeshAttachment = {
|
|
5730
5725
|
type: 'mesh',
|
|
5726
|
+
...meshTextureKeys(att, placeholder),
|
|
5731
5727
|
uvs: geometry.uvs.map(f32),
|
|
5732
5728
|
triangles: geometry.triangles,
|
|
5733
5729
|
vertices: vertices.map(f32),
|
|
@@ -5735,14 +5731,6 @@ function buildContourAttachment(
|
|
|
5735
5731
|
width: f32(w),
|
|
5736
5732
|
height: f32(h),
|
|
5737
5733
|
};
|
|
5738
|
-
// Same rule a region attachment follows: the atlas region is the PNG's
|
|
5739
|
-
// basename, so a placeholder named anything else needs `path` written down or
|
|
5740
|
-
// the loader resolves nothing. Stated once in `attachmentPath` since #577 —
|
|
5741
|
-
// this comment used to be the rule's only statement, beside four emit sites
|
|
5742
|
-
// that disagreed with it.
|
|
5743
|
-
const path = attachmentPath(att, placeholder);
|
|
5744
|
-
if (path !== undefined) out.path = path;
|
|
5745
|
-
if (att.color !== undefined) out.color = att.color;
|
|
5746
5734
|
return out;
|
|
5747
5735
|
}
|
|
5748
5736
|
|