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/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
- * `4.3`, `4.3.<patch>`, or `4.3.<patch>-<suffix>` — the last of which is what the
516
- * Spine editor writes for a pre-release (`"4.3.75-beta"` in all twelve official
517
- * example exports). The major/minor pair is the load-bearing part; see A16.
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 SPINE_4_3_VERSION = /^4\.3(\.\d+(-[0-9A-Za-z][0-9A-Za-z.+-]*)?)?$/;
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 (issue #691).
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 rawLinkedMeshSources(raw: unknown): Map<string, string> {
630
- const links = new Map<string, 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}`, source);
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' || !SPINE_4_3_VERSION.test(declared)) {
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 ['ik', 'transform', 'path', 'physics', 'slider']) {
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) && 'transform' in 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
- * `source` it names (issue #691).
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, string>();
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 = rawLinkedMeshSources(raw);
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 source = rawLinks.get(`${skin.name}\u0000${data.slots[entry.slotIndex].name}\u0000${entry.placeholder}`);
1719
- if (source !== undefined) linkedMeshes.set(att, source);
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 every one of its ${width}x${height} texels on shared page ` +
4135
- `"${page.name}", so it would paint a solid rectangle over whatever is drawn behind it. Re-export ` +
4136
- `the part with transparency and pack again. ${exemption} This is renderer policy, and it belongs ` +
4137
- 'to --profile spine-html: the default --profile spine does not run this check.',
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;