spine-rigc 2.20.0 → 2.20.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.20.0",
3
+ "version": "2.20.1",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,26 +54,22 @@
54
54
  * `MeshReductionError` is defined in `src/mesh.ts` for exactly that reason.
55
55
  */
56
56
  import {
57
- artOf,
58
57
  checkHullOrder,
59
58
  distanceToSegment,
60
- fillEnclosed,
61
59
  findSelfIntersection,
62
- labelIslands,
63
60
  MeshError,
64
61
  MeshReductionError,
65
62
  meshEdges,
66
- prunePolygon,
67
63
  r6,
68
64
  rasteriseTriangles,
69
65
  segmentsMeet,
70
66
  squaredDistanceToSet,
71
- traceAlphaOutline,
72
67
  traceOutline,
73
68
  type AlphaMask,
74
69
  type MeshOutline,
75
70
  } from './mesh.ts';
76
71
  import { areaBand, triangleAreas } from './areaband.ts';
72
+ import { artRastersOf, type ArtRasters } from './meshrasters.ts';
77
73
  import { cropToSpineY } from './transform.ts';
78
74
 
79
75
  // ---------------------------------------------------------------------------
@@ -1012,6 +1008,53 @@ function compareRows(a: MeasureRow, b: MeasureRow): number {
1012
1008
  */
1013
1009
  export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1014
1010
  validateInput(input);
1011
+ return measureValidated(input, artRastersOf(input.art));
1012
+ }
1013
+
1014
+ /**
1015
+ * `measureMeshQuality` over art rasters the caller already holds
1016
+ * (`src/meshrasters.ts`) — what `reduceMesh` calls for every measurement of
1017
+ * one call, so the quantities of the art alone are computed once per call
1018
+ * rather than once per step (issue #1240). The report is the one
1019
+ * `measureMeshQuality` returns for the same input, byte for byte; rasters
1020
+ * taken from another art are refused (`REDUCE_ART_RASTERS_MISMATCH`), never
1021
+ * read.
1022
+ *
1023
+ * Internal: it is on `spine-rigc/mesh` only because that entry re-exports this
1024
+ * module with `export *`, and a symbol that is merely exported is not promised
1025
+ * (RELEASING.md, *The import surface*).
1026
+ */
1027
+ export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
1028
+ validateInput(input);
1029
+ checkArtRasters(input, rasters);
1030
+ return measureValidated(input, rasters);
1031
+ }
1032
+
1033
+ /**
1034
+ * Rasters are read only for the art they were taken from: the same mask
1035
+ * array, mask size, threshold and frame. The first field that differs is
1036
+ * refused by name, with the value the rasters were taken at and the value the
1037
+ * input requires.
1038
+ */
1039
+ function checkArtRasters(input: MeshMeasureInput, rasters: ArtRasters): void {
1040
+ const want = input.art;
1041
+ const got = rasters.art;
1042
+ if (got === want) return;
1043
+ const who = `attachment ${nameOf(input.attachment)}`;
1044
+ const differs = (field: string, found: string, required: string): never =>
1045
+ refuse('REDUCE_ART_RASTERS_MISMATCH', `${who}: the art rasters were taken at ${field} ${found}; required ${field} ${required}, the input's — rasters are read only for the art they were taken from`);
1046
+ const size = (m: AlphaMask | undefined): string => (m === undefined ? 'undefined' : `${m.width}x${m.height}`);
1047
+ if (size(got.mask) !== size(want.mask)) differs('art.mask size', size(got.mask), size(want.mask));
1048
+ if (got.threshold !== want.threshold) differs('art.threshold', JSON.stringify(got.threshold), JSON.stringify(want.threshold));
1049
+ for (const field of ['pageScale', 'width', 'height'] as const) {
1050
+ if (got.frame?.[field] !== want.frame[field]) differs(`art.frame.${field}`, JSON.stringify(got.frame?.[field]), JSON.stringify(want.frame[field]));
1051
+ }
1052
+ if (got.mask.alpha !== want.mask.alpha) differs('art.mask.alpha', 'another array', 'the input\'s own array (the same object)');
1053
+ }
1054
+
1055
+ /** The measurement proper, over an input `validateInput` accepted and rasters taken from its art. */
1056
+ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
1057
+ rasters.tally.uses++;
1015
1058
  const { attachment, art, source, targets } = input;
