rig-c 0.0.0-stage → 2.20.4

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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1188 -0
  185. package/src/meshquality.ts +2042 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1425 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
@@ -0,0 +1,944 @@
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
+ /** Region pixel sets (`regionPixels`) computed, and handed back because the polygon equals one already read. */
219
+ regionSetsComputed: number;
220
+ regionSetsReused: number;
221
+ /** Region polygons checked for a self-intersection (`polygonCrossing`), and checks handed back for an equal polygon. */
222
+ polygonChecksComputed: number;
223
+ polygonChecksReused: number;
224
+ /** Region edge readings (`regionEdges`): an edge's answers carried from the last reading of its region, or computed. */
225
+ regionEdgesCarried: number;
226
+ regionEdgesComputed: number;
227
+ /** Fill readings (`regionFill`): the region pixels in the hull handed back for an equal hull, carried by the hull edges that changed, or measured from nothing; and pixels measured again inside a changed hull's box. */
228
+ fillHullsReused: number;
229
+ fillHullsCarried: number;
230
+ fillHullsMeasured: number;
231
+ fillHullPixelsMeasured: number;
232
+ /** Fill readings whose nearest-vertex distances were built from nothing, and pixels carried or measured again in the others. */
233
+ fillNearestRebuilt: number;
234
+ fillNearestCarried: number;
235
+ fillNearestMeasured: number;
236
+ }
237
+
238
+ /**
239
+ * A fault planted on purpose in the carried path, for the mesh-quality
240
+ * controls to show they catch it: one added triangle not drawn, one changed
241
+ * column or row of the distance transform not redone, an outline reading
242
+ * reused when only the vertex counts match, or a region's art pixels handed
243
+ * back, or a region polygon's self-intersection check handed back, for a
244
+ * polygon that only has as many vertices as one read before, or a region
245
+ * edge's answers carried from an edge that shares only its first end,
246
+ * the region pixels near a changed hull edge not measured again, or a pixel's
247
+ * nearest vertex kept after that vertex was removed. `reduceMesh` plants none.
248
+ */
249
+ export type StepRasterPlant = 'skip-a-triangle' | 'skip-a-column' | 'skip-a-row' | 'stale-outline' | 'skip-an-edge' | 'stale-region' | 'stale-polygon-check' | 'stale-region-edge' | 'stale-fill-hull' | 'stale-fill-nearest';
250
+
251
+ /**
252
+ * One outline slot's carried distances (`src/meshquality.ts`, `hausdorff`): a
253
+ * directed Hausdorff reading asks, for points along one polygon, the distance
254
+ * to every edge of the other, and those lists are kept from the last reading
255
+ * of the slot by the point they were asked for, keyed by its exact
256
+ * coordinates.
257
+ *
258
+ * - `toAgainst` — lists of distances to the edges of the polygon the outline
259
+ * is read against (fixed for a reduction). Kept while that polygon is equal;
260
+ * an entry not asked for by the last two readings is dropped.
261
+ * - `toOutline` — lists of distances to the outline's own edges, which a step
262
+ * changes. Kept for the last reading's outline only, with its edges
263
+ * (`outlineEdges`, keyed by both ends), so the next reading copies each
264
+ * distance whose edge it still has and computes the rest.
265
+ */
266
+ export interface OutlineMemo {
267
+ against: Float64Array | null;
268
+ toAgainst: Map<string, { list: Float64Array; used: number }>;
269
+ outlineEdges: Map<string, number> | null;
270
+ toOutline: Map<string, Float64Array>;
271
+ readings: number;
272
+ }
273
+
274
+ /**
275
+ * The per-step state of one reduction (issue #1246): the last coverage
276
+ * reading and the last outline deviations, so the next measurement redoes only
277
+ * what its mesh changed. Like `ArtRasters` it is a value made by the caller,
278
+ * passed explicitly and dropped with the call; nothing is kept at module level.
279
+ *
280
+ * ## Why every reading equals the full one
281
+ *
282
+ * A reduction step changes the triangles of one vertex's link and nothing
283
+ * else, yet the full measurement redraws every triangle and redoes the whole
284
+ * distance transform. Here each quantity is carried from the last reading by a
285
+ * rule under which it is the same function of the same input:
286
+ *
287
+ * - **Coverage** is held as a count per pixel centre of the triangles whose
288
+ * centre test (`triangleRasterBox`, `pixelCentreInTriangle` — the rule
289
+ * `rasteriseTriangles` reads) holds there. The triangles are compared with
290
+ * the last reading's as a multiset keyed by their three corners' exact
291
+ * coordinates in the order given (a zero's sign included); a triangle taken
292
+ * away is drawn with −1 and one added with +1, so every count is the exact
293
+ * number of triangles covering that centre, and a centre is covered exactly
294
+ * when its count is positive — `rasteriseTriangles`'s union.
295
+ * - **The distance transform** is `squaredDistanceToSet`'s two passes
296
+ * (`distancePassesOf`). A column's pass reads that column of the coverage
297
+ * alone, so only columns holding a flipped centre are redone; a row's pass
298
+ * reads that row of the column results alone, so only rows where a redone
299
+ * column's value changed are redone. Every other value is the output of the
300
+ * same pass over the same input.
301
+ * - **Covered art and islands** are integer counts updated at each flipped
302
+ * centre: art pixels covered, centres covered, and covered pixels per island
303
+ * label; the islands touched are the labels whose count is positive.
304
+ * - **Outline deviations** (`outline`) are handed back only when the outline
305
+ * and the polygon it is read against are equal, coordinate by coordinate
306
+ * with a zero's sign, to the last ones of that slot — the same pure function
307
+ * of the same input.
308
+ *
309
+ * - **A region's art pixels** (`regionPixels`, issue #1253) — the art pixels
310
+ * whose centre lies in a region's closed polygon, which `MQ_FILL_DISTANCE`
311
+ * reads — are a function of the art and the polygon alone, so a set is
312
+ * handed back whenever the polygon equals, coordinate by coordinate with a
313
+ * zero's sign, one already read through this object. A region polygon's
314
+ * self-intersection check (`polygonCrossing`), a function of the polygon
315
+ * alone, is handed back the same way.
316
+ * - **A region's edge answers** (`regionEdges`, issue #1253) — whether an
317
+ * edge meets the closed polygon, and whether the region holds it and how far
318
+ * it lies — are functions of the edge's two ends and the region's polygon and
319
+ * band alone, so each is carried from the last reading of that region for an
320
+ * edge with both ends equal, coordinate by coordinate, in the same order.
321
+ * - **A region's fill distances** (`regionFill`, issue #1253) — which of its
322
+ * art pixels lie in the hull, and each one's distance to the nearest vertex.
323
+ * Whether a pixel lies in the hull is measured again only inside the box of
324
+ * the hull edges that changed (directed, both ends exact), widened by
325
+ * `HULL_CARRY_MARGIN`: outside it the edges the two hulls share answer the
326
+ * same, a changed edge is further than the boundary tolerance, and the
327
+ * changed edges cross a horizontal ray from the pixel an even number of
328
+ * times between them — none when the pixel is right of, above or below the
329
+ * box, and when it is left of it, every changed edge that spans its row,
330
+ * whose count is even because the removed and added edges meet each vertex
331
+ * an even number of times — so the crossing parity is the last one's.
332
+ * A pixel's nearest distance is carried by the vertices that differ
333
+ * from the last reading's, compared as a multiset of exact coordinates: when
334
+ * the vertex that attained it is still there, the minimum over the vertices
335
+ * kept is the last minimum, and the new one is the smaller of it and the
336
+ * distances to the vertices added; when that vertex is gone, the pixel is
337
+ * measured again over every vertex. A minimum is exact, so either way it is
338
+ * the same number. A reading whose vertices differ from the last in more than
339
+ * they share is built from nothing.
340
+ *
341
+ * A reading whose triangles differ from the last in more triangles than they
342
+ * share is built from nothing, as the first one is; which way a reading is
343
+ * built changes its cost and never its values.
344
+ */
345
+ export interface StepRasters {
346
+ readonly rasters: ArtRasters;
347
+ readonly tally: StepRasterTally;
348
+ /** The coverage reading of these triangles over the drawing-pixel `points`; its arrays are valid until the next reading. */
349
+ coverage(points: Array<[number, number]>, triangles: number[]): CoverageReading;
350
+ /** The fault planted for a control, or null. */
351
+ readonly plant: StepRasterPlant | null;
352
+ /**
353
+ * `measure(memo)`'s value, or the last one of `slot` when `outline` and
354
+ * `against` equal the last ones exactly; `memo` is the slot's carried
355
+ * distances, which `measure` reads and keeps current.
356
+ */
357
+ outline<T>(slot: string, outline: ReadonlyArray<readonly [number, number]>, against: ReadonlyArray<readonly [number, number]>, measure: (memo: OutlineMemo) => T): T;
358
+ /**
359
+ * `compute()`'s value — the art pixels, ascending, whose centre lies in the
360
+ * closed `polygon` — or the set already computed for a polygon equal to it
361
+ * coordinate by coordinate. The art is this object's, so the polygon is the
362
+ * whole of the key.
363
+ */
364
+ regionPixels(polygon: ReadonlyArray<readonly [number, number]>, compute: () => Int32Array): Int32Array;
365
+ /** `compute()`'s value — `findSelfIntersection` of `polygon` — or the one already computed for a polygon equal to it coordinate by coordinate. */
366
+ polygonCrossing(polygon: ReadonlyArray<readonly [number, number]>, compute: () => [number, number] | null): [number, number] | null;
367
+ /**
368
+ * One reading's answers per edge for the region with this polygon and band:
369
+ * `entry(a, b)` is the edge's record, carried from the last reading of the
370
+ * region when an edge with the same two ends (exact coordinates, this order)
371
+ * was read there, else empty for the caller to fill. Records not read again
372
+ * are dropped at the next reading.
373
+ */
374
+ regionEdges(polygon: ReadonlyArray<readonly [number, number]>, transition: number): RegionEdgeReading;
375
+ /** The region's fill reading, carried from the last one of the region with this polygon; valid until the next. */
376
+ regionFill(polygon: ReadonlyArray<readonly [number, number]>, question: RegionFillQuestion): RegionFillReading;
377
+ }
378
+
379
+ /** A region edge's answers, as `src/meshquality.ts`'s `regionRows` reads them; a field absent is not yet computed. */
380
+ export interface RegionEdgeRecord {
381
+ /** `segmentMeetsPolygon` of the edge against the closed polygon. */
382
+ meets?: boolean;
383
+ /** The edge's distance from the closed polygon when the region holds it (`edgeIsHeldByRegion`), else null. */
384
+ held?: number | null;
385
+ }
386
+
387
+ export interface RegionEdgeReading {
388
+ entry(a: readonly [number, number], b: readonly [number, number]): RegionEdgeRecord;
389
+ }
390
+
391
+ /**
392
+ * How far beyond the box of a hull's changed edges a pixel centre must lie for
393
+ * its answer to be carried, in the hull's units. Far larger than any rounding
394
+ * of a crossing's abscissa and than the boundary tolerance the test reads.
395
+ */
396
+ const HULL_CARRY_MARGIN = 1;
397
+
398
+ /** What `regionFill` is handed: the definitions it reads, which stay `src/meshquality.ts`'s. */
399
+ export interface RegionFillQuestion {
400
+ /** The region's art pixels, ascending (`regionPixels`). */
401
+ pixels: Int32Array;
402
+ /** The centre of art pixel `i`, in the hull's units. */
403
+ centre(i: number): readonly [number, number];
404
+ hull: ReadonlyArray<readonly [number, number]>;
405
+ points: ReadonlyArray<readonly [number, number]>;
406
+ /** Does the centre of art pixel `i` lie in the closed hull? */
407
+ inHull(i: number): boolean;
408
+ /** The distance from the centre of art pixel `i` to `p`. */
409
+ distance(i: number, p: readonly [number, number]): number;
410
+ }
411
+
412
+ /** The region pixels in the hull, ascending, and each one's distance to its nearest vertex. */
413
+ export interface RegionFillReading {
414
+ inside: Int32Array;
415
+ nearest: Float64Array;
416
+ }
417
+
418
+ /** A coordinate as a key: its shortest round-trip decimal, with a negative zero told from a positive one. */
419
+ function coordKey(v: number): string {
420
+ return v === 0 && 1 / v < 0 ? '-0' : String(v);
421
+ }
422
+
423
+ /** Equal coordinate by coordinate, a zero's sign included. */
424
+ function samePolygon(a: ReadonlyArray<readonly [number, number]>, b: Float64Array): boolean {
425
+ if (a.length * 2 !== b.length) return false;
426
+ 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;
427
+ return true;
428
+ }
429
+
430
+ function flat(p: ReadonlyArray<readonly [number, number]>): Float64Array {
431
+ const out = new Float64Array(p.length * 2);
432
+ for (let i = 0; i < p.length; i++) {
433
+ out[i * 2] = p[i][0];
434
+ out[i * 2 + 1] = p[i][1];
435
+ }
436
+ return out;
437
+ }
438
+
439
+ type Corners = [readonly [number, number], readonly [number, number], readonly [number, number]];
440
+
441
+ /** One region's carried fill state: the pixels it was read for, the hull and the positions inside it, and per pixel the nearest vertex. */
442
+ interface FillSlot {
443
+ polygon: Float64Array;
444
+ pixels: Int32Array;
445
+ /** The last hull's directed edges by both ends' exact coordinates, with their counts, and per pixel whether it lay in that hull. */
446
+ hullEdges: Map<string, { a: readonly [number, number]; b: readonly [number, number]; n: number }> | null;
447
+ inHull: Uint8Array;
448
+ points: Map<string, { p: readonly [number, number]; n: number }> | null;
449
+ nearest: Float64Array;
450
+ nearX: Float64Array;
451
+ nearY: Float64Array;
452
+ }
453
+
454
+ interface HeldTriangles {
455
+ corners: Corners;
456
+ n: number;
457
+ }
458
+
459
+ interface CoverageState {
460
+ held: Map<string, HeldTriangles>;
461
+ /** Triangles covering each pixel centre. */
462
+ count: Int32Array;
463
+ covered: Uint8Array;
464
+ /** The column pass of the distance transform, and the transform. */
465
+ columns: Float64Array;
466
+ dist: Float64Array;
467
+ coveredArt: number;
468
+ coveredTotal: number;
469
+ /** Covered pixels per island label (index 0 unused). */
470
+ islandHits: Int32Array;
471
+ islandsTouched: number;
472
+ /** Art pixels not covered. */
473
+ uncoveredArt: Set<number>;
474
+ /** Per connectivity, once read: covered centres outside the filled silhouette, and covered centres inside it that are not art. */
475
+ silhouettes: Map<4 | 8, { outside: Set<number>; holes: Set<number> }>;
476
+ }
477
+
478
+ /** Step rasters over `rasters`' art. Reads nothing until the first reading. */
479
+ export function stepRastersOf(rasters: ArtRasters, plant: StepRasterPlant | null = null): StepRasters {
480
+ const tally: StepRasterTally = {
481
+ readings: 0,
482
+ rebuilt: 0,
483
+ carried: 0,
484
+ trianglesDrawn: 0,
485
+ trianglesKept: 0,
486
+ pixelsFlipped: 0,
487
+ columnsRedone: 0,
488
+ rowsRedone: 0,
489
+ outlinesMeasured: 0,
490
+ outlinesReused: 0,
491
+ distanceLists: 0,
492
+ distanceListsReused: 0,
493
+ distanceListsCarried: 0,
494
+ edgesCarried: 0,
495
+ edgesComputed: 0,
496
+ regionSetsComputed: 0,
497
+ regionSetsReused: 0,
498
+ polygonChecksComputed: 0,
499
+ polygonChecksReused: 0,
500
+ regionEdgesCarried: 0,
501
+ regionEdgesComputed: 0,
502
+ fillHullsReused: 0,
503
+ fillHullsCarried: 0,
504
+ fillHullsMeasured: 0,
505
+ fillHullPixelsMeasured: 0,
506
+ fillNearestRebuilt: 0,
507
+ fillNearestCarried: 0,
508
+ fillNearestMeasured: 0,
509
+ };
510
+ const { width: w, height: h } = rasters.art.mask;
511
+ let state: CoverageState | null = null;
512
+ const outlines = new Map<string, { outline: Float64Array; against: Float64Array; value: unknown }>();
513
+ const memos = new Map<string, OutlineMemo>();
514
+ const regionSets: Array<{ polygon: Float64Array; pixels: Int32Array }> = [];
515
+ const polygonChecks: Array<{ polygon: Float64Array; crossing: [number, number] | null }> = [];
516
+ const regionEdgeSlots: Array<{ polygon: Float64Array; transition: number; last: Map<string, RegionEdgeRecord> }> = [];
517
+ const fillSlots: FillSlot[] = [];
518
+
519
+ /** Add `sign` to the count of every centre the triangle covers, pushing each such centre to `touched`. */
520
+ const draw = (count: Int32Array, corners: Corners, sign: number, touched: number[] | null): void => {
521
+ const [a, b, c] = corners;
522
+ const box = triangleRasterBox(a, b, c, w, h);
523
+ if (box === null) return;
524
+ for (let y = box.minY; y <= box.maxY; y++) {
525
+ for (let x = box.minX; x <= box.maxX; x++) {
526
+ if (!pixelCentreInTriangle(x, y, a, b, c, box.orient)) continue;
527
+ count[y * w + x] += sign;
528
+ if (touched !== null) touched.push(y * w + x);
529
+ }
530
+ }
531
+ };
532
+
533
+ const readingOf = (s: CoverageState): CoverageReading => {
534
+ let second = 0;
535
+ if (s.islandsTouched >= 2) {
536
+ let seen = 0;
537
+ for (let l = 1; l < s.islandHits.length; l++) {
538
+ if (s.islandHits[l] > 0 && ++seen === 2) {
539
+ second = l;
540
+ break;
541
+ }
542
+ }
543
+ }
544
+ // The worst of a set by the full scan's rule: the largest value, the smallest index among equals.
545
+ const worstOf = (set: Set<number>, value: Float64Array): { at: number; sq: number } => {
546
+ let at = -1;
547
+ let sq = 0;
548
+ for (const i of set) {
549
+ const v = value[i];
550
+ if (at === -1 || v > sq || (v === sq && i < at)) {
551
+ at = i;
552
+ sq = v;
553
+ }
554
+ }
555
+ return { at, sq };
556
+ };
557
+ const silhouette = (connectivity: 4 | 8): SilhouetteReading => {
558
+ let sets = s.silhouettes.get(connectivity);
559
+ if (sets === undefined) {
560
+ const fills = rasters.fills();
561
+ const filled = connectivity === 8 ? fills.fill8 : fills.fill4;
562
+ const artBits = rasters.artBits();
563
+ sets = { outside: new Set(), holes: new Set() };
564
+ for (let i = 0; i < s.covered.length; i++) {
565
+ if (!s.covered[i]) continue;
566
+ if (!filled[i]) sets.outside.add(i);
567
+ else if (!artBits[i]) sets.holes.add(i);
568
+ }
569
+ s.silhouettes.set(connectivity, sets);
570
+ }
571
+ const over = worstOf(sets.outside, rasters.toFilled(connectivity));
572
+ let holeAt = -1;
573
+ for (const i of sets.holes) if (holeAt === -1 || i < holeAt) holeAt = i;
574
+ return { overAt: over.at, overSq: over.sq, holes: sets.holes.size, holeAt };
575
+ };
576
+ return {
577
+ covered: s.covered,
578
+ toCovered: s.dist,
579
+ coveredArt: s.coveredArt,
580
+ anyCovered: s.coveredTotal > 0,
581
+ undercut: worstOf(s.uncoveredArt, s.dist),
582
+ silhouette,
583
+ islandsTouched: s.islandsTouched,
584
+ secondIsland: second,
585
+ };
586
+ };
587
+
588
+ const rebuild = (held: Map<string, HeldTriangles>): CoverageState => {
589
+ tally.rebuilt++;
590
+ const artBits = rasters.artBits();
591
+ const { label, sizes } = rasters.islands();
592
+ const count = new Int32Array(w * h);
593
+ for (const { corners, n } of held.values()) for (let k = 0; k < n; k++) draw(count, corners, 1, null);
594
+ const covered = new Uint8Array(w * h);
595
+ const islandHits = new Int32Array(sizes.length + 1);
596
+ let coveredArt = 0;
597
+ let coveredTotal = 0;
598
+ let islandsTouched = 0;
599
+ const uncoveredArt = new Set<number>();
600
+ for (let i = 0; i < covered.length; i++) {
601
+ if (count[i] <= 0) {
602
+ if (artBits[i]) uncoveredArt.add(i);
603
+ continue;
604
+ }
605
+ covered[i] = 1;
606
+ coveredTotal++;
607
+ if (artBits[i]) coveredArt++;
608
+ if (label[i] && islandHits[label[i]]++ === 0) islandsTouched++;
609
+ }
610
+ const passes = distancePassesOf(w, h);
611
+ const columns = new Float64Array(w * h);
612
+ for (let x = 0; x < w; x++) {
613
+ const out = passes.column(covered, x);
614
+ for (let y = 0; y < h; y++) columns[y * w + x] = out[y];
615
+ }
616
+ const dist = new Float64Array(w * h);
617
+ for (let y = 0; y < h; y++) {
618
+ const out = passes.row(columns, y);
619
+ for (let x = 0; x < w; x++) dist[y * w + x] = out[x];
620
+ }
621
+ return { held, count, covered, columns, dist, coveredArt, coveredTotal, islandHits, islandsTouched, uncoveredArt, silhouettes: new Map() };
622
+ };
623
+
624
+ /** Carry `s` to the triangles `held` by drawing the ones that differ, then redo the distance transform where it changed. */
625
+ const carry = (s: CoverageState, held: Map<string, HeldTriangles>, removed: Corners[], added: Corners[]): void => {
626
+ tally.carried++;
627
+ tally.trianglesDrawn += removed.length + added.length;
628
+ const touched: number[] = [];
629
+ for (const corners of removed) draw(s.count, corners, -1, touched);
630
+ added.forEach((corners, k) => {
631
+ if (plant === 'skip-a-triangle' && k === 0) return;
632
+ draw(s.count, corners, 1, touched);
633
+ });
634
+ s.held = held;
635
+ // Flip the centres whose count crossed zero, keeping the integer counts with them.
636
+ const artBits = rasters.artBits();
637
+ const { label } = rasters.islands();
638
+ const fills = s.silhouettes.size > 0 ? rasters.fills() : null;
639
+ const dirtyColumns = new Uint8Array(w);
640
+ for (const i of touched) {
641
+ const now = s.count[i] > 0 ? 1 : 0;
642
+ if (now === s.covered[i]) continue;
643
+ s.covered[i] = now;
644
+ tally.pixelsFlipped++;
645
+ dirtyColumns[i % w] = 1;
646
+ const d = now === 1 ? 1 : -1;
647
+ s.coveredTotal += d;
648
+ if (artBits[i]) {
649
+ s.coveredArt += d;
650
+ if (now === 1) s.uncoveredArt.delete(i);
651
+ else s.uncoveredArt.add(i);
652
+ }
653
+ for (const [connectivity, sets] of s.silhouettes) {
654
+ if (now === 0) {
655
+ sets.outside.delete(i);
656
+ sets.holes.delete(i);
657
+ continue;
658
+ }
659
+ const filled = connectivity === 8 ? fills!.fill8 : fills!.fill4;
660
+ if (!filled[i]) sets.outside.add(i);
661
+ else if (!artBits[i]) sets.holes.add(i);
662
+ }
663
+ const l = label[i];
664
+ if (l) {
665
+ const before = s.islandHits[l];
666
+ s.islandHits[l] += d;
667
+ if (before === 0) s.islandsTouched++;
668
+ else if (s.islandHits[l] === 0) s.islandsTouched--;
669
+ }
670
+ }
671
+ // Redo the columns holding a flipped centre, then the rows where a column value changed.
672
+ const passes = distancePassesOf(w, h);
673
+ const dirtyRows = new Uint8Array(h);
674
+ let skipColumn = plant === 'skip-a-column';
675
+ for (let x = 0; x < w; x++) {
676
+ if (!dirtyColumns[x]) continue;
677
+ if (skipColumn) {
678
+ skipColumn = false;
679
+ continue;
680
+ }
681
+ tally.columnsRedone++;
682
+ const out = passes.column(s.covered, x);
683
+ for (let y = 0; y < h; y++) {
684
+ if (Object.is(s.columns[y * w + x], out[y])) continue;
685
+ s.columns[y * w + x] = out[y];
686
+ dirtyRows[y] = 1;
687
+ }
688
+ }
689
+ let skipRow = plant === 'skip-a-row';
690
+ for (let y = 0; y < h; y++) {
691
+ if (!dirtyRows[y]) continue;
692
+ if (skipRow) {
693
+ skipRow = false;
694
+ continue;
695
+ }
696
+ tally.rowsRedone++;
697
+ const out = passes.row(s.columns, y);
698
+ for (let x = 0; x < w; x++) s.dist[y * w + x] = out[x];
699
+ }
700
+ };
701
+
702
+ return {
703
+ rasters,
704
+ tally,
705
+ coverage: (points, triangles) => {
706
+ tally.readings++;
707
+ const held = new Map<string, HeldTriangles>();
708
+ for (let t = 0; t + 2 < triangles.length; t += 3) {
709
+ const corners: Corners = [points[triangles[t]], points[triangles[t + 1]], points[triangles[t + 2]]];
710
+ 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])}`;
711
+ const entry = held.get(key);
712
+ if (entry === undefined) held.set(key, { corners, n: 1 });
713
+ else entry.n++;
714
+ }
715
+ if (state === null) {
716
+ state = rebuild(held);
717
+ return readingOf(state);
718
+ }
719
+ // The multiset difference against the last reading's triangles.
720
+ const removed: Corners[] = [];
721
+ const added: Corners[] = [];
722
+ let kept = 0;
723
+ for (const [key, { corners, n }] of held) {
724
+ const was = state.held.get(key)?.n ?? 0;
725
+ kept += Math.min(was, n);
726
+ for (let k = was; k < n; k++) added.push(corners);
727
+ }
728
+ for (const [key, { corners, n }] of state.held) {
729
+ const now = held.get(key)?.n ?? 0;
730
+ for (let k = now; k < n; k++) removed.push(corners);
731
+ }
732
+ if (removed.length + added.length > kept) {
733
+ state = rebuild(held);
734
+ return readingOf(state);
735
+ }
736
+ tally.trianglesKept += kept;
737
+ carry(state, held, removed, added);
738
+ return readingOf(state);
739
+ },
740
+ plant,
741
+ outline: <T>(slot: string, outline: ReadonlyArray<readonly [number, number]>, against: ReadonlyArray<readonly [number, number]>, measure: (memo: OutlineMemo) => T): T => {
742
+ const last = outlines.get(slot);
743
+ if (last !== undefined) {
744
+ const same =
745
+ plant === 'stale-outline'
746
+ ? last.outline.length === outline.length * 2 && last.against.length === against.length * 2
747
+ : samePolygon(outline, last.outline) && samePolygon(against, last.against);
748
+ if (same) {
749
+ tally.outlinesReused++;
750
+ return last.value as T;
751
+ }
752
+ }
753
+ tally.outlinesMeasured++;
754
+ let memo = memos.get(slot);
755
+ if (memo === undefined) {
756
+ memo = { against: null, toAgainst: new Map(), outlineEdges: null, toOutline: new Map(), readings: 0 };
757
+ memos.set(slot, memo);
758
+ }
759
+ const value = measure(memo);
760
+ outlines.set(slot, { outline: flat(outline), against: flat(against), value });
761
+ return value;
762
+ },
763
+ regionPixels: (polygon, compute) => {
764
+ const kept = regionSets.find((r) => (plant === 'stale-region' ? r.polygon.length === polygon.length * 2 : samePolygon(polygon, r.polygon)));
765
+ if (kept !== undefined) {
766
+ tally.regionSetsReused++;
767
+ return kept.pixels;
768
+ }
769
+ tally.regionSetsComputed++;
770
+ const pixels = compute();
771
+ regionSets.push({ polygon: flat(polygon), pixels });
772
+ return pixels;
773
+ },
774
+ polygonCrossing: (polygon, compute) => {
775
+ const kept = polygonChecks.find((r) => (plant === 'stale-polygon-check' ? r.polygon.length === polygon.length * 2 : samePolygon(polygon, r.polygon)));
776
+ if (kept !== undefined) {
777
+ tally.polygonChecksReused++;
778
+ return kept.crossing;
779
+ }
780
+ tally.polygonChecksComputed++;
781
+ const crossing = compute();
782
+ polygonChecks.push({ polygon: flat(polygon), crossing });
783
+ return crossing;
784
+ },
785
+ regionFill: (polygon, q) => {
786
+ let slot = fillSlots.find((r) => samePolygon(polygon, r.polygon));
787
+ if (slot === undefined) {
788
+ slot = { polygon: flat(polygon), pixels: q.pixels, hullEdges: null, inHull: new Uint8Array(0), points: null, nearest: new Float64Array(0), nearX: new Float64Array(0), nearY: new Float64Array(0) };
789
+ fillSlots.push(slot);
790
+ }
791
+ const n = q.pixels.length;
792
+ // The vertices, as a multiset of exact coordinates, against the last reading's.
793
+ const keyOf = (p: readonly [number, number]): string => `${coordKey(p[0])},${coordKey(p[1])}`;
794
+ const points = new Map<string, { p: readonly [number, number]; n: number }>();
795
+ for (const p of q.points) {
796
+ const k = keyOf(p);
797
+ const e = points.get(k);
798
+ if (e === undefined) points.set(k, { p, n: 1 });
799
+ else e.n++;
800
+ }
801
+ const measure = (k: number): void => {
802
+ let best = Infinity;
803
+ let bx = NaN;
804
+ let by = NaN;
805
+ for (const p of q.points) {
806
+ const d = q.distance(q.pixels[k], p);
807
+ if (d < best) {
808
+ best = d;
809
+ bx = p[0];
810
+ by = p[1];
811
+ }
812
+ }
813
+ slot!.nearest[k] = best;
814
+ slot!.nearX[k] = bx;
815
+ slot!.nearY[k] = by;
816
+ };
817
+ // A vertex whose coordinates are gone, and one whose coordinates are new; a count that only moved changes no minimum.
818
+ let gone: Set<string> | null = null;
819
+ const added: Array<readonly [number, number]> = [];
820
+ const samePixels = slot.points !== null && slot.pixels === q.pixels;
821
+ if (samePixels) {
822
+ gone = new Set();
823
+ let kept = 0;
824
+ let differ = 0;
825
+ for (const [k, { p, n: now }] of points) {
826
+ const was = slot.points!.get(k)?.n ?? 0;
827
+ kept += Math.min(was, now);
828
+ differ += Math.max(0, now - was);
829
+ if (was === 0) added.push(p);
830
+ }
831
+ for (const [k, { n: was }] of slot.points!) {
832
+ const now = points.get(k)?.n ?? 0;
833
+ differ += Math.max(0, was - now);
834
+ if (now === 0) gone.add(k);
835
+ }
836
+ if (differ > kept) gone = null;
837
+ }
838
+ if (gone === null) {
839
+ tally.fillNearestRebuilt++;
840
+ slot.pixels = q.pixels;
841
+ slot.nearest = new Float64Array(n);
842
+ slot.nearX = new Float64Array(n);
843
+ slot.nearY = new Float64Array(n);
844
+ for (let k = 0; k < n; k++) measure(k);
845
+ } else {
846
+ for (let k = 0; k < n; k++) {
847
+ if (plant !== 'stale-fill-nearest' && gone.has(`${coordKey(slot.nearX[k])},${coordKey(slot.nearY[k])}`)) {
848
+ tally.fillNearestMeasured++;
849
+ measure(k);
850
+ continue;
851
+ }
852
+ tally.fillNearestCarried++;
853
+ for (const p of added) {
854
+ const d = q.distance(q.pixels[k], p);
855
+ if (d < slot.nearest[k]) {
856
+ slot.nearest[k] = d;
857
+ slot.nearX[k] = p[0];
858
+ slot.nearY[k] = p[1];
859
+ }
860
+ }
861
+ }
862
+ }
863
+ slot.points = points;
864
+ // The pixels in the hull: carried outside the box of the hull edges that changed, measured again inside it.
865
+ const hullEdges = new Map<string, { a: readonly [number, number]; b: readonly [number, number]; n: number }>();
866
+ for (let k = 0; k < q.hull.length; k++) {
867
+ const a = q.hull[k];
868
+ const b = q.hull[(k + 1) % q.hull.length];
869
+ const key = `${keyOf(a)}>${keyOf(b)}`;
870
+ const e = hullEdges.get(key);
871
+ if (e === undefined) hullEdges.set(key, { a, b, n: 1 });
872
+ else e.n++;
873
+ }
874
+ if (!samePixels || slot.hullEdges === null) {
875
+ tally.fillHullsMeasured++;
876
+ slot.inHull = new Uint8Array(n);
877
+ for (let k = 0; k < n; k++) slot.inHull[k] = q.inHull(q.pixels[k]) ? 1 : 0;
878
+ } else {
879
+ let minX = Infinity;
880
+ let minY = Infinity;
881
+ let maxX = -Infinity;
882
+ let maxY = -Infinity;
883
+ const widen = (p: readonly [number, number]): void => {
884
+ minX = Math.min(minX, p[0]);
885
+ maxX = Math.max(maxX, p[0]);
886
+ minY = Math.min(minY, p[1]);
887
+ maxY = Math.max(maxY, p[1]);
888
+ };
889
+ for (const [key, e] of hullEdges) if ((slot.hullEdges.get(key)?.n ?? 0) !== e.n) (widen(e.a), widen(e.b));
890
+ for (const [key, e] of slot.hullEdges) if ((hullEdges.get(key)?.n ?? 0) !== e.n) (widen(e.a), widen(e.b));
891
+ if (minX === Infinity) tally.fillHullsReused++;
892
+ else {
893
+ tally.fillHullsCarried++;
894
+ minX -= HULL_CARRY_MARGIN;
895
+ minY -= HULL_CARRY_MARGIN;
896
+ maxX += HULL_CARRY_MARGIN;
897
+ maxY += HULL_CARRY_MARGIN;
898
+ for (let k = 0; k < n; k++) {
899
+ const c = q.centre(q.pixels[k]);
900
+ if (c[0] < minX || c[0] > maxX || c[1] < minY || c[1] > maxY) continue;
901
+ if (plant === 'stale-fill-hull') continue;
902
+ tally.fillHullPixelsMeasured++;
903
+ slot.inHull[k] = q.inHull(q.pixels[k]) ? 1 : 0;
904
+ }
905
+ }
906
+ }
907
+ slot.hullEdges = hullEdges;
908
+ const kept: number[] = [];
909
+ for (let k = 0; k < n; k++) if (slot.inHull[k]) kept.push(k);
910
+ const inside = new Int32Array(kept.length);
911
+ const nearest = new Float64Array(kept.length);
912
+ kept.forEach((k, j) => {
913
+ inside[j] = q.pixels[k];
914
+ nearest[j] = slot!.nearest[k];
915
+ });
916
+ return { inside, nearest };
917
+ },
918
+ regionEdges: (polygon, transition) => {
919
+ let slot = regionEdgeSlots.find((r) => Object.is(r.transition, transition) && samePolygon(polygon, r.polygon));
920
+ if (slot === undefined) {
921
+ slot = { polygon: flat(polygon), transition, last: new Map() };
922
+ regionEdgeSlots.push(slot);
923
+ }
924
+ const last = slot.last;
925
+ const now = new Map<string, RegionEdgeRecord>();
926
+ slot.last = now;
927
+ const keyOf = (p: readonly [number, number]): string => `${coordKey(p[0])},${coordKey(p[1])}`;
928
+ return {
929
+ entry: (a, b) => {
930
+ const key = plant === 'stale-region-edge' ? keyOf(a) : `${keyOf(a)};${keyOf(b)}`;
931
+ let record = now.get(key);
932
+ if (record !== undefined) return record;
933
+ record = last.get(key);
934
+ if (record === undefined) {
935
+ tally.regionEdgesComputed++;
936
+ record = {};
937
+ } else tally.regionEdgesCarried++;
938
+ now.set(key, record);
939
+ return record;
940
+ },
941
+ };
942
+ },
943
+ };
944
+ }