spine-rigc 2.20.0 → 2.20.2
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 +1 -1
- package/src/mesh.ts +84 -23
- package/src/meshquality.ts +275 -81
- package/src/meshrasters.ts +621 -0
- package/src/meshreduce.ts +71 -25
|
@@ -0,0 +1,621 @@
|
|
|
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, distancePassesOf, fillEnclosed, labelIslands, MeshError, pixelCentreInTriangle, prunePolygon, squaredDistanceToSet, traceAlphaOutline, triangleRasterBox } 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
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Overshoot and holes against one filled silhouette: the covered centre
|
|
160
|
+
* outside it furthest from it (the first in pixel order among equals; −1 when
|
|
161
|
+
* none) with its squared distance, and the covered centres inside it that are
|
|
162
|
+
* not art — how many, and the first in pixel order (−1 when none).
|
|
163
|
+
*/
|
|
164
|
+
export interface SilhouetteReading {
|
|
165
|
+
overAt: number;
|
|
166
|
+
overSq: number;
|
|
167
|
+
holes: number;
|
|
168
|
+
holeAt: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* What one step's coverage reads, from either path: the covered pixel centres,
|
|
173
|
+
* their distance transform, the covered art pixels, whether any centre is
|
|
174
|
+
* covered, the uncovered art pixel furthest from the covered set (the first in
|
|
175
|
+
* pixel order among equals; −1 when none) with its squared distance, the
|
|
176
|
+
* silhouette reading at either connectivity, and the 4-connected art islands
|
|
177
|
+
* the coverage touches — how many, and the second-smallest label among them
|
|
178
|
+
* (0 when fewer than two).
|
|
179
|
+
*/
|
|
180
|
+
export interface CoverageReading {
|
|
181
|
+
covered: Uint8Array;
|
|
182
|
+
toCovered: Float64Array;
|
|
183
|
+
coveredArt: number;
|
|
184
|
+
anyCovered: boolean;
|
|
185
|
+
undercut: { at: number; sq: number };
|
|
186
|
+
silhouette(connectivity: 4 | 8): SilhouetteReading;
|
|
187
|
+
islandsTouched: number;
|
|
188
|
+
secondIsland: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** How a step-rasters object came by each reading, and what it redid. */
|
|
192
|
+
export interface StepRasterTally {
|
|
193
|
+
/** Coverage readings handed out. */
|
|
194
|
+
readings: number;
|
|
195
|
+
/** Readings built from nothing: the first, and any whose triangles differ from the last in more than they share. */
|
|
196
|
+
rebuilt: number;
|
|
197
|
+
/** Readings carried from the last one by the triangles that differ. */
|
|
198
|
+
carried: number;
|
|
199
|
+
/** Triangles drawn (taken away or added) by carried readings. */
|
|
200
|
+
trianglesDrawn: number;
|
|
201
|
+
/** Triangles a carried reading did not draw because the last reading held them already. */
|
|
202
|
+
trianglesKept: number;
|
|
203
|
+
/** Pixel centres whose coverage changed between readings. */
|
|
204
|
+
pixelsFlipped: number;
|
|
205
|
+
/** Columns and rows of the distance transform carried readings redid. */
|
|
206
|
+
columnsRedone: number;
|
|
207
|
+
rowsRedone: number;
|
|
208
|
+
/** Outline deviations measured, and handed back because the outline and the polygon it is read against equal the last ones. */
|
|
209
|
+
outlinesMeasured: number;
|
|
210
|
+
outlinesReused: number;
|
|
211
|
+
/** Point-to-edge distance lists an outline measurement asked for, and how each was had: handed back whole, carried edge by edge from the last list of the same point, or computed. */
|
|
212
|
+
distanceLists: number;
|
|
213
|
+
distanceListsReused: number;
|
|
214
|
+
distanceListsCarried: number;
|
|
215
|
+
/** Edges a carried list took from the last list of its point, and edges it computed because the edge is new. */
|
|
216
|
+
edgesCarried: number;
|
|
217
|
+
edgesComputed: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A fault planted on purpose in the carried path, for the mesh-quality
|
|
222
|
+
* controls to show they catch it: one added triangle not drawn, one changed
|
|
223
|
+
* column or row of the distance transform not redone, or an outline reading
|
|
224
|
+
* reused when only the vertex counts match. `reduceMesh` plants none.
|
|
225
|
+
*/
|
|
226
|
+
export type StepRasterPlant = 'skip-a-triangle' | 'skip-a-column' | 'skip-a-row' | 'stale-outline' | 'skip-an-edge';
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* One outline slot's carried distances (`src/meshquality.ts`, `hausdorff`): a
|
|
230
|
+
* directed Hausdorff reading asks, for points along one polygon, the distance
|
|
231
|
+
* to every edge of the other, and those lists are kept from the last reading
|
|
232
|
+
* of the slot by the point they were asked for, keyed by its exact
|
|
233
|
+
* coordinates.
|
|
234
|
+
*
|
|
235
|
+
* - `toAgainst` — lists of distances to the edges of the polygon the outline
|
|
236
|
+
* is read against (fixed for a reduction). Kept while that polygon is equal;
|
|
237
|
+
* an entry not asked for by the last two readings is dropped.
|
|
238
|
+
* - `toOutline` — lists of distances to the outline's own edges, which a step
|
|
239
|
+
* changes. Kept for the last reading's outline only, with its edges
|
|
240
|
+
* (`outlineEdges`, keyed by both ends), so the next reading copies each
|
|
241
|
+
* distance whose edge it still has and computes the rest.
|
|
242
|
+
*/
|
|
243
|
+
export interface OutlineMemo {
|
|
244
|
+
against: Float64Array | null;
|
|
245
|
+
toAgainst: Map<string, { list: Float64Array; used: number }>;
|
|
246
|
+
outlineEdges: Map<string, number> | null;
|
|
247
|
+
toOutline: Map<string, Float64Array>;
|
|
248
|
+
readings: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The per-step state of one reduction (issue #1246): the last coverage
|
|
253
|
+
* reading and the last outline deviations, so the next measurement redoes only
|
|
254
|
+
* what its mesh changed. Like `ArtRasters` it is a value made by the caller,
|
|
255
|
+
* passed explicitly and dropped with the call; nothing is kept at module level.
|
|
256
|
+
*
|
|
257
|
+
* ## Why every reading equals the full one
|
|
258
|
+
*
|
|
259
|
+
* A reduction step changes the triangles of one vertex's link and nothing
|
|
260
|
+
* else, yet the full measurement redraws every triangle and redoes the whole
|
|
261
|
+
* distance transform. Here each quantity is carried from the last reading by a
|
|
262
|
+
* rule under which it is the same function of the same input:
|
|
263
|
+
*
|
|
264
|
+
* - **Coverage** is held as a count per pixel centre of the triangles whose
|
|
265
|
+
* centre test (`triangleRasterBox`, `pixelCentreInTriangle` — the rule
|
|
266
|
+
* `rasteriseTriangles` reads) holds there. The triangles are compared with
|
|
267
|
+
* the last reading's as a multiset keyed by their three corners' exact
|
|
268
|
+
* coordinates in the order given (a zero's sign included); a triangle taken
|
|
269
|
+
* away is drawn with −1 and one added with +1, so every count is the exact
|
|
270
|
+
* number of triangles covering that centre, and a centre is covered exactly
|
|
271
|
+
* when its count is positive — `rasteriseTriangles`'s union.
|
|
272
|
+
* - **The distance transform** is `squaredDistanceToSet`'s two passes
|
|
273
|
+
* (`distancePassesOf`). A column's pass reads that column of the coverage
|
|
274
|
+
* alone, so only columns holding a flipped centre are redone; a row's pass
|
|
275
|
+
* reads that row of the column results alone, so only rows where a redone
|
|
276
|
+
* column's value changed are redone. Every other value is the output of the
|
|
277
|
+
* same pass over the same input.
|
|
278
|
+
* - **Covered art and islands** are integer counts updated at each flipped
|
|
279
|
+
* centre: art pixels covered, centres covered, and covered pixels per island
|
|
280
|
+
* label; the islands touched are the labels whose count is positive.
|
|
281
|
+
* - **Outline deviations** (`outline`) are handed back only when the outline
|
|
282
|
+
* and the polygon it is read against are equal, coordinate by coordinate
|
|
283
|
+
* with a zero's sign, to the last ones of that slot — the same pure function
|
|
284
|
+
* of the same input.
|
|
285
|
+
*
|
|
286
|
+
* A reading whose triangles differ from the last in more triangles than they
|
|
287
|
+
* share is built from nothing, as the first one is; which way a reading is
|
|
288
|
+
* built changes its cost and never its values.
|
|
289
|
+
*/
|
|
290
|
+
export interface StepRasters {
|
|
291
|
+
readonly rasters: ArtRasters;
|
|
292
|
+
readonly tally: StepRasterTally;
|
|
293
|
+
/** The coverage reading of these triangles over the drawing-pixel `points`; its arrays are valid until the next reading. */
|
|
294
|
+
coverage(points: Array<[number, number]>, triangles: number[]): CoverageReading;
|
|
295
|
+
/** The fault planted for a control, or null. */
|
|
296
|
+
readonly plant: StepRasterPlant | null;
|
|
297
|
+
/**
|
|
298
|
+
* `measure(memo)`'s value, or the last one of `slot` when `outline` and
|
|
299
|
+
* `against` equal the last ones exactly; `memo` is the slot's carried
|
|
300
|
+
* distances, which `measure` reads and keeps current.
|
|
301
|
+
*/
|
|
302
|
+
outline<T>(slot: string, outline: ReadonlyArray<readonly [number, number]>, against: ReadonlyArray<readonly [number, number]>, measure: (memo: OutlineMemo) => T): T;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** A coordinate as a key: its shortest round-trip decimal, with a negative zero told from a positive one. */
|
|
306
|
+
function coordKey(v: number): string {
|
|
307
|
+
return v === 0 && 1 / v < 0 ? '-0' : String(v);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Equal coordinate by coordinate, a zero's sign included. */
|
|
311
|
+
function samePolygon(a: ReadonlyArray<readonly [number, number]>, b: Float64Array): boolean {
|
|
312
|
+
if (a.length * 2 !== b.length) return false;
|
|
313
|
+
for (let i = 0; i < a.length; i++) if (!Object.is(a[i][0], b[i * 2]) || !Object.is(a[i][1], b[i * 2 + 1])) return false;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function flat(p: ReadonlyArray<readonly [number, number]>): Float64Array {
|
|
318
|
+
const out = new Float64Array(p.length * 2);
|
|
319
|
+
for (let i = 0; i < p.length; i++) {
|
|
320
|
+
out[i * 2] = p[i][0];
|
|
321
|
+
out[i * 2 + 1] = p[i][1];
|
|
322
|
+
}
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
type Corners = [readonly [number, number], readonly [number, number], readonly [number, number]];
|
|
327
|
+
|
|
328
|
+
interface HeldTriangles {
|
|
329
|
+
corners: Corners;
|
|
330
|
+
n: number;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface CoverageState {
|
|
334
|
+
held: Map<string, HeldTriangles>;
|
|
335
|
+
/** Triangles covering each pixel centre. */
|
|
336
|
+
count: Int32Array;
|
|
337
|
+
covered: Uint8Array;
|
|
338
|
+
/** The column pass of the distance transform, and the transform. */
|
|
339
|
+
columns: Float64Array;
|
|
340
|
+
dist: Float64Array;
|
|
341
|
+
coveredArt: number;
|
|
342
|
+
coveredTotal: number;
|
|
343
|
+
/** Covered pixels per island label (index 0 unused). */
|
|
344
|
+
islandHits: Int32Array;
|
|
345
|
+
islandsTouched: number;
|
|
346
|
+
/** Art pixels not covered. */
|
|
347
|
+
uncoveredArt: Set<number>;
|
|
348
|
+
/** Per connectivity, once read: covered centres outside the filled silhouette, and covered centres inside it that are not art. */
|
|
349
|
+
silhouettes: Map<4 | 8, { outside: Set<number>; holes: Set<number> }>;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Step rasters over `rasters`' art. Reads nothing until the first reading. */
|
|
353
|
+
export function stepRastersOf(rasters: ArtRasters, plant: StepRasterPlant | null = null): StepRasters {
|
|
354
|
+
const tally: StepRasterTally = {
|
|
355
|
+
readings: 0,
|
|
356
|
+
rebuilt: 0,
|
|
357
|
+
carried: 0,
|
|
358
|
+
trianglesDrawn: 0,
|
|
359
|
+
trianglesKept: 0,
|
|
360
|
+
pixelsFlipped: 0,
|
|
361
|
+
columnsRedone: 0,
|
|
362
|
+
rowsRedone: 0,
|
|
363
|
+
outlinesMeasured: 0,
|
|
364
|
+
outlinesReused: 0,
|
|
365
|
+
distanceLists: 0,
|
|
366
|
+
distanceListsReused: 0,
|
|
367
|
+
distanceListsCarried: 0,
|
|
368
|
+
edgesCarried: 0,
|
|
369
|
+
edgesComputed: 0,
|
|
370
|
+
};
|
|
371
|
+
const { width: w, height: h } = rasters.art.mask;
|
|
372
|
+
let state: CoverageState | null = null;
|
|
373
|
+
const outlines = new Map<string, { outline: Float64Array; against: Float64Array; value: unknown }>();
|
|
374
|
+
const memos = new Map<string, OutlineMemo>();
|
|
375
|
+
|
|
376
|
+
/** Add `sign` to the count of every centre the triangle covers, pushing each such centre to `touched`. */
|
|
377
|
+
const draw = (count: Int32Array, corners: Corners, sign: number, touched: number[] | null): void => {
|
|
378
|
+
const [a, b, c] = corners;
|
|
379
|
+
const box = triangleRasterBox(a, b, c, w, h);
|
|
380
|
+
if (box === null) return;
|
|
381
|
+
for (let y = box.minY; y <= box.maxY; y++) {
|
|
382
|
+
for (let x = box.minX; x <= box.maxX; x++) {
|
|
383
|
+
if (!pixelCentreInTriangle(x, y, a, b, c, box.orient)) continue;
|
|
384
|
+
count[y * w + x] += sign;
|
|
385
|
+
if (touched !== null) touched.push(y * w + x);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const readingOf = (s: CoverageState): CoverageReading => {
|
|
391
|
+
let second = 0;
|
|
392
|
+
if (s.islandsTouched >= 2) {
|
|
393
|
+
let seen = 0;
|
|
394
|
+
for (let l = 1; l < s.islandHits.length; l++) {
|
|
395
|
+
if (s.islandHits[l] > 0 && ++seen === 2) {
|
|
396
|
+
second = l;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// The worst of a set by the full scan's rule: the largest value, the smallest index among equals.
|
|
402
|
+
const worstOf = (set: Set<number>, value: Float64Array): { at: number; sq: number } => {
|
|
403
|
+
let at = -1;
|
|
404
|
+
let sq = 0;
|
|
405
|
+
for (const i of set) {
|
|
406
|
+
const v = value[i];
|
|
407
|
+
if (at === -1 || v > sq || (v === sq && i < at)) {
|
|
408
|
+
at = i;
|
|
409
|
+
sq = v;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { at, sq };
|
|
413
|
+
};
|
|
414
|
+
const silhouette = (connectivity: 4 | 8): SilhouetteReading => {
|
|
415
|
+
let sets = s.silhouettes.get(connectivity);
|
|
416
|
+
if (sets === undefined) {
|
|
417
|
+
const fills = rasters.fills();
|
|
418
|
+
const filled = connectivity === 8 ? fills.fill8 : fills.fill4;
|
|
419
|
+
const artBits = rasters.artBits();
|
|
420
|
+
sets = { outside: new Set(), holes: new Set() };
|
|
421
|
+
for (let i = 0; i < s.covered.length; i++) {
|
|
422
|
+
if (!s.covered[i]) continue;
|
|
423
|
+
if (!filled[i]) sets.outside.add(i);
|
|
424
|
+
else if (!artBits[i]) sets.holes.add(i);
|
|
425
|
+
}
|
|
426
|
+
s.silhouettes.set(connectivity, sets);
|
|
427
|
+
}
|
|
428
|
+
const over = worstOf(sets.outside, rasters.toFilled(connectivity));
|
|
429
|
+
let holeAt = -1;
|
|
430
|
+
for (const i of sets.holes) if (holeAt === -1 || i < holeAt) holeAt = i;
|
|
431
|
+
return { overAt: over.at, overSq: over.sq, holes: sets.holes.size, holeAt };
|
|
432
|
+
};
|
|
433
|
+
return {
|
|
434
|
+
covered: s.covered,
|
|
435
|
+
toCovered: s.dist,
|
|
436
|
+
coveredArt: s.coveredArt,
|
|
437
|
+
anyCovered: s.coveredTotal > 0,
|
|
438
|
+
undercut: worstOf(s.uncoveredArt, s.dist),
|
|
439
|
+
silhouette,
|
|
440
|
+
islandsTouched: s.islandsTouched,
|
|
441
|
+
secondIsland: second,
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const rebuild = (held: Map<string, HeldTriangles>): CoverageState => {
|
|
446
|
+
tally.rebuilt++;
|
|
447
|
+
const artBits = rasters.artBits();
|
|
448
|
+
const { label, sizes } = rasters.islands();
|
|
449
|
+
const count = new Int32Array(w * h);
|
|
450
|
+
for (const { corners, n } of held.values()) for (let k = 0; k < n; k++) draw(count, corners, 1, null);
|
|
451
|
+
const covered = new Uint8Array(w * h);
|
|
452
|
+
const islandHits = new Int32Array(sizes.length + 1);
|
|
453
|
+
let coveredArt = 0;
|
|
454
|
+
let coveredTotal = 0;
|
|
455
|
+
let islandsTouched = 0;
|
|
456
|
+
const uncoveredArt = new Set<number>();
|
|
457
|
+
for (let i = 0; i < covered.length; i++) {
|
|
458
|
+
if (count[i] <= 0) {
|
|
459
|
+
if (artBits[i]) uncoveredArt.add(i);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
covered[i] = 1;
|
|
463
|
+
coveredTotal++;
|
|
464
|
+
if (artBits[i]) coveredArt++;
|
|
465
|
+
if (label[i] && islandHits[label[i]]++ === 0) islandsTouched++;
|
|
466
|
+
}
|
|
467
|
+
const passes = distancePassesOf(w, h);
|
|
468
|
+
const columns = new Float64Array(w * h);
|
|
469
|
+
for (let x = 0; x < w; x++) {
|
|
470
|
+
const out = passes.column(covered, x);
|
|
471
|
+
for (let y = 0; y < h; y++) columns[y * w + x] = out[y];
|
|
472
|
+
}
|
|
473
|
+
const dist = new Float64Array(w * h);
|
|
474
|
+
for (let y = 0; y < h; y++) {
|
|
475
|
+
const out = passes.row(columns, y);
|
|
476
|
+
for (let x = 0; x < w; x++) dist[y * w + x] = out[x];
|
|
477
|
+
}
|
|
478
|
+
return { held, count, covered, columns, dist, coveredArt, coveredTotal, islandHits, islandsTouched, uncoveredArt, silhouettes: new Map() };
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/** Carry `s` to the triangles `held` by drawing the ones that differ, then redo the distance transform where it changed. */
|
|
482
|
+
const carry = (s: CoverageState, held: Map<string, HeldTriangles>, removed: Corners[], added: Corners[]): void => {
|
|
483
|
+
tally.carried++;
|
|
484
|
+
tally.trianglesDrawn += removed.length + added.length;
|
|
485
|
+
const touched: number[] = [];
|
|
486
|
+
for (const corners of removed) draw(s.count, corners, -1, touched);
|
|
487
|
+
added.forEach((corners, k) => {
|
|
488
|
+
if (plant === 'skip-a-triangle' && k === 0) return;
|
|
489
|
+
draw(s.count, corners, 1, touched);
|
|
490
|
+
});
|
|
491
|
+
s.held = held;
|
|
492
|
+
// Flip the centres whose count crossed zero, keeping the integer counts with them.
|
|
493
|
+
const artBits = rasters.artBits();
|
|
494
|
+
const { label } = rasters.islands();
|
|
495
|
+
const fills = s.silhouettes.size > 0 ? rasters.fills() : null;
|
|
496
|
+
const dirtyColumns = new Uint8Array(w);
|
|
497
|
+
for (const i of touched) {
|
|
498
|
+
const now = s.count[i] > 0 ? 1 : 0;
|
|
499
|
+
if (now === s.covered[i]) continue;
|
|
500
|
+
s.covered[i] = now;
|
|
501
|
+
tally.pixelsFlipped++;
|
|
502
|
+
dirtyColumns[i % w] = 1;
|
|
503
|
+
const d = now === 1 ? 1 : -1;
|
|
504
|
+
s.coveredTotal += d;
|
|
505
|
+
if (artBits[i]) {
|
|
506
|
+
s.coveredArt += d;
|
|
507
|
+
if (now === 1) s.uncoveredArt.delete(i);
|
|
508
|
+
else s.uncoveredArt.add(i);
|
|
509
|
+
}
|
|
510
|
+
for (const [connectivity, sets] of s.silhouettes) {
|
|
511
|
+
if (now === 0) {
|
|
512
|
+
sets.outside.delete(i);
|
|
513
|
+
sets.holes.delete(i);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const filled = connectivity === 8 ? fills!.fill8 : fills!.fill4;
|
|
517
|
+
if (!filled[i]) sets.outside.add(i);
|
|
518
|
+
else if (!artBits[i]) sets.holes.add(i);
|
|
519
|
+
}
|
|
520
|
+
const l = label[i];
|
|
521
|
+
if (l) {
|
|
522
|
+
const before = s.islandHits[l];
|
|
523
|
+
s.islandHits[l] += d;
|
|
524
|
+
if (before === 0) s.islandsTouched++;
|
|
525
|
+
else if (s.islandHits[l] === 0) s.islandsTouched--;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Redo the columns holding a flipped centre, then the rows where a column value changed.
|
|
529
|
+
const passes = distancePassesOf(w, h);
|
|
530
|
+
const dirtyRows = new Uint8Array(h);
|
|
531
|
+
let skipColumn = plant === 'skip-a-column';
|
|
532
|
+
for (let x = 0; x < w; x++) {
|
|
533
|
+
if (!dirtyColumns[x]) continue;
|
|
534
|
+
if (skipColumn) {
|
|
535
|
+
skipColumn = false;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
tally.columnsRedone++;
|
|
539
|
+
const out = passes.column(s.covered, x);
|
|
540
|
+
for (let y = 0; y < h; y++) {
|
|
541
|
+
if (Object.is(s.columns[y * w + x], out[y])) continue;
|
|
542
|
+
s.columns[y * w + x] = out[y];
|
|
543
|
+
dirtyRows[y] = 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
let skipRow = plant === 'skip-a-row';
|
|
547
|
+
for (let y = 0; y < h; y++) {
|
|
548
|
+
if (!dirtyRows[y]) continue;
|
|
549
|
+
if (skipRow) {
|
|
550
|
+
skipRow = false;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
tally.rowsRedone++;
|
|
554
|
+
const out = passes.row(s.columns, y);
|
|
555
|
+
for (let x = 0; x < w; x++) s.dist[y * w + x] = out[x];
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
rasters,
|
|
561
|
+
tally,
|
|
562
|
+
coverage: (points, triangles) => {
|
|
563
|
+
tally.readings++;
|
|
564
|
+
const held = new Map<string, HeldTriangles>();
|
|
565
|
+
for (let t = 0; t + 2 < triangles.length; t += 3) {
|
|
566
|
+
const corners: Corners = [points[triangles[t]], points[triangles[t + 1]], points[triangles[t + 2]]];
|
|
567
|
+
const key = `${coordKey(corners[0][0])},${coordKey(corners[0][1])},${coordKey(corners[1][0])},${coordKey(corners[1][1])},${coordKey(corners[2][0])},${coordKey(corners[2][1])}`;
|
|
568
|
+
const entry = held.get(key);
|
|
569
|
+
if (entry === undefined) held.set(key, { corners, n: 1 });
|
|
570
|
+
else entry.n++;
|
|
571
|
+
}
|
|
572
|
+
if (state === null) {
|
|
573
|
+
state = rebuild(held);
|
|
574
|
+
return readingOf(state);
|
|
575
|
+
}
|
|
576
|
+
// The multiset difference against the last reading's triangles.
|
|
577
|
+
const removed: Corners[] = [];
|
|
578
|
+
const added: Corners[] = [];
|
|
579
|
+
let kept = 0;
|
|
580
|
+
for (const [key, { corners, n }] of held) {
|
|
581
|
+
const was = state.held.get(key)?.n ?? 0;
|
|
582
|
+
kept += Math.min(was, n);
|
|
583
|
+
for (let k = was; k < n; k++) added.push(corners);
|
|
584
|
+
}
|
|
585
|
+
for (const [key, { corners, n }] of state.held) {
|
|
586
|
+
const now = held.get(key)?.n ?? 0;
|
|
587
|
+
for (let k = now; k < n; k++) removed.push(corners);
|
|
588
|
+
}
|
|
589
|
+
if (removed.length + added.length > kept) {
|
|
590
|
+
state = rebuild(held);
|
|
591
|
+
return readingOf(state);
|
|
592
|
+
}
|
|
593
|
+
tally.trianglesKept += kept;
|
|
594
|
+
carry(state, held, removed, added);
|
|
595
|
+
return readingOf(state);
|
|
596
|
+
},
|
|
597
|
+
plant,
|
|
598
|
+
outline: <T>(slot: string, outline: ReadonlyArray<readonly [number, number]>, against: ReadonlyArray<readonly [number, number]>, measure: (memo: OutlineMemo) => T): T => {
|
|
599
|
+
const last = outlines.get(slot);
|
|
600
|
+
if (last !== undefined) {
|
|
601
|
+
const same =
|
|
602
|
+
plant === 'stale-outline'
|
|
603
|
+
? last.outline.length === outline.length * 2 && last.against.length === against.length * 2
|
|
604
|
+
: samePolygon(outline, last.outline) && samePolygon(against, last.against);
|
|
605
|
+
if (same) {
|
|
606
|
+
tally.outlinesReused++;
|
|
607
|
+
return last.value as T;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
tally.outlinesMeasured++;
|
|
611
|
+
let memo = memos.get(slot);
|
|
612
|
+
if (memo === undefined) {
|
|
613
|
+
memo = { against: null, toAgainst: new Map(), outlineEdges: null, toOutline: new Map(), readings: 0 };
|
|
614
|
+
memos.set(slot, memo);
|
|
615
|
+
}
|
|
616
|
+
const value = measure(memo);
|
|
617
|
+
outlines.set(slot, { outline: flat(outline), against: flat(against), value });
|
|
618
|
+
return value;
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|