spine-rigc 0.26.0 → 0.28.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 +4 -4
- package/cli.ts +159 -10
- package/docs/AUTHORING.md +208 -6
- package/docs/INGEST.md +58 -2
- package/docs/SPEC_COVERAGE.md +1 -0
- package/package.json +1 -1
- package/src/diff.ts +235 -9
- package/src/generation.ts +138 -0
- package/src/ingest.ts +187 -0
- package/src/pose.ts +195 -21
- package/src/validate.ts +226 -24
package/src/validate.ts
CHANGED
|
@@ -59,6 +59,12 @@ import {
|
|
|
59
59
|
type DeformReach,
|
|
60
60
|
type DialSpan,
|
|
61
61
|
} from './deformmeasure.ts';
|
|
62
|
+
import {
|
|
63
|
+
LEGACY_BONE_INHERIT_KEY,
|
|
64
|
+
spineGeneration,
|
|
65
|
+
TOPLEVEL_CONSTRAINT_ARRAYS,
|
|
66
|
+
type SpineGeneration,
|
|
67
|
+
} from './generation.ts';
|
|
62
68
|
import { colourTypeName, readPngInfo } from './png.ts';
|
|
63
69
|
import {
|
|
64
70
|
CHANNELS_BY_KIND,
|
|
@@ -188,6 +194,7 @@ const ASSERTION_KIND: Record<string, 'validity' | 'renderer' | 'archetype'> = {
|
|
|
188
194
|
A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: 'validity',
|
|
189
195
|
A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER: 'validity',
|
|
190
196
|
A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN: 'validity',
|
|
197
|
+
A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN: 'validity',
|
|
191
198
|
};
|
|
192
199
|
|
|
193
200
|
/**
|
|
@@ -291,6 +298,7 @@ export const SKIP_NO_ATTACHMENT_REGION_JOIN =
|
|
|
291
298
|
'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
|
|
292
299
|
export const SKIP_NO_TWO_COLOR_TINT =
|
|
293
300
|
'no slot declares a "dark" colour and no animation keys an "rgba2" timeline, so there is no two-colour tint to read back';
|
|
301
|
+
export const SKIP_NO_LINKED_MESH = 'no attachment in this skeleton takes its geometry from another one';
|
|
294
302
|
/**
|
|
295
303
|
* A09's, which predates this list and joins it rather than being rewritten: it
|
|
296
304
|
* is the same fact about the same subject, and a control that compares against
|
|
@@ -512,11 +520,20 @@ function float32Step(t: number): number {
|
|
|
512
520
|
}
|
|
513
521
|
|
|
514
522
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
523
|
+
* The generation `A16` demands, which is the whole of what that assertion is
|
|
524
|
+
* about: the MAJOR.MINOR pair.
|
|
525
|
+
*
|
|
526
|
+
* ⭐ **The reading itself moved to [`generation.ts`](generation.ts)** with issue
|
|
527
|
+
* #706 — one reader of `skeleton.spine` for the whole repository, because
|
|
528
|
+
* `ingest` has to ask the same question of a file somebody else wrote and two
|
|
529
|
+
* regexes would answer it two ways. What did NOT move is the accepted set:
|
|
530
|
+
* `4.3`, `4.3.<patch>` and `4.3.<patch>-<suffix>`, the last of which is what the
|
|
531
|
+
* editor writes for a pre-release (`"4.3.75-beta"` in all twelve official
|
|
532
|
+
* example exports, and the string the original `/^4\.3(\.\d+)?$/` rejected —
|
|
533
|
+
* blocker B2). `GN05` holds that set against the old regex, which survives in
|
|
534
|
+
* `selftest.ts` and nowhere else, for exactly that comparison.
|
|
518
535
|
*/
|
|
519
|
-
const
|
|
536
|
+
const SPINE_4_3: SpineGeneration = '4.3';
|
|
520
537
|
|
|
521
538
|
type Json = Record<string, unknown>;
|
|
522
539
|
|
|
@@ -616,9 +633,54 @@ export function attachmentRegionJoins(raw: unknown): AttachmentRegionJoin[] {
|
|
|
616
633
|
return joins;
|
|
617
634
|
}
|
|
618
635
|
|
|
636
|
+
/**
|
|
637
|
+
* The mesh keys the `source` branch never reaches — the geometry a linked mesh
|
|
638
|
+
* may state and nothing reads (issue #710).
|
|
639
|
+
*
|
|
640
|
+
* Derived from the branch rather than chosen. `readAttachment` returns at
|
|
641
|
+
* `SkeletonJson.js:586` as soon as `source` is truthy, and everything below that
|
|
642
|
+
* return reads `map.uvs` (twice: as the length handed to `readVertices` and as
|
|
643
|
+
* `regionUVs`), `map.triangles`, `map.edges` and `map.hull`. `vertices` is on the
|
|
644
|
+
* list because `readVertices` reads `map.vertices` and nothing else (`:654`), so
|
|
645
|
+
* it goes unread with the call that would have read it.
|
|
646
|
+
*
|
|
647
|
+
* ⚠️ `width` and `height` are NOT on this list, although `setSourceMesh`
|
|
648
|
+
* overwrites both with the source's (`MeshAttachment.js:102-103`). The branch
|
|
649
|
+
* reads them at `:569-570`, the format carries them on a link and rigc emits
|
|
650
|
+
* them (#691). A key the parser reads is not a key the parser ignores, whatever
|
|
651
|
+
* a later pass does with the value.
|
|
652
|
+
*/
|
|
653
|
+
const LINKED_MESH_UNREAD_KEYS = ['uvs', 'triangles', 'vertices', 'hull', 'edges'] as const;
|
|
654
|
+
|
|
655
|
+
/** One linked mesh as the FILE spells it, before the loader has resolved anything. */
|
|
656
|
+
interface RawLinkedMesh {
|
|
657
|
+
/** The placeholder of the mesh whose geometry this attachment draws. */
|
|
658
|
+
source: string;
|
|
659
|
+
/**
|
|
660
|
+
* The `skin` the entry states, or `undefined` for the parser's default — which
|
|
661
|
+
* is the DEFAULT skin and never the skin the link is written in (`:429`).
|
|
662
|
+
*/
|
|
663
|
+
skin?: string;
|
|
664
|
+
/** The `slot` the entry states, or `undefined` for the parser's default: this attachment's own slot (`:573-579`). */
|
|
665
|
+
slot?: string;
|
|
666
|
+
/** Which of `LINKED_MESH_UNREAD_KEYS` this entry states, in that order. */
|
|
667
|
+
geometry: string[];
|
|
668
|
+
/**
|
|
669
|
+
* How big a mesh those keys describe — `uvs.length / 2` and
|
|
670
|
+
* `triangles.length / 3` — when the entry states them as arrays.
|
|
671
|
+
*
|
|
672
|
+
* Read so that the failure can put the shape the author wrote beside the shape
|
|
673
|
+
* the runtime draws. `undefined` where the file states the key as something
|
|
674
|
+
* other than an array, which is a file this rule refuses for the key rather
|
|
675
|
+
* than for its length.
|
|
676
|
+
*/
|
|
677
|
+
statedVertices?: number;
|
|
678
|
+
statedTriangles?: number;
|
|
679
|
+
}
|
|
680
|
+
|
|
619
681
|
/**
|
|
620
682
|
* `"<skin>\0<slot>\0<placeholder>" -> source` for every linked mesh the raw
|
|
621
|
-
* skeleton declares (
|
|
683
|
+
* skeleton declares, with what the file says about each one (issues #691, #710).
|
|
622
684
|
*
|
|
623
685
|
* 🔑 The test is the parser's own and it is not `type`: `type: "mesh"` and
|
|
624
686
|
* `type: "linkedmesh"` share one branch and a truthy `source` is what decides
|
|
@@ -626,8 +688,8 @@ export function attachmentRegionJoins(raw: unknown): AttachmentRegionJoin[] {
|
|
|
626
688
|
* there, so it is not a link here either — that map is read as an ordinary mesh,
|
|
627
689
|
* which is exactly what the runtime does with it.
|
|
628
690
|
*/
|
|
629
|
-
function
|
|
630
|
-
const links = new Map<string,
|
|
691
|
+
function rawLinkedMeshes(raw: unknown): Map<string, RawLinkedMesh> {
|
|
692
|
+
const links = new Map<string, RawLinkedMesh>();
|
|
631
693
|
if (!isObj(raw) || !Array.isArray(raw.skins)) return links;
|
|
632
694
|
for (const skin of raw.skins as unknown[]) {
|
|
633
695
|
if (!isObj(skin) || !isObj(skin.attachments)) continue;
|
|
@@ -640,7 +702,14 @@ function rawLinkedMeshSources(raw: unknown): Map<string, string> {
|
|
|
640
702
|
if (type !== 'mesh' && type !== 'linkedmesh') continue;
|
|
641
703
|
const source = entry.source;
|
|
642
704
|
if (typeof source !== 'string' || source.length === 0) continue;
|
|
643
|
-
links.set(`${skinName}\u0000${slot}\u0000${placeholder}`,
|
|
705
|
+
links.set(`${skinName}\u0000${slot}\u0000${placeholder}`, {
|
|
706
|
+
source,
|
|
707
|
+
skin: typeof entry.skin === 'string' ? entry.skin : undefined,
|
|
708
|
+
slot: typeof entry.slot === 'string' ? entry.slot : undefined,
|
|
709
|
+
geometry: LINKED_MESH_UNREAD_KEYS.filter((key) => entry[key] !== undefined),
|
|
710
|
+
statedVertices: Array.isArray(entry.uvs) ? entry.uvs.length / 2 : undefined,
|
|
711
|
+
statedTriangles: Array.isArray(entry.triangles) ? entry.triangles.length / 3 : undefined,
|
|
712
|
+
});
|
|
644
713
|
}
|
|
645
714
|
}
|
|
646
715
|
}
|
|
@@ -1585,7 +1654,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1585
1654
|
// stay rejected.
|
|
1586
1655
|
check('A16_SKELETON_VERSION_4_3', () => {
|
|
1587
1656
|
const declared = isObj(raw?.skeleton) ? (raw.skeleton as Json).spine : undefined;
|
|
1588
|
-
if (typeof declared !== 'string' ||
|
|
1657
|
+
if (typeof declared !== 'string' || spineGeneration(declared) !== SPINE_4_3) {
|
|
1589
1658
|
fail(
|
|
1590
1659
|
'A16_SKELETON_VERSION_4_3',
|
|
1591
1660
|
`skeleton.spine is ${JSON.stringify(declared)}, expected 4.3, 4.3.<patch> or 4.3.<patch>-<suffix>`,
|
|
@@ -1596,9 +1665,11 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1596
1665
|
// --- A01: no legacy top-level constraint arrays ---------------------------
|
|
1597
1666
|
// 4.3 folds every constraint into one `constraints` array with a `type`.
|
|
1598
1667
|
// A 4.1/4.2-shaped `physics` array loads clean and the constraint just
|
|
1599
|
-
// vanishes.
|
|
1668
|
+
// vanishes. ⭐ The list is `generation.ts`'s since #706, because `ingest` has
|
|
1669
|
+
// to count the same arrays in a file it did not emit, and two copies of five
|
|
1670
|
+
// names is how one of them comes to be four.
|
|
1600
1671
|
check('A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS', () => {
|
|
1601
|
-
for (const key of
|
|
1672
|
+
for (const key of TOPLEVEL_CONSTRAINT_ARRAYS) {
|
|
1602
1673
|
if (raw && key in raw) {
|
|
1603
1674
|
fail(
|
|
1604
1675
|
'A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS',
|
|
@@ -1610,11 +1681,12 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1610
1681
|
|
|
1611
1682
|
// --- A02: no bone.transform key ------------------------------------------
|
|
1612
1683
|
// 4.3 renamed it to `inherit`; the old key loads and silently falls back to
|
|
1613
|
-
// Normal inheritance (case 6b).
|
|
1684
|
+
// Normal inheritance (case 6b). The key itself is `generation.ts`'s, for
|
|
1685
|
+
// A01's reason.
|
|
1614
1686
|
check('A02_NO_BONE_TRANSFORM_KEY', () => {
|
|
1615
1687
|
const bones = Array.isArray(raw?.bones) ? (raw.bones as unknown[]) : [];
|
|
1616
1688
|
for (const bone of bones) {
|
|
1617
|
-
if (isObj(bone) &&
|
|
1689
|
+
if (isObj(bone) && LEGACY_BONE_INHERIT_KEY in bone) {
|
|
1618
1690
|
fail('A02_NO_BONE_TRANSFORM_KEY', `bone "${String(bone.name)}" uses 4.2's "transform"; 4.3 wants "inherit"`);
|
|
1619
1691
|
}
|
|
1620
1692
|
}
|
|
@@ -1689,8 +1761,8 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1689
1761
|
let clippingCount = 0;
|
|
1690
1762
|
const meshSlots = new Set<number>();
|
|
1691
1763
|
/**
|
|
1692
|
-
* The loaded mesh of every attachment the FILE spells as a link, to the
|
|
1693
|
-
*
|
|
1764
|
+
* The loaded mesh of every attachment the FILE spells as a link, to what the
|
|
1765
|
+
* file says about it (issues #691, #710).
|
|
1694
1766
|
*
|
|
1695
1767
|
* 🔑 Read off the raw JSON and joined by (skin, slot, placeholder) rather than
|
|
1696
1768
|
* asked of the loaded object, because `MeshAttachment.sourceMesh` is **private
|
|
@@ -1703,11 +1775,23 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1703
1775
|
* placeholder is unique only within one skin's slot and several skins fill
|
|
1704
1776
|
* one — and every assertion downstream holds the attachment, not its address.
|
|
1705
1777
|
*/
|
|
1706
|
-
const linkedMeshes = new Map<MeshAttachment,
|
|
1778
|
+
const linkedMeshes = new Map<MeshAttachment, RawLinkedMesh>();
|
|
1779
|
+
/**
|
|
1780
|
+
* The same pairing the other way round — join key -> the attachment the loader
|
|
1781
|
+
* produced for it — which is what `A44` needs and `kindOf` does not.
|
|
1782
|
+
*
|
|
1783
|
+
* 🔑 Two maps rather than one because the two questions are different. Every
|
|
1784
|
+
* rule that asks "is THIS attachment a link" holds the object and wants the
|
|
1785
|
+
* file's word about it; `A44` walks the FILE's links and asks what the runtime
|
|
1786
|
+
* made of each, including the answer "nothing" — a link whose region is
|
|
1787
|
+
* missing loads as `null` and is in no skin at all (`A08` names that), so its
|
|
1788
|
+
* join key is absent here while the file still declares it.
|
|
1789
|
+
*/
|
|
1790
|
+
const loadedLinks = new Map<string, MeshAttachment>();
|
|
1707
1791
|
|
|
1708
1792
|
if (skeletonData) {
|
|
1709
1793
|
const data = skeletonData as NonNullable<typeof skeletonData>;
|
|
1710
|
-
const rawLinks =
|
|
1794
|
+
const rawLinks = rawLinkedMeshes(raw);
|
|
1711
1795
|
for (const skin of data.skins) {
|
|
1712
1796
|
for (const entry of skin.getAttachments()) {
|
|
1713
1797
|
const att = entry.attachment;
|
|
@@ -1715,8 +1799,12 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1715
1799
|
else if (att instanceof MeshAttachment) {
|
|
1716
1800
|
meshAttachments.push(att);
|
|
1717
1801
|
meshSlots.add(entry.slotIndex);
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1802
|
+
const join = `${skin.name}\u0000${data.slots[entry.slotIndex].name}\u0000${entry.placeholder}`;
|
|
1803
|
+
const link = rawLinks.get(join);
|
|
1804
|
+
if (link !== undefined) {
|
|
1805
|
+
linkedMeshes.set(att, link);
|
|
1806
|
+
loadedLinks.set(join, att);
|
|
1807
|
+
}
|
|
1720
1808
|
} else if (att instanceof ClippingAttachment) clippingCount++;
|
|
1721
1809
|
}
|
|
1722
1810
|
}
|
|
@@ -2097,7 +2185,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2097
2185
|
list
|
|
2098
2186
|
.filter((m) => kindOf(m) === 'authored')
|
|
2099
2187
|
.map((m) => {
|
|
2100
|
-
const source = linkedMeshes.get(m);
|
|
2188
|
+
const source = linkedMeshes.get(m)?.source;
|
|
2101
2189
|
return source === undefined ? `"${m.name}"` : `"${m.name}" (linked to "${source}")`;
|
|
2102
2190
|
});
|
|
2103
2191
|
|
|
@@ -3859,6 +3947,70 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3859
3947
|
}
|
|
3860
3948
|
}
|
|
3861
3949
|
});
|
|
3950
|
+
|
|
3951
|
+
// --- A44: a linked mesh states no geometry of its own ------------------
|
|
3952
|
+
//
|
|
3953
|
+
// 🚨 The one shape the parser reads in SILENCE. `readAttachment` returns from
|
|
3954
|
+
// the `source` branch at `SkeletonJson.js:586`, before `map.uvs` is touched
|
|
3955
|
+
// at all, so `uvs`, `triangles`, `vertices`, `hull` and `edges` written on a
|
|
3956
|
+
// link are read by nothing — and `setSourceMesh` then fills the attachment
|
|
3957
|
+
// with the SOURCE's arrays. The file says one mesh and every runtime draws
|
|
3958
|
+
// another, which is why this is `validity` and not one renderer's policy.
|
|
3959
|
+
//
|
|
3960
|
+
// ⭐ **It is a separate assertion rather than a clause on A04, and the reason
|
|
3961
|
+
// is measurable both ways.** A04 reads the LOADED attachment —
|
|
3962
|
+
// `mesh.triangles`, `mesh.worldVerticesLength`, `mesh.vertices` — which on a
|
|
3963
|
+
// link are the source's after `setSourceMesh`: measured on a forged link
|
|
3964
|
+
// declaring 5 uvs and 3 triangles beside a 4-vertex source, A04 PASSED
|
|
3965
|
+
// having read 8 and 2, the source's own. The keys this rule is about are not
|
|
3966
|
+
// in the data A04 holds, so the clause would have had to reach for the raw
|
|
3967
|
+
// file, and a verdict line reading A04's name would then be naming a
|
|
3968
|
+
// measurement of the loaded geometry while deciding about file keys nothing
|
|
3969
|
+
// read. The SKIP is the sharper half: A04's subject is mesh attachments, so
|
|
3970
|
+
// on a rig with meshes and no link its subject is PRESENT and it passes —
|
|
3971
|
+
// there is no verdict left for "this rig has no link to measure", and a
|
|
3972
|
+
// clause that cannot report SKIP reports a pass for an absent subject, which
|
|
3973
|
+
// this repository already has a judgment about.
|
|
3974
|
+
//
|
|
3975
|
+
// ⚠️ The subject is the FILE's links and not the loaded ones. A link whose
|
|
3976
|
+
// region is missing loads as `null` and is in no skin (`A08` names it), so
|
|
3977
|
+
// walking the loaded attachments would let the whole rule vanish on exactly
|
|
3978
|
+
// the file that is already wrong.
|
|
3979
|
+
check('A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN', () => {
|
|
3980
|
+
if (rawLinks.size === 0) return skip('A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN', SKIP_NO_LINKED_MESH);
|
|
3981
|
+
for (const [join, link] of rawLinks) {
|
|
3982
|
+
if (link.geometry.length === 0) continue;
|
|
3983
|
+
const [skinName, slotName, placeholder] = join.split('\u0000');
|
|
3984
|
+
const at = `skin ${JSON.stringify(skinName)} slot ${JSON.stringify(slotName)} placeholder ${JSON.stringify(placeholder)}`;
|
|
3985
|
+
const keys = link.geometry.map((key) => JSON.stringify(key)).join(', ');
|
|
3986
|
+
// Where the parser looks for `source`, with the two defaults spelled out:
|
|
3987
|
+
// an omitted `skin` is the DEFAULT skin rather than this attachment's own
|
|
3988
|
+
// (`:429`), and an omitted `slot` IS this attachment's own (`:573-579`).
|
|
3989
|
+
const where =
|
|
3990
|
+
`skin ${JSON.stringify(link.skin ?? 'default')}${link.skin === undefined ? ' (the default skin, because no "skin" was stated — never the skin this attachment is written in)' : ''} ` +
|
|
3991
|
+
`slot ${JSON.stringify(link.slot ?? slotName)}${link.slot === undefined ? ' (this attachment\'s own, because no "slot" was stated)' : ''}`;
|
|
3992
|
+
// What the author's own keys describe, printed only when both are
|
|
3993
|
+
// readable — the shape the file states, beside the shape it draws.
|
|
3994
|
+
const states =
|
|
3995
|
+
link.statedVertices === undefined || link.statedTriangles === undefined
|
|
3996
|
+
? ''
|
|
3997
|
+
: ` (${link.statedVertices} vertices and ${link.statedTriangles} triangles)`;
|
|
3998
|
+
const drawn = loadedLinks.get(join);
|
|
3999
|
+
const loaded =
|
|
4000
|
+
drawn === undefined
|
|
4001
|
+
? 'what it loaded is not shown here because the round trip produced no attachment for it (A00 owns that)'
|
|
4002
|
+
: `it loaded ${drawn.worldVerticesLength / 2} vertices and ${drawn.triangles.length / 3} triangles`;
|
|
4003
|
+
fail(
|
|
4004
|
+
'A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN',
|
|
4005
|
+
`${at} links to ${JSON.stringify(link.source)} and states ${keys}${states}, and a linked mesh has no geometry of ` +
|
|
4006
|
+
'its own. The parser returns from the `source` branch before `readVertices` ' +
|
|
4007
|
+
`(\`SkeletonJson.ts:582-586\`), so ${link.geometry.length === 1 ? 'that key is' : 'those keys are'} read by ` +
|
|
4008
|
+
`nothing at all: what this attachment draws is the geometry of ${JSON.stringify(link.source)} in ${where}, and ` +
|
|
4009
|
+
`${loaded}. Remove ${link.geometry.length === 1 ? 'it' : 'them'}, or remove "source" and author this as a ` +
|
|
4010
|
+
'mesh of its own.',
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
});
|
|
3862
4014
|
}
|
|
3863
4015
|
|
|
3864
4016
|
// --- A06 / A17 / A19: the atlas against the PNGs on disk ------------------
|
|
@@ -4114,14 +4266,39 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4114
4266
|
// `rotate: 270` and red at 0, 90 and 180 — this assertion's own verdict,
|
|
4115
4267
|
// flipped by the rotation it does not judge (issue #579). The footprint
|
|
4116
4268
|
// is `pageFootprint`'s, which every other reader of it now calls.
|
|
4269
|
+
// 🚨 **The scan counts what it READ, and zero texels read is not a
|
|
4270
|
+
// verdict** (issue #705). The `continue` above walks past every
|
|
4271
|
+
// coordinate that is not on the page, so a rectangle none of whose
|
|
4272
|
+
// texels are on it came out of this loop with `transparent` still
|
|
4273
|
+
// false — indistinguishable from a solid drawing — and the sentence
|
|
4274
|
+
// below then stated opacity over texels nobody had opened. Measured on
|
|
4275
|
+
// a pack shaped like #707's: `part "block" is opaque in every one of
|
|
4276
|
+
// its 12x8 texels`, over **0 of 96**, on art carrying 36 clear texels
|
|
4277
|
+
// where it was packed. That is the message-as-UI defect in one line —
|
|
4278
|
+
// the reader is sent to re-export a part whose alpha was never the
|
|
4279
|
+
// problem, and the rectangle that is the problem belongs to A06.
|
|
4280
|
+
//
|
|
4281
|
+
// ⚠️ It is a FAIL rather than a SKIP, and the report's own shape
|
|
4282
|
+
// decides that rather than taste. `skip()` is per ASSERTION, so
|
|
4283
|
+
// skipping here would delete the verdicts on every other part of the
|
|
4284
|
+
// page — on that same pack the second part is genuinely opaque and is
|
|
4285
|
+
// named — and adding a skip BESIDE those failures puts A19 in two of
|
|
4286
|
+
// the four buckets `reportLines` adds up, which prints `45 assertions`
|
|
4287
|
+
// where the registry holds 44. What is left is a failure that says
|
|
4288
|
+
// what was not measured, which is also what "green means measured"
|
|
4289
|
+
// requires: a part this rule could not read must not be certified by
|
|
4290
|
+
// it.
|
|
4117
4291
|
const plate = readPlate(abs);
|
|
4118
4292
|
for (const region of on) {
|
|
4119
4293
|
if (baseRegions.has(region.name)) continue;
|
|
4120
4294
|
const { width, height } = pageFootprint(region);
|
|
4295
|
+
const declared = width * height;
|
|
4296
|
+
let read = 0;
|
|
4121
4297
|
let transparent = false;
|
|
4122
4298
|
for (let y = region.y; y < region.y + height && !transparent; y++) {
|
|
4123
4299
|
for (let x = region.x; x < region.x + width; x++) {
|
|
4124
4300
|
if (x < 0 || y < 0 || x >= plate.width || y >= plate.height) continue;
|
|
4301
|
+
read++;
|
|
4125
4302
|
if (plate.get(x, y)[3] < 255) {
|
|
4126
4303
|
transparent = true;
|
|
4127
4304
|
break;
|
|
@@ -4129,12 +4306,37 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
4129
4306
|
}
|
|
4130
4307
|
}
|
|
4131
4308
|
if (transparent) continue;
|
|
4309
|
+
// The page's size here is the DECODED image's and not the `size:`
|
|
4310
|
+
// line's, because it is the bound this scan actually clipped
|
|
4311
|
+
// against; where the two disagree A06 says so in its own sentence.
|
|
4312
|
+
if (read === 0) {
|
|
4313
|
+
fail(
|
|
4314
|
+
'A19_OVERLAY_PNGS_HAVE_ALPHA',
|
|
4315
|
+
`part "${region.name}" is not measured: this rule read 0 of the ${declared} texels of its ` +
|
|
4316
|
+
`${width}x${height} rectangle at ${region.x},${region.y} on page "${page.name}", whose image is ` +
|
|
4317
|
+
`${plate.width}x${plate.height}, so it states nothing about whether "${region.name}" can draw a ` +
|
|
4318
|
+
"transparent pixel. A region's rectangle is A06_ATLAS_PAGE_SIZE_MATCHES_PNG's to judge, and one " +
|
|
4319
|
+
'that runs off its page is refused there by name. This is renderer policy, and it belongs to ' +
|
|
4320
|
+
'--profile spine-html: the default --profile spine does not run this check.',
|
|
4321
|
+
);
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
// A rectangle partly on the page states the verdict over the texels
|
|
4325
|
+
// it read and says how many of the declared ones that was. A whole
|
|
4326
|
+
// rectangle prints the sentence it has always printed, to the byte.
|
|
4327
|
+
const over =
|
|
4328
|
+
read === declared
|
|
4329
|
+
? `every one of its ${width}x${height} texels on shared page "${page.name}"`
|
|
4330
|
+
: `every one of the ${read} texels of its ${width}x${height} rectangle at ${region.x},${region.y} ` +
|
|
4331
|
+
`that are on shared page "${page.name}", whose image is ${plate.width}x${plate.height} — the ` +
|
|
4332
|
+
`other ${declared - read} of the ${declared} it declares are not on the page and are not ` +
|
|
4333
|
+
'measured here';
|
|
4132
4334
|
fail(
|
|
4133
4335
|
'A19_OVERLAY_PNGS_HAVE_ALPHA',
|
|
4134
|
-
`part "${region.name}" is opaque in
|
|
4135
|
-
`
|
|
4136
|
-
|
|
4137
|
-
'
|
|
4336
|
+
`part "${region.name}" is opaque in ${over}, so it would paint a solid rectangle over whatever is ` +
|
|
4337
|
+
`drawn behind it. Re-export the part with transparency and pack again. ${exemption} This is ` +
|
|
4338
|
+
'renderer policy, and it belongs to --profile spine-html: the default --profile spine does not run ' +
|
|
4339
|
+
'this check.',
|
|
4138
4340
|
);
|
|
4139
4341
|
}
|
|
4140
4342
|
continue;
|