1016
1059
  const { mask, threshold, frame } = art;
1017
1060
  const scale = frame.pageScale;
@@ -1053,9 +1096,8 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1053
1096
  const w = mask.width;
1054
1097
  const h = mask.height;
1055
1098
  const grid = { width: w, height: h, pageScale: scale };
1056
- const artBits = artOf(mask, threshold);
1057
- let artCount = 0;
1058
- for (const bit of artBits) artCount += bit;
1099
+ const artBits = rasters.artBits();
1100
+ const artCount = rasters.artCount();
1059
1101
  const onGrid: Array<[number, number]> = source.points.map(([x, y]) => [x * scale, y * scale]);
1060
1102
  const covered = rasteriseTriangles(onGrid, source.triangles, w, h);
1061
1103
  const fit = targets.artFit;
@@ -1108,17 +1150,9 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1108
1150
 
1109
1151
  // Overshoot and holes against the filled silhouette: 8-connected background over ALL art (P12) — and,
1110
1152
  // where the legacy 4-connected fill differs (a diagonal pinch), the labelled 4-connected reading beside it.
1111
- const fill8 = fillEnclosed(artBits, w, h, 8).filled;
1112
- const fill4 = fillEnclosed(artBits, w, h, 4).filled;
1113
- let fillsDiffer = false;
1114
- for (let i = 0; i < fill8.length; i++) {
1115
- if (fill8[i] !== fill4[i]) {
1116
- fillsDiffer = true;
1117
- break;
1118
- }
1119
- }
1153
+ const { fill8, fill4, differ: fillsDiffer } = rasters.fills();
1120
1154
  const silhouetteRows = (filled: Uint8Array, connectivity: 4 | 8, gated: boolean): void => {
1121
- const toFilled = squaredDistanceToSet(filled, w, h);
1155
+ const toFilled = rasters.toFilled(connectivity);
1122
1156
  let overAt = -1;
1123
1157
  let overSq = 0;
1124
1158
  let holes = 0;
@@ -1150,7 +1184,7 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1150
1184
  if (fillsDiffer) silhouetteRows(fill4, 4, false);
1151
1185
 
1152
1186
  // Islands: the 4-connected art islands (the tracer's, `labelIslands`) the triangles cover a pixel of.
1153
- const { label } = labelIslands(artBits, w, h);
1187
+ const { label } = rasters.islands();
1154
1188
  const touched = new Set<number>();
1155
1189
  for (let i = 0; i < label.length; i++) if (label[i] && covered[i]) touched.add(label[i]);
1156
1190
  let joinedAt = -1;
@@ -1179,14 +1213,12 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1179
1213
  rows.push(measuredRow(boundarySpec, r6(hd.d), bound, hd.d === 0 ? NOTHING_WORSE : { at: { edge: edgeOfHull(hd.candidateEdge) } }, true));
1180
1214
  }
1181
1215
  const traceSpec = spec('MQ_TRACE_DEVIATION', 'px');
1182
- try {
1183
- const traced = traceAlphaOutline(mask, threshold);
1184
- const outlinePx = prunePolygon(traced.outline.map(([x, y]): [number, number] => [x / scale, y / scale]));
1185
- const hd = hausdorff(hullPolygon, outlinePx);
1216
+ const traced = rasters.traced();
1217
+ if ('outline' in traced) {
1218
+ const hd = hausdorff(hullPolygon, traced.outline);
1186
1219
  rows.push(measuredRow(traceSpec, r6(hd.d), null, hd.d === 0 ? NOTHING_WORSE : { at: { edge: edgeOfHull(hd.candidateEdge) } }, false));
1187
- } catch (err) {
1188
- if (!(err instanceof MeshError)) throw err;
1189
- rows.push(unmeasuredRow(traceSpec, 'not-measurable', `attachment ${nameOf(attachment)}: the tracer refused the art at alpha >= ${threshold}: ${err.message}`, false));
1220
+ } else {
1221
+ rows.push(unmeasuredRow(traceSpec, 'not-measurable', `attachment ${nameOf(attachment)}: the tracer refused the art at alpha >= ${threshold}: ${traced.refused}`, false));
1190
1222
  }
1191
1223
 
1192
1224
  // --- triangles: sign, degeneracy, angle --------------------------------------------------
@@ -1233,7 +1265,7 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1233
1265
  );
1234
1266
 
1235
1267
  // --- regions (§5) ------------------------------------------------------------------------
1236
- rows.push(...regionRows(input, outline, hullPolygon));
1268
+ rows.push(...regionRows(input, outline, hullPolygon, rasters));
1237
1269
 
1238
1270
  const ordered = rows.slice().sort((a, b) => compareRows(a.row, b.row));
1239
1271
  const geometry = sectionOf(ordered);
@@ -1331,7 +1363,7 @@ interface Edge {
1331
1363
  * length most exceeds its own bound — so the row fails exactly when some edge
1332
1364
  * it covers is over — and the row names that edge and that bound.
1333
1365
  */
1334
- function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon: readonly Pt[]): Built[] {
1366
+ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon: readonly Pt[], rasters: ArtRasters): Built[] {
1335
1367
  const { attachment, source, art } = input;
1336
1368
  const who = `attachment ${nameOf(attachment)}`;
1337
1369
  const scale = art.frame.pageScale;
@@ -1407,7 +1439,7 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
1407
1439
 
1408
1440
  const floorOf = (name: string): number => input.regionArtSamples.find((f) => f.region === name)!.minArtSamples;
1409
1441
  const { mask, threshold } = art;
1410
- const artBits = artOf(mask, threshold);
1442
+ const artBits = rasters.artBits();
1411
1443
 
1412
1444
  for (const region of input.targets.regions) {
1413
1445
  const refused = refusals.get(region.name);
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The art-derived rasters one mesh measurement reads — every quantity of
3
+ * `measureMeshQuality`'s raster rows (`src/meshquality.ts`) whose only input is
4
+ * the art: the art bits at the threshold, the filled silhouette at both
5
+ * background connectivities and each one's distance transform, the
6
+ * 4-connected island labels, and the traced outline in drawing pixels.
7
+ *
8
+ * ## Why it is a value and not a cache
9
+ *
10
+ * `reduceMesh` measures one candidate per step against art that never changes
11
+ * within the call, and recomputing these per step was nearly all of its cost
12
+ * (issue #1240: 98.98 % of a 1101-step call's samples in the measurement, at
13
+ * least 40 % in functions of the art alone). So the call takes them once, at
14
+ * admission, and hands this object to every measurement it makes. There is no
15
+ * module-level cache and no hidden state: the object is created by the caller,
16
+ * passed explicitly, and dropped with the call. `measureMeshQuality` itself
17
+ * makes a fresh one per call, so its behaviour is the uncached one.
18
+ *
19
+ * Each quantity is computed on first read and kept, so a measurement that
20
+ * never reaches a row (a source that is not one loop, art under its sample
21
+ * floor) computes no more than it did before. `tally` counts every
22
+ * computation and every measurement handed the object — the count a control
23
+ * reads to see that a reduction computed each quantity once.
24
+ *
25
+ * ## What it is taken at, and the refusal
26
+ *
27
+ * The object holds the `ArtInput` it was made from. A measurement handed it
28
+ * for a different art is refused, not served stale numbers: the mask array,
29
+ * the mask's size, the threshold and the frame's scale and size are each
30
+ * compared, and a difference is `REDUCE_ART_RASTERS_MISMATCH` naming the field,
31
+ * the value the rasters were taken at and the value the input requires
32
+ * (`checkArtRasters`, `src/meshquality.ts`).
33
+ *
34
+ * Not re-exported by `src/mesh.ts`: nothing here is on `spine-rigc/mesh`.
35
+ */
36
+ import { artOf, fillEnclosed, labelIslands, MeshError, prunePolygon, squaredDistanceToSet, traceAlphaOutline } from './mesh.ts';
37
+ import type { ArtInput } from './meshquality.ts';
38
+
39
+ /** How many times each quantity was computed, and how many measurements were handed the object. */
40
+ export interface ArtRasterTally {
41
+ /** Measurements that read the object (each after its own check). */
42
+ uses: number;
43
+ artBits: number;
44
+ fills: number;
45
+ toFilled8: number;
46
+ toFilled4: number;
47
+ islands: number;
48
+ traced: number;
49
+ }
50
+
51
+ /** The traced art outline in drawing pixels, pruned — or the tracer's refusal, as its message. */
52
+ export type TracedArt = { outline: Array<[number, number]> } | { refused: string };
53
+
54
+ export interface ArtRasters {
55
+ /** The art these were taken from: its mask array, size, threshold and frame. */
56
+ readonly art: ArtInput;
57
+ readonly tally: ArtRasterTally;
58
+ /** `artOf(mask, threshold)`: 1 where alpha >= threshold. */
59
+ artBits(): Uint8Array;
60
+ /** The number of art pixels in `artBits()`. */
61
+ artCount(): number;
62
+ /** `fillEnclosed` of the art at both background connectivities, and whether they differ anywhere. */
63
+ fills(): { fill8: Uint8Array; fill4: Uint8Array; differ: boolean };
64
+ /** `squaredDistanceToSet` of the filled silhouette at one connectivity. */
65
+ toFilled(connectivity: 4 | 8): Float64Array;
66
+ /** `labelIslands` of the art: 4-connected labels, 1-based, and each island's size. */
67
+ islands(): { label: Int32Array; sizes: number[] };
68
+ /** `traceAlphaOutline` at the threshold, carried to drawing pixels by `pageScale` and pruned. */
69
+ traced(): TracedArt;
70
+ }
71
+
72
+ /**
73
+ * The rasters of one art, each computed on first read. Reads nothing at
74
+ * construction, so it may be made before the art is validated; every reader
75
+ * checks it against a validated input first.
76
+ */
77
+ export function artRastersOf(art: ArtInput): ArtRasters {
78
+ const tally: ArtRasterTally = { uses: 0, artBits: 0, fills: 0, toFilled8: 0, toFilled4: 0, islands: 0, traced: 0 };
79
+ let bits: { bits: Uint8Array; count: number } | null = null;
80
+ let fills: { fill8: Uint8Array; fill4: Uint8Array; differ: boolean } | null = null;
81
+ let toFilled8: Float64Array | null = null;
82
+ let toFilled4: Float64Array | null = null;
83
+ let islands: { label: Int32Array; sizes: number[] } | null = null;
84
+ let traced: TracedArt | null = null;
85
+ const artBitsOf = (): { bits: Uint8Array; count: number } => {
86
+ if (bits === null) {
87
+ tally.artBits++;
88
+ const b = artOf(art.mask, art.threshold);
89
+ let count = 0;
90
+ for (const bit of b) count += bit;
91
+ bits = { bits: b, count };
92
+ }
93
+ return bits;
94
+ };
95
+ const fillsOf = (): { fill8: Uint8Array; fill4: Uint8Array; differ: boolean } => {
96
+ if (fills === null) {
97
+ tally.fills++;
98
+ const { width: w, height: h } = art.mask;
99
+ const b = artBitsOf().bits;
100
+ const fill8 = fillEnclosed(b, w, h, 8).filled;
101
+ const fill4 = fillEnclosed(b, w, h, 4).filled;
102
+ let differ = false;
103
+ for (let i = 0; i < fill8.length; i++) {
104
+ if (fill8[i] !== fill4[i]) {
105
+ differ = true;
106
+ break;
107
+ }
108
+ }
109
+ fills = { fill8, fill4, differ };
110
+ }
111
+ return fills;
112
+ };
113
+ return {
114
+ art,
115
+ tally,
116
+ artBits: () => artBitsOf().bits,
117
+ artCount: () => artBitsOf().count,
118
+ fills: fillsOf,
119
+ toFilled: (connectivity) => {
120
+ const { width: w, height: h } = art.mask;
121
+ if (connectivity === 8) {
122
+ if (toFilled8 === null) {
123
+ tally.toFilled8++;
124
+ toFilled8 = squaredDistanceToSet(fillsOf().fill8, w, h);
125
+ }
126
+ return toFilled8;
127
+ }
128
+ if (toFilled4 === null) {
129
+ tally.toFilled4++;
130
+ toFilled4 = squaredDistanceToSet(fillsOf().fill4, w, h);
131
+ }
132
+ return toFilled4;
133
+ },
134
+ islands: () => {
135
+ if (islands === null) {
136
+ tally.islands++;
137
+ islands = labelIslands(artBitsOf().bits, art.mask.width, art.mask.height);
138
+ }
139
+ return islands;
140
+ },
141
+ traced: () => {
142
+ if (traced === null) {
143
+ tally.traced++;
144
+ const scale = art.frame.pageScale;
145
+ try {
146
+ const t = traceAlphaOutline(art.mask, art.threshold);
147
+ traced = { outline: prunePolygon(t.outline.map(([x, y]): [number, number] => [x / scale, y / scale])) };
148
+ } catch (err) {
149
+ if (!(err instanceof MeshError)) throw err;
150
+ traced = { refused: err.message };
151
+ }
152
+ }
153
+ return traced;
154
+ },
155
+ };
156
+ }
package/src/meshreduce.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Mesh reduction and local refinement — stage B2 of the contract in
3
3
  * docs/MESH_REDUCTION.md (issue #1221, stage B: issue #1224).
4
4
  *
5
- * One operation is exported, `reduceMesh`, and it is two operations composed:
5
+ * One operation is exported, `reduceMesh` — `reduceMeshWith` is the same
6
+ * operation over art rasters the caller made — and it is two operations
7
+ * composed:
6
8
  *
7
9
  * 1. **Refinement** (`refineRegions`) inserts vertices inside the union of the
8
10
  * declared regions and their bands until §5's `L(R)` holds on every edge
@@ -22,7 +24,9 @@
22
24
  * ## What every step is held to
23
25
  *
24
26
  * Each candidate step is measured by `measureMeshQuality` (`src/meshquality.ts`)
25
- * — no row is re-implemented here — and taken only when every row the caller's
27
+ * — no row is re-implemented here; the call reads it as `measureMeshQualityWith`
28
+ * over the art's rasters taken once at admission (`src/meshrasters.ts`, issue
29
+ * #1240), which returns the same report — and taken only when every row the caller's
26
30
  * contract requires is `pass`: the art fit at `targets.artFit`, the boundary
27
31
  * deviation from the source hull at `targets.maxBoundaryDeviation`, the minimum
28
32
  * angle when declared, orientation and degeneracy at 0, and every region's
@@ -71,8 +75,6 @@ import {
71
75
  distanceToSegment,
72
76
  earClip,
73
77
  findSelfIntersection,
74
- labelIslands,
75
- artOf,
76
78
  MeshError,
77
79
  MeshReductionError,
78
80
  meshEdges,
@@ -85,7 +87,7 @@ import {
85
87
  import {
86
88
  BAND_CONTACT_TOLERANCE,
87
89
  edgeIsHeldByRegion,
88
- measureMeshQuality,
90
+ measureMeshQualityWith,
89
91
  type AttachmentRef,
90
92
  type ArtFitBounds,
91
93
  type CandidateReport,
@@ -101,6 +103,7 @@ import {
101
103
  type SourceMesh,
102
104
  type Termination,
103
105
  } from './meshquality.ts';
106
+ import { artRastersOf, type ArtRasters } from './meshrasters.ts';
104
107
 
105
108
  // ---------------------------------------------------------------------------
106
109
  // the result
@@ -711,6 +714,8 @@ interface Run {
711
714
  protectedVertices: Set<number>;
712
715
  protectedEdges: Array<[number, number]>;
713
716
  sourceEdges: Set<string>;
717
+ /** The art's rasters, taken once for the call and read by every measurement in it (issue #1240). */
718
+ rasters: ArtRasters;
714
719
  }
715
720
 
716
721
  /** A measurement of a canonical mesh against the result's full contract. */
@@ -731,7 +736,7 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string): MeshQual
731
736
  boneOrder: input.boneOrder,
732
737
  preset: input.preset,
733
738
  };
734
- return measureMeshQuality(measureInput);
739
+ return measureMeshQualityWith(measureInput, run.rasters);
735
740
  }
736
741
 
737
742
  /** The order constraints are named in when several fail on one step: structure, then shape, then art, then density. */
@@ -1167,26 +1172,50 @@ function tryRemoval(run: Run, v: number): string | null {
1167
1172
  */
1168
1173
  export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
1169
1174
  validateReduction(input);
1175
+ return reduceValidated(input, artRastersOf(input.art));
1176
+ }
1177
+
1178
+ /**
1179
+ * `reduceMesh` over art rasters the caller made (`artRastersOf`,
1180
+ * `src/meshrasters.ts`) — the result is `reduceMesh`'s for the same input, byte
1181
+ * for byte, and every measurement of the call reads the one object, so its
1182
+ * `tally` is the count of what the call computed. Rasters taken from another
1183
+ * art are refused at admission (`REDUCE_ART_RASTERS_MISMATCH`).
1184
+ *
1185
+ * Internal: it is on `spine-rigc/mesh` only because that entry re-exports this
1186
+ * module with `export *`, and a symbol that is merely exported is not promised
1187
+ * (RELEASING.md, *The import surface*).
1188
+ */
1189
+ export function reduceMeshWith(input: MeshReductionInput, rasters: ArtRasters): MeshReductionResult {
1190
+ validateReduction(input);
1191
+ return reduceValidated(input, rasters);
1192
+ }
1193
+
1194
+ /** The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in `rasters`. */
1195
+ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters): MeshReductionResult {
1170
1196
  const who = `attachment ${nameOf(input.attachment)}`;
1171
1197
  const src = input.source;
1172
1198
  const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
1173
1199
 
1174
1200
  // The source, measured against its own admissibility bounds (correction 3) — this is also what refuses a
1175
1201
  // malformed art, mesh, region list or sample floor, by measureMeshQuality's own codes.
1176
- const admit = measureMeshQuality({
1177
- id: 'source',
1178
- attachment: input.attachment,
1179
- art: input.art,
1180
- source: src,
1181
- targets: { artFit: input.sourceBounds, maxBoundaryDeviation: null, regions: input.targets.regions },
1182
- referenceHull: null,
1183
- minArtSamples: input.minArtSamples,
1184
- regionArtSamples: input.regionArtSamples,
1185
- protect: input.protect,
1186
- influences: input.influences,
1187
- boneOrder: input.boneOrder,
1188
- preset: input.preset,
1189
- });
1202
+ const admit = measureMeshQualityWith(
1203
+ {
1204
+ id: 'source',
1205
+ attachment: input.attachment,
1206
+ art: input.art,
1207
+ source: src,
1208
+ targets: { artFit: input.sourceBounds, maxBoundaryDeviation: null, regions: input.targets.regions },
1209
+ referenceHull: null,
1210
+ minArtSamples: input.minArtSamples,
1211
+ regionArtSamples: input.regionArtSamples,
1212
+ protect: input.protect,
1213
+ influences: input.influences,
1214
+ boneOrder: input.boneOrder,
1215
+ preset: input.preset,
1216
+ },
1217
+ rasters,
1218
+ );
1190
1219
  const effective = effectiveOf(input, admit.effective, sourceHull);
1191
1220
  const sourceCounts = admit.sourceCounts;
1192
1221
  const noMesh = (termination: Termination): MeshReductionResult => ({
@@ -1215,7 +1244,7 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
1215
1244
  );
1216
1245
  }
1217
1246
  }
1218
- checkIslands(input, who);
1247
+ checkIslands(input, who, rasters);
1219
1248
  checkDeformBeforeWork(input);
1220
1249
 
1221
1250
  const boneRank = new Map((input.boneOrder ?? []).map((b, i) => [b, i]));
@@ -1243,6 +1272,7 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
1243
1272
  keyed: keyedSourceVertices(input),
1244
1273
  ...protectionOf(input, work, sourceEdges),
1245
1274
  sourceEdges,
1275
+ rasters,
1246
1276
  };
1247
1277
 
1248
1278
  const refined = refineRegions(run);
@@ -1268,11 +1298,10 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
1268
1298
  }
1269
1299
 
1270
1300
  /** §4: full coverage asked of a source that touches no pixel of some art island is refused, never met by deleting art. */
1271
- function checkIslands(input: MeshReductionInput, who: string): void {
1301
+ function checkIslands(input: MeshReductionInput, who: string, rasters: ArtRasters): void {
1272
1302
  if (input.targets.artFit.minCoverage !== 1) return;
1273
- const { mask, threshold, frame } = input.art;
1274
- const bits = artOf(mask, threshold);
1275
- const { label, sizes } = labelIslands(bits, mask.width, mask.height);
1303
+ const { mask, frame } = input.art;
1304
+ const { label, sizes } = rasters.islands();
1276
1305
  const onGrid = input.source.points.map(([x, y]): [number, number] => [x * frame.pageScale, y * frame.pageScale]);
1277
1306
  const covered = rasteriseTriangles(onGrid, input.source.triangles, mask.width, mask.height);
1278
1307
  const touched = new Set<number>();