spine-rigc 2.20.1 → 2.20.3
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 +2 -2
- package/src/mesh.ts +84 -23
- package/src/meshquality.ts +221 -59
- package/src/meshrasters.ts +466 -1
- package/src/meshreduce.ts +26 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.3",
|
|
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": {
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"fetch-examples": "bash scripts/fetch-examples.sh",
|
|
97
97
|
"bench:usage": "bun bench/count_features.ts",
|
|
98
98
|
"viewer": "vite --config viewer/vite.config.ts",
|
|
99
|
-
"prepublishOnly": "bun
|
|
99
|
+
"prepublishOnly": "bun scripts/prepublish_gate.ts"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/mesh.ts
CHANGED
|
@@ -1319,23 +1319,60 @@ export function rasteriseTriangles(points: Array<[number, number]>, triangles: n
|
|
|
1319
1319
|
const a = points[triangles[t]];
|
|
1320
1320
|
const b = points[triangles[t + 1]];
|
|
1321
1321
|
const c = points[triangles[t + 2]];
|
|
1322
|
-
const
|
|
1323
|
-
if (
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const maxX = Math.min(w - 1, Math.ceil(Math.max(a[0], b[0], c[0]) + 1));
|
|
1327
|
-
const minY = Math.max(0, Math.floor(Math.min(a[1], b[1], c[1]) - 1));
|
|
1328
|
-
const maxY = Math.min(h - 1, Math.ceil(Math.max(a[1], b[1], c[1]) + 1));
|
|
1329
|
-
for (let y = minY; y <= maxY; y++) {
|
|
1330
|
-
for (let x = minX; x <= maxX; x++) {
|
|
1322
|
+
const box = triangleRasterBox(a, b, c, w, h);
|
|
1323
|
+
if (box === null) continue;
|
|
1324
|
+
for (let y = box.minY; y <= box.maxY; y++) {
|
|
1325
|
+
for (let x = box.minX; x <= box.maxX; x++) {
|
|
1331
1326
|
if (covered[y * w + x]) continue;
|
|
1332
|
-
if (
|
|
1327
|
+
if (pixelCentreInTriangle(x, y, a, b, c, box.orient)) covered[y * w + x] = 1;
|
|
1333
1328
|
}
|
|
1334
1329
|
}
|
|
1335
1330
|
}
|
|
1336
1331
|
return covered;
|
|
1337
1332
|
}
|
|
1338
1333
|
|
|
1334
|
+
/**
|
|
1335
|
+
* The pixels of a `w`x`h` grid one triangle can cover under `rasteriseTriangles`'s
|
|
1336
|
+
* rule — the box of its corners widened by a pixel and clamped to the grid —
|
|
1337
|
+
* and the orientation its centre test reads; null for a degenerate triangle
|
|
1338
|
+
* (zero doubled area), which covers nothing. `rasteriseTriangles` and the
|
|
1339
|
+
* per-step coverage of `src/meshrasters.ts` both read the rule from here and
|
|
1340
|
+
* from `pixelCentreInTriangle`, so the two cannot drift apart.
|
|
1341
|
+
*/
|
|
1342
|
+
export function triangleRasterBox(
|
|
1343
|
+
a: readonly [number, number],
|
|
1344
|
+
b: readonly [number, number],
|
|
1345
|
+
c: readonly [number, number],
|
|
1346
|
+
w: number,
|
|
1347
|
+
h: number,
|
|
1348
|
+
): { orient: number; minX: number; maxX: number; minY: number; maxY: number } | null {
|
|
1349
|
+
const twice = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
1350
|
+
if (Math.abs(twice) < 1e-12) return null;
|
|
1351
|
+
return {
|
|
1352
|
+
orient: twice > 0 ? 1 : -1,
|
|
1353
|
+
minX: Math.max(0, Math.floor(Math.min(a[0], b[0], c[0]) - 1)),
|
|
1354
|
+
maxX: Math.min(w - 1, Math.ceil(Math.max(a[0], b[0], c[0]) + 1)),
|
|
1355
|
+
minY: Math.max(0, Math.floor(Math.min(a[1], b[1], c[1]) - 1)),
|
|
1356
|
+
maxY: Math.min(h - 1, Math.ceil(Math.max(a[1], b[1], c[1]) + 1)),
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Is the centre of pixel (`x`, `y`) in or on the triangle — `rasteriseTriangles`'s
|
|
1362
|
+
* test, with the orientation `triangleRasterBox` gave. `pointInTriangle` at
|
|
1363
|
+
* `[x + 0.5, y + 0.5]`, written out without the point array: the same three
|
|
1364
|
+
* products, in the same order, against the same tolerance.
|
|
1365
|
+
*/
|
|
1366
|
+
export function pixelCentreInTriangle(x: number, y: number, a: readonly [number, number], b: readonly [number, number], c: readonly [number, number], orient: number): boolean {
|
|
1367
|
+
const px = x + 0.5;
|
|
1368
|
+
const py = y + 0.5;
|
|
1369
|
+
return (
|
|
1370
|
+
((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) * orient >= -1e-9 &&
|
|
1371
|
+
((c[0] - b[0]) * (py - b[1]) - (c[1] - b[1]) * (px - b[0])) * orient >= -1e-9 &&
|
|
1372
|
+
((a[0] - c[0]) * (py - c[1]) - (a[1] - c[1]) * (px - c[0])) * orient >= -1e-9
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1339
1376
|
/**
|
|
1340
1377
|
* Rasterise a triangle set over the part's pixel grid and measure two things the
|
|
1341
1378
|
* skeleton file cannot say: how much of the art the mesh covers, and how far past
|
|
@@ -1492,10 +1529,33 @@ export function measureAuthoredMeshFit(
|
|
|
1492
1529
|
* never asks (a mask with no art has no covered-outside pixel to ask about).
|
|
1493
1530
|
*/
|
|
1494
1531
|
export function squaredDistanceToSet(inside: Uint8Array, w: number, h: number): Float64Array {
|
|
1495
|
-
const
|
|
1532
|
+
const passes = distancePassesOf(w, h);
|
|
1496
1533
|
const dist = new Float64Array(w * h);
|
|
1497
|
-
for (let
|
|
1534
|
+
for (let x = 0; x < w; x++) {
|
|
1535
|
+
const out = passes.column(inside, x);
|
|
1536
|
+
for (let y = 0; y < h; y++) dist[y * w + x] = out[y];
|
|
1537
|
+
}
|
|
1538
|
+
for (let y = 0; y < h; y++) {
|
|
1539
|
+
const out = passes.row(dist, y);
|
|
1540
|
+
for (let x = 0; x < w; x++) dist[y * w + x] = out[x];
|
|
1541
|
+
}
|
|
1542
|
+
return dist;
|
|
1543
|
+
}
|
|
1498
1544
|
|
|
1545
|
+
/**
|
|
1546
|
+
* `squaredDistanceToSet`'s two passes over one `w`x`h` grid, one column or one
|
|
1547
|
+
* row per call, so a caller holding the previous grid's passes can redo only
|
|
1548
|
+
* the columns whose set changed and the rows whose column values changed.
|
|
1549
|
+
* `column(inside, x)` is column `x`'s pass over the set (`INF` where the
|
|
1550
|
+
* column has no set pixel); `row(columns, y)` is row `y`'s pass over the
|
|
1551
|
+
* column results. Each returns a scratch array whose first `h` (column) or
|
|
1552
|
+
* `w` (row) entries are the result, valid until the next call. A column's
|
|
1553
|
+
* result depends on that column of `inside` alone and a row's on that row of
|
|
1554
|
+
* `columns` alone, which is what makes redoing a subset exact: every output
|
|
1555
|
+
* is this one function of the same input.
|
|
1556
|
+
*/
|
|
1557
|
+
export function distancePassesOf(w: number, h: number): { column(inside: Uint8Array, x: number): Float64Array; row(columns: Float64Array, y: number): Float64Array } {
|
|
1558
|
+
const INF = w * w + h * h + 1;
|
|
1499
1559
|
const span = Math.max(w, h);
|
|
1500
1560
|
const f = new Float64Array(span);
|
|
1501
1561
|
const out = new Float64Array(span);
|
|
@@ -1526,17 +1586,18 @@ export function squaredDistanceToSet(inside: Uint8Array, w: number, h: number):
|
|
|
1526
1586
|
}
|
|
1527
1587
|
};
|
|
1528
1588
|
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1589
|
+
return {
|
|
1590
|
+
column: (inside, x) => {
|
|
1591
|
+
for (let y = 0; y < h; y++) f[y] = inside[y * w + x] ? 0 : INF;
|
|
1592
|
+
envelope(h);
|
|
1593
|
+
return out;
|
|
1594
|
+
},
|
|
1595
|
+
row: (columns, y) => {
|
|
1596
|
+
for (let x = 0; x < w; x++) f[x] = columns[y * w + x];
|
|
1597
|
+
envelope(w);
|
|
1598
|
+
return out;
|
|
1599
|
+
},
|
|
1600
|
+
};
|
|
1540
1601
|
}
|
|
1541
1602
|
|
|
1542
1603
|
/**
|
package/src/meshquality.ts
CHANGED
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
type MeshOutline,
|
|
70
70
|
} from './mesh.ts';
|
|
71
71
|
import { areaBand, triangleAreas } from './areaband.ts';
|
|
72
|
-
import { artRastersOf, type ArtRasters } from './meshrasters.ts';
|
|
72
|
+
import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
|
|
73
73
|
import { cropToSpineY } from './transform.ts';
|
|
74
74
|
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
@@ -852,13 +852,15 @@ function pointToBoundary(p: Pt, poly: readonly Pt[]): { d: number; edge: number
|
|
|
852
852
|
* value already found is dropped; the rest are halved. No sampling density is
|
|
853
853
|
* chosen, so nothing here can miss a maximum between two samples.
|
|
854
854
|
*/
|
|
855
|
-
function directedHausdorff(a: readonly Pt[], b: readonly Pt[]): { d: number; edge: number; at: Pt } {
|
|
855
|
+
function directedHausdorff(a: readonly Pt[], b: readonly Pt[], carried: ((p: Pt) => Float64Array) | null = null): { d: number; edge: number; at: Pt } {
|
|
856
856
|
const m = b.length;
|
|
857
|
-
const toEach =
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
857
|
+
const toEach =
|
|
858
|
+
carried ??
|
|
859
|
+
((p: Pt): Float64Array => {
|
|
860
|
+
const out = new Float64Array(m);
|
|
861
|
+
for (let j = 0; j < m; j++) out[j] = distanceToSegment(p, b[j], b[(j + 1) % m]);
|
|
862
|
+
return out;
|
|
863
|
+
});
|
|
862
864
|
const minOf = (g: Float64Array): number => {
|
|
863
865
|
let v = Infinity;
|
|
864
866
|
for (let j = 0; j < m; j++) v = Math.min(v, g[j]);
|
|
@@ -905,13 +907,136 @@ function directedHausdorff(a: readonly Pt[], b: readonly Pt[]): { d: number; edg
|
|
|
905
907
|
* taken on when the worst point is the candidate's, else the candidate edge
|
|
906
908
|
* nearest the reference's worst point.
|
|
907
909
|
*/
|
|
908
|
-
function hausdorff(candidate: readonly Pt[], reference: readonly Pt[]): { d: number; candidateEdge: number } {
|
|
909
|
-
const forward = directedHausdorff(candidate, reference);
|
|
910
|
-
const backward = directedHausdorff(reference, candidate);
|
|
910
|
+
function hausdorff(candidate: readonly Pt[], reference: readonly Pt[], carried: { forward: (p: Pt) => Float64Array; backward: (p: Pt) => Float64Array } | null = null): { d: number; candidateEdge: number } {
|
|
911
|
+
const forward = directedHausdorff(candidate, reference, carried?.forward ?? null);
|
|
912
|
+
const backward = directedHausdorff(reference, candidate, carried?.backward ?? null);
|
|
911
913
|
if (forward.d >= backward.d) return { d: forward.d, candidateEdge: forward.edge };
|
|
912
914
|
return { d: backward.d, candidateEdge: pointToBoundary(backward.at, candidate).edge };
|
|
913
915
|
}
|
|
914
916
|
|
|
917
|
+
/** A point as a key: both coordinates' shortest round-trip decimals, a negative zero told from a positive one. */
|
|
918
|
+
function pointKey(p: Pt): string {
|
|
919
|
+
const k = (v: number): string => (v === 0 && 1 / v < 0 ? '-0' : String(v));
|
|
920
|
+
return `${k(p[0])},${k(p[1])}`;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* `hausdorff`'s point-to-edge distance lists for one outline slot of a
|
|
925
|
+
* reduction, carried from the slot's last reading (`OutlineMemo`, issue #1246).
|
|
926
|
+
* Every list is the one `directedHausdorff` computes — `distanceToSegment(p,
|
|
927
|
+
* b[j], b[(j + 1) % m])` for every edge `j` — had in one of three ways, each
|
|
928
|
+
* the same function of the same input:
|
|
929
|
+
*
|
|
930
|
+
* - **toward the polygon read against** (fixed in a reduction): the list
|
|
931
|
+
* kept for the same point, handed back whole while that polygon is equal;
|
|
932
|
+
* - **toward the outline**, which a step changes: the last reading's list for
|
|
933
|
+
* the same point, each distance copied when its edge — both ends, exactly —
|
|
934
|
+
* is an edge of the last outline, and computed when it is not;
|
|
935
|
+
* - anything else: computed.
|
|
936
|
+
*/
|
|
937
|
+
function carriedDistances(
|
|
938
|
+
memo: OutlineMemo,
|
|
939
|
+
outline: readonly Pt[],
|
|
940
|
+
against: readonly Pt[],
|
|
941
|
+
steps: StepRasters,
|
|
942
|
+
): { forward: (p: Pt) => Float64Array; backward: (p: Pt) => Float64Array; commit: () => void } {
|
|
943
|
+
const { tally, plant } = steps;
|
|
944
|
+
memo.readings++;
|
|
945
|
+
const reading = memo.readings;
|
|
946
|
+
// The polygon read against: keep the lists while it is equal, coordinate by coordinate.
|
|
947
|
+
const againstFlat = new Float64Array(against.length * 2);
|
|
948
|
+
against.forEach((p, i) => {
|
|
949
|
+
againstFlat[i * 2] = p[0];
|
|
950
|
+
againstFlat[i * 2 + 1] = p[1];
|
|
951
|
+
});
|
|
952
|
+
const sameAgainst = memo.against !== null && memo.against.length === againstFlat.length && memo.against.every((v, i) => Object.is(v, againstFlat[i]));
|
|
953
|
+
if (!sameAgainst) {
|
|
954
|
+
memo.toAgainst.clear();
|
|
955
|
+
memo.against = againstFlat;
|
|
956
|
+
}
|
|
957
|
+
const ma = against.length;
|
|
958
|
+
const forward = (p: Pt): Float64Array => {
|
|
959
|
+
tally.distanceLists++;
|
|
960
|
+
const key = pointKey(p);
|
|
961
|
+
const kept = memo.toAgainst.get(key);
|
|
962
|
+
if (kept !== undefined) {
|
|
963
|
+
kept.used = reading;
|
|
964
|
+
tally.distanceListsReused++;
|
|
965
|
+
return kept.list;
|
|
966
|
+
}
|
|
967
|
+
const list = new Float64Array(ma);
|
|
968
|
+
for (let j = 0; j < ma; j++) list[j] = distanceToSegment(p, against[j], against[(j + 1) % ma]);
|
|
969
|
+
tally.edgesComputed += ma;
|
|
970
|
+
memo.toAgainst.set(key, { list, used: reading });
|
|
971
|
+
return list;
|
|
972
|
+
};
|
|
973
|
+
// The outline's edges, and where each sat in the last outline.
|
|
974
|
+
const mo = outline.length;
|
|
975
|
+
const edges = new Map<string, number>();
|
|
976
|
+
const from = new Int32Array(mo);
|
|
977
|
+
const lastEdges = memo.outlineEdges;
|
|
978
|
+
let plantPending = plant === 'skip-an-edge';
|
|
979
|
+
for (let j = 0; j < mo; j++) {
|
|
980
|
+
const key = `${pointKey(outline[j])};${pointKey(outline[(j + 1) % mo])}`;
|
|
981
|
+
edges.set(key, j);
|
|
982
|
+
const was = lastEdges?.get(key);
|
|
983
|
+
from[j] = was ?? -1;
|
|
984
|
+
if (was === undefined && plantPending && lastEdges !== null && j < lastEdges.size) {
|
|
985
|
+
// The plant: one new edge read as if it were the last outline's edge at the same index.
|
|
986
|
+
plantPending = false;
|
|
987
|
+
from[j] = j;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const last = memo.toOutline;
|
|
991
|
+
const next = new Map<string, Float64Array>();
|
|
992
|
+
const backward = (p: Pt): Float64Array => {
|
|
993
|
+
tally.distanceLists++;
|
|
994
|
+
const key = pointKey(p);
|
|
995
|
+
const done = next.get(key);
|
|
996
|
+
if (done !== undefined) {
|
|
997
|
+
tally.distanceListsReused++;
|
|
998
|
+
return done;
|
|
999
|
+
}
|
|
1000
|
+
const before = last.get(key);
|
|
1001
|
+
const list = new Float64Array(mo);
|
|
1002
|
+
if (before === undefined) {
|
|
1003
|
+
for (let j = 0; j < mo; j++) list[j] = distanceToSegment(p, outline[j], outline[(j + 1) % mo]);
|
|
1004
|
+
tally.edgesComputed += mo;
|
|
1005
|
+
} else {
|
|
1006
|
+
tally.distanceListsCarried++;
|
|
1007
|
+
for (let j = 0; j < mo; j++) {
|
|
1008
|
+
if (from[j] >= 0) {
|
|
1009
|
+
list[j] = before[from[j]];
|
|
1010
|
+
tally.edgesCarried++;
|
|
1011
|
+
} else {
|
|
1012
|
+
list[j] = distanceToSegment(p, outline[j], outline[(j + 1) % mo]);
|
|
1013
|
+
tally.edgesComputed++;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
next.set(key, list);
|
|
1018
|
+
return list;
|
|
1019
|
+
};
|
|
1020
|
+
return {
|
|
1021
|
+
forward,
|
|
1022
|
+
backward,
|
|
1023
|
+
commit: () => {
|
|
1024
|
+
// Keep this reading's outline lists for the next one; drop the lists toward the fixed polygon the last two readings did not ask for.
|
|
1025
|
+
memo.toOutline = next;
|
|
1026
|
+
memo.outlineEdges = edges;
|
|
1027
|
+
for (const [key, entry] of memo.toAgainst) if (entry.used < reading - 1) memo.toAgainst.delete(key);
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** `hausdorff` through a reduction's carried distances (`carriedDistances`), dropping what the last two readings of the slot did not ask for. */
|
|
1033
|
+
function hausdorffCarried(memo: OutlineMemo, candidate: readonly Pt[], reference: readonly Pt[], steps: StepRasters): { d: number; candidateEdge: number } {
|
|
1034
|
+
const carried = carriedDistances(memo, candidate, reference, steps);
|
|
1035
|
+
const value = hausdorff(candidate, reference, carried);
|
|
1036
|
+
carried.commit();
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
915
1040
|
// ---------------------------------------------------------------------------
|
|
916
1041
|
// rows
|
|
917
1042
|
// ---------------------------------------------------------------------------
|
|
@@ -1008,7 +1133,7 @@ function compareRows(a: MeasureRow, b: MeasureRow): number {
|
|
|
1008
1133
|
*/
|
|
1009
1134
|
export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
|
|
1010
1135
|
validateInput(input);
|
|
1011
|
-
return measureValidated(input, artRastersOf(input.art));
|
|
1136
|
+
return measureValidated(input, artRastersOf(input.art), null);
|
|
1012
1137
|
}
|
|
1013
1138
|
|
|
1014
1139
|
/**
|
|
@@ -1027,7 +1152,74 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
|
|
|
1027
1152
|
export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
|
|
1028
1153
|
validateInput(input);
|
|
1029
1154
|
checkArtRasters(input, rasters);
|
|
1030
|
-
return measureValidated(input, rasters);
|
|
1155
|
+
return measureValidated(input, rasters, null);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* `measureMeshQualityWith` carried from the last measurement the same
|
|
1160
|
+
* `StepRasters` made (`src/meshrasters.ts`, issue #1246) — what `reduceMesh`
|
|
1161
|
+
* calls for each step, so a step redraws only the triangles its removal
|
|
1162
|
+
* changed, redoes the distance transform only where the coverage flipped, and
|
|
1163
|
+
* reads the outline rows again only when the outline moved. The report is the
|
|
1164
|
+
* one `measureMeshQuality` returns for the same input, byte for byte; rasters
|
|
1165
|
+
* taken from another art are refused as `measureMeshQualityWith` refuses them.
|
|
1166
|
+
*
|
|
1167
|
+
* Internal, as `measureMeshQualityWith` is.
|
|
1168
|
+
*/
|
|
1169
|
+
export function measureMeshQualityStep(input: MeshMeasureInput, steps: StepRasters): MeshQualityReport {
|
|
1170
|
+
validateInput(input);
|
|
1171
|
+
checkArtRasters(input, steps.rasters);
|
|
1172
|
+
return measureValidated(input, steps.rasters, steps);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* The coverage reading of a triangle set measured on its own — the full path:
|
|
1177
|
+
* every triangle drawn (`rasteriseTriangles`), the whole distance transform,
|
|
1178
|
+
* and the covered art and islands counted over every pixel.
|
|
1179
|
+
*/
|
|
1180
|
+
function coverageOf(onGrid: Array<[number, number]>, triangles: number[], w: number, h: number, rasters: ArtRasters): CoverageReading {
|
|
1181
|
+
const artBits = rasters.artBits();
|
|
1182
|
+
const covered = rasteriseTriangles(onGrid, triangles, w, h);
|
|
1183
|
+
let coveredArt = 0;
|
|
1184
|
+
for (let i = 0; i < artBits.length; i++) if (artBits[i] && covered[i]) coveredArt++;
|
|
1185
|
+
const toCovered = squaredDistanceToSet(covered, w, h);
|
|
1186
|
+
const anyCovered = covered.some((c) => c === 1);
|
|
1187
|
+
let undercutAt = -1;
|
|
1188
|
+
let undercutSq = 0;
|
|
1189
|
+
for (let i = 0; i < artBits.length; i++) {
|
|
1190
|
+
if (!artBits[i] || covered[i]) continue;
|
|
1191
|
+
if (undercutAt === -1 || toCovered[i] > undercutSq) {
|
|
1192
|
+
undercutAt = i;
|
|
1193
|
+
undercutSq = toCovered[i];
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const { label } = rasters.islands();
|
|
1197
|
+
const touched = new Set<number>();
|
|
1198
|
+
for (let i = 0; i < label.length; i++) if (label[i] && covered[i]) touched.add(label[i]);
|
|
1199
|
+
const secondIsland = touched.size >= 2 ? [...touched].sort((p, q) => p - q)[1] : 0;
|
|
1200
|
+
const silhouette = (connectivity: 4 | 8): SilhouetteReading => {
|
|
1201
|
+
const fills = rasters.fills();
|
|
1202
|
+
const filled = connectivity === 8 ? fills.fill8 : fills.fill4;
|
|
1203
|
+
const toFilled = rasters.toFilled(connectivity);
|
|
1204
|
+
let overAt = -1;
|
|
1205
|
+
let overSq = 0;
|
|
1206
|
+
let holes = 0;
|
|
1207
|
+
let holeAt = -1;
|
|
1208
|
+
for (let i = 0; i < covered.length; i++) {
|
|
1209
|
+
if (!covered[i]) continue;
|
|
1210
|
+
if (!filled[i]) {
|
|
1211
|
+
if (overAt === -1 || toFilled[i] > overSq) {
|
|
1212
|
+
overAt = i;
|
|
1213
|
+
overSq = toFilled[i];
|
|
1214
|
+
}
|
|
1215
|
+
} else if (!artBits[i]) {
|
|
1216
|
+
holes++;
|
|
1217
|
+
if (holeAt === -1) holeAt = i;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return { overAt, overSq, holes, holeAt };
|
|
1221
|
+
};
|
|
1222
|
+
return { covered, toCovered, coveredArt, anyCovered, undercut: { at: undercutAt, sq: undercutSq }, silhouette, islandsTouched: touched.size, secondIsland };
|
|
1031
1223
|
}
|
|
1032
1224
|
|
|
1033
1225
|
/**
|
|
@@ -1053,7 +1245,7 @@ function checkArtRasters(input: MeshMeasureInput, rasters: ArtRasters): void {
|
|
|
1053
1245
|
}
|
|
1054
1246
|
|
|
1055
1247
|
/** The measurement proper, over an input `validateInput` accepted and rasters taken from its art. */
|
|
1056
|
-
function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
|
|
1248
|
+
function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null): MeshQualityReport {
|
|
1057
1249
|
rasters.tally.uses++;
|
|
1058
1250
|
const { attachment, art, source, targets } = input;
|
|
1059
1251
|
const { mask, threshold, frame } = art;
|
|
@@ -1099,7 +1291,6 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQua
|
|
|
1099
1291
|
const artBits = rasters.artBits();
|
|
1100
1292
|
const artCount = rasters.artCount();
|
|
1101
1293
|
const onGrid: Array<[number, number]> = source.points.map(([x, y]) => [x * scale, y * scale]);
|
|
1102
|
-
const covered = rasteriseTriangles(onGrid, source.triangles, w, h);
|
|
1103
1294
|
const fit = targets.artFit;
|
|
1104
1295
|
const pxIncrement = 1 / scale;
|
|
1105
1296
|
const rasterArt = (connectivity: 4 | 8 | null): NonNullable<MeasureRow['art']> => ({ threshold, connectivity, samples: artCount });
|
|
@@ -1112,21 +1303,12 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQua
|
|
|
1112
1303
|
rows.push(withRaster(unmeasuredRow(spec('MQ_HOLES', 'count'), 'not-measurable', why, false), rasterArt(8), grid, 1));
|
|
1113
1304
|
rows.push(withRaster(unmeasuredRow(spec('MQ_ISLANDS', 'count'), 'not-measurable', why, false), rasterArt(4), grid, 1));
|
|
1114
1305
|
} else {
|
|
1115
|
-
// Coverage — `measureAuthoredMeshFit`'s, unchanged: art pixel centres a triangle covers.
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
const anyCovered = covered.some((c) => c === 1);
|
|
1306
|
+
// Coverage — `measureAuthoredMeshFit`'s, unchanged: art pixel centres a triangle covers. A reduction step
|
|
1307
|
+
// carries it from its last measurement (`StepRasters`), which reads the same values.
|
|
1308
|
+
const reading = steps === null ? coverageOf(onGrid, source.triangles, w, h, rasters) : steps.coverage(onGrid, source.triangles);
|
|
1309
|
+
const { coveredArt, anyCovered } = reading;
|
|
1120
1310
|
// Undercut: the furthest uncovered art pixel from the covered set. Its pixel is coverage's worst too.
|
|
1121
|
-
|
|
1122
|
-
let undercutSq = 0;
|
|
1123
|
-
for (let i = 0; i < artBits.length; i++) {
|
|
1124
|
-
if (!artBits[i] || covered[i]) continue;
|
|
1125
|
-
if (undercutAt === -1 || toCovered[i] > undercutSq) {
|
|
1126
|
-
undercutAt = i;
|
|
1127
|
-
undercutSq = toCovered[i];
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1311
|
+
const { at: undercutAt, sq: undercutSq } = reading.undercut;
|
|
1130
1312
|
const missingWorst: WorstSample = undercutAt === -1 ? NOTHING_WORSE : { at: { pixel: pixelOf(undercutAt) } };
|
|
1131
1313
|
rows.push(
|
|
1132
1314
|
withRaster(
|
|
@@ -1150,25 +1332,9 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQua
|
|
|
1150
1332
|
|
|
1151
1333
|
// Overshoot and holes against the filled silhouette: 8-connected background over ALL art (P12) — and,
|
|
1152
1334
|
// where the legacy 4-connected fill differs (a diagonal pinch), the labelled 4-connected reading beside it.
|
|
1153
|
-
const
|
|
1154
|
-
const silhouetteRows = (
|
|
1155
|
-
const
|
|
1156
|
-
let overAt = -1;
|
|
1157
|
-
let overSq = 0;
|
|
1158
|
-
let holes = 0;
|
|
1159
|
-
let holeAt = -1;
|
|
1160
|
-
for (let i = 0; i < covered.length; i++) {
|
|
1161
|
-
if (!covered[i]) continue;
|
|
1162
|
-
if (!filled[i]) {
|
|
1163
|
-
if (overAt === -1 || toFilled[i] > overSq) {
|
|
1164
|
-
overAt = i;
|
|
1165
|
-
overSq = toFilled[i];
|
|
1166
|
-
}
|
|
1167
|
-
} else if (!artBits[i]) {
|
|
1168
|
-
holes++;
|
|
1169
|
-
if (holeAt === -1) holeAt = i;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1335
|
+
const fillsDiffer = rasters.fills().differ;
|
|
1336
|
+
const silhouetteRows = (connectivity: 4 | 8, gated: boolean): void => {
|
|
1337
|
+
const { overAt, overSq, holes, holeAt } = reading.silhouette(connectivity);
|
|
1172
1338
|
const bound = gated && fit !== null ? { op: '<=' as const, value: fit.maxOvershoot } : null;
|
|
1173
1339
|
rows.push(
|
|
1174
1340
|
withRaster(
|
|
@@ -1180,21 +1346,15 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQua
|
|
|
1180
1346
|
);
|
|
1181
1347
|
rows.push(withRaster(measuredRow(spec('MQ_HOLES', 'count'), holes, null, holeAt === -1 ? NOTHING_WORSE : { at: { pixel: pixelOf(holeAt) } }, false), rasterArt(connectivity), grid, 1));
|
|
1182
1348
|
};
|
|
1183
|
-
silhouetteRows(
|
|
1184
|
-
if (fillsDiffer) silhouetteRows(
|
|
1349
|
+
silhouetteRows(8, true);
|
|
1350
|
+
if (fillsDiffer) silhouetteRows(4, false);
|
|
1185
1351
|
|
|
1186
1352
|
// Islands: the 4-connected art islands (the tracer's, `labelIslands`) the triangles cover a pixel of.
|
|
1187
1353
|
const { label } = rasters.islands();
|
|
1188
|
-
const
|
|
1189
|
-
for (let i = 0; i < label.length; i++) if (label[i] && covered[i]) touched.add(label[i]);
|
|
1190
|
-
let joinedAt = -1;
|
|
1191
|
-
if (touched.size >= 2) {
|
|
1192
|
-
const second = [...touched].sort((p, q) => p - q)[1];
|
|
1193
|
-
joinedAt = label.indexOf(second);
|
|
1194
|
-
}
|
|
1354
|
+
const joinedAt = reading.islandsTouched >= 2 ? label.indexOf(reading.secondIsland) : -1;
|
|
1195
1355
|
rows.push(
|
|
1196
1356
|
withRaster(
|
|
1197
|
-
measuredRow(spec('MQ_ISLANDS', 'count'),
|
|
1357
|
+
measuredRow(spec('MQ_ISLANDS', 'count'), reading.islandsTouched, null, joinedAt === -1 ? NOTHING_WORSE : { at: { pixel: pixelOf(joinedAt) } }, false),
|
|
1198
1358
|
{ threshold, connectivity: 4, samples: artCount },
|
|
1199
1359
|
grid,
|
|
1200
1360
|
1,
|
|
@@ -1208,14 +1368,16 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters): MeshQua
|
|
|
1208
1368
|
if (input.referenceHull === null) {
|
|
1209
1369
|
rows.push(unmeasuredRow(boundarySpec, 'not-measurable', `attachment ${nameOf(attachment)}: no referenceHull was given, so there is no polygon to measure the hull's deviation from`, targets.maxBoundaryDeviation !== null));
|
|
1210
1370
|
} else {
|
|
1211
|
-
const
|
|
1371
|
+
const reference = input.referenceHull;
|
|
1372
|
+
const hd = steps === null ? hausdorff(hullPolygon, reference) : steps.outline('reference', hullPolygon, reference, (memo) => hausdorffCarried(memo, hullPolygon, reference, steps));
|
|
1212
1373
|
const bound = targets.maxBoundaryDeviation === null ? null : { op: '<=' as const, value: targets.maxBoundaryDeviation };
|
|
1213
1374
|
rows.push(measuredRow(boundarySpec, r6(hd.d), bound, hd.d === 0 ? NOTHING_WORSE : { at: { edge: edgeOfHull(hd.candidateEdge) } }, true));
|
|
1214
1375
|
}
|
|
1215
1376
|
const traceSpec = spec('MQ_TRACE_DEVIATION', 'px');
|
|
1216
1377
|
const traced = rasters.traced();
|
|
1217
1378
|
if ('outline' in traced) {
|
|
1218
|
-
const
|
|
1379
|
+
const tracedOutline = traced.outline;
|
|
1380
|
+
const hd = steps === null ? hausdorff(hullPolygon, tracedOutline) : steps.outline('traced', hullPolygon, tracedOutline, (memo) => hausdorffCarried(memo, hullPolygon, tracedOutline, steps));
|
|
1219
1381
|
rows.push(measuredRow(traceSpec, r6(hd.d), null, hd.d === 0 ? NOTHING_WORSE : { at: { edge: edgeOfHull(hd.candidateEdge) } }, false));
|
|
1220
1382
|
} else {
|
|
1221
1383
|
rows.push(unmeasuredRow(traceSpec, 'not-measurable', `attachment ${nameOf(attachment)}: the tracer refused the art at alpha >= ${threshold}: ${traced.refused}`, false));
|
package/src/meshrasters.ts
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Not re-exported by `src/mesh.ts`: nothing here is on `spine-rigc/mesh`.
|
|
35
35
|
*/
|
|
36
|
-
import { artOf, fillEnclosed, labelIslands, MeshError, prunePolygon, squaredDistanceToSet, traceAlphaOutline } from './mesh.ts';
|
|
36
|
+
import { artOf, distancePassesOf, fillEnclosed, labelIslands, MeshError, pixelCentreInTriangle, prunePolygon, squaredDistanceToSet, traceAlphaOutline, triangleRasterBox } from './mesh.ts';
|
|
37
37
|
import type { ArtInput } from './meshquality.ts';
|
|
38
38
|
|
|
39
39
|
/** How many times each quantity was computed, and how many measurements were handed the object. */
|
|
@@ -154,3 +154,468 @@ export function artRastersOf(art: ArtInput): ArtRasters {
|
|
|
154
154
|
},
|
|
155
155
|
};
|
|
156
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
|
+
}
|
package/src/meshreduce.ts
CHANGED
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
* Each candidate step is measured by `measureMeshQuality` (`src/meshquality.ts`)
|
|
27
27
|
* — no row is re-implemented here; the call reads it as `measureMeshQualityWith`
|
|
28
28
|
* over the art's rasters taken once at admission (`src/meshrasters.ts`, issue
|
|
29
|
-
* #1240),
|
|
29
|
+
* #1240), and as `measureMeshQualityStep` carried from the last step's
|
|
30
|
+
* measurement (`StepRasters`, issue #1246), each of which returns the same
|
|
31
|
+
* report — and taken only when every row the caller's
|
|
30
32
|
* contract requires is `pass`: the art fit at `targets.artFit`, the boundary
|
|
31
33
|
* deviation from the source hull at `targets.maxBoundaryDeviation`, the minimum
|
|
32
34
|
* angle when declared, orientation and degeneracy at 0, and every region's
|
|
@@ -87,6 +89,7 @@ import {
|
|
|
87
89
|
import {
|
|
88
90
|
BAND_CONTACT_TOLERANCE,
|
|
89
91
|
edgeIsHeldByRegion,
|
|
92
|
+
measureMeshQualityStep,
|
|
90
93
|
measureMeshQualityWith,
|
|
91
94
|
type AttachmentRef,
|
|
92
95
|
type ArtFitBounds,
|
|
@@ -103,7 +106,7 @@ import {
|
|
|
103
106
|
type SourceMesh,
|
|
104
107
|
type Termination,
|
|
105
108
|
} from './meshquality.ts';
|
|
106
|
-
import { artRastersOf, type ArtRasters } from './meshrasters.ts';
|
|
109
|
+
import { artRastersOf, stepRastersOf, type ArtRasters, type StepRasters } from './meshrasters.ts';
|
|
107
110
|
|
|
108
111
|
// ---------------------------------------------------------------------------
|
|
109
112
|
// the result
|
|
@@ -716,6 +719,8 @@ interface Run {
|
|
|
716
719
|
sourceEdges: Set<string>;
|
|
717
720
|
/** The art's rasters, taken once for the call and read by every measurement in it (issue #1240). */
|
|
718
721
|
rasters: ArtRasters;
|
|
722
|
+
/** The step state each removal and insertion is measured through, carried from the last measurement (issue #1246); null measures every step in full. */
|
|
723
|
+
stepRasters: StepRasters | null;
|
|
719
724
|
}
|
|
720
725
|
|
|
721
726
|
/** A measurement of a canonical mesh against the result's full contract. */
|
|
@@ -736,7 +741,7 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string): MeshQual
|
|
|
736
741
|
boneOrder: input.boneOrder,
|
|
737
742
|
preset: input.preset,
|
|
738
743
|
};
|
|
739
|
-
return measureMeshQualityWith(measureInput, run.rasters);
|
|
744
|
+
return run.stepRasters === null ? measureMeshQualityWith(measureInput, run.rasters) : measureMeshQualityStep(measureInput, run.stepRasters);
|
|
740
745
|
}
|
|
741
746
|
|
|
742
747
|
/** The order constraints are named in when several fail on one step: structure, then shape, then art, then density. */
|
|
@@ -1172,7 +1177,8 @@ function tryRemoval(run: Run, v: number): string | null {
|
|
|
1172
1177
|
*/
|
|
1173
1178
|
export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
|
|
1174
1179
|
validateReduction(input);
|
|
1175
|
-
|
|
1180
|
+
const rasters = artRastersOf(input.art);
|
|
1181
|
+
return reduceValidated(input, rasters, stepRastersOf(rasters));
|
|
1176
1182
|
}
|
|
1177
1183
|
|
|
1178
1184
|
/**
|
|
@@ -1180,19 +1186,29 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
|
|
|
1180
1186
|
* `src/meshrasters.ts`) — the result is `reduceMesh`'s for the same input, byte
|
|
1181
1187
|
* for byte, and every measurement of the call reads the one object, so its
|
|
1182
1188
|
* `tally` is the count of what the call computed. Rasters taken from another
|
|
1183
|
-
* art are refused at admission (`REDUCE_ART_RASTERS_MISMATCH`).
|
|
1189
|
+
* art are refused at admission (`REDUCE_ART_RASTERS_MISMATCH`). Each step is
|
|
1190
|
+
* measured through `steps` (`stepRastersOf(rasters)` unless given), carried
|
|
1191
|
+
* from the last measurement (issue #1246); `null` measures every step in full,
|
|
1192
|
+
* which is the path the carried one is held equal to. Step rasters made over
|
|
1193
|
+
* another rasters object are refused by the same code.
|
|
1184
1194
|
*
|
|
1185
1195
|
* Internal: it is on `spine-rigc/mesh` only because that entry re-exports this
|
|
1186
1196
|
* module with `export *`, and a symbol that is merely exported is not promised
|
|
1187
1197
|
* (RELEASING.md, *The import surface*).
|
|
1188
1198
|
*/
|
|
1189
|
-
export function reduceMeshWith(input: MeshReductionInput, rasters: ArtRasters): MeshReductionResult {
|
|
1199
|
+
export function reduceMeshWith(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null = stepRastersOf(rasters)): MeshReductionResult {
|
|
1190
1200
|
validateReduction(input);
|
|
1191
|
-
|
|
1201
|
+
if (steps !== null && steps.rasters !== rasters) {
|
|
1202
|
+
refuse('REDUCE_ART_RASTERS_MISMATCH', `attachment ${nameOf(input.attachment)}: the step rasters were made over another rasters object; required step rasters made over the rasters passed beside them (stepRastersOf(rasters))`);
|
|
1203
|
+
}
|
|
1204
|
+
return reduceValidated(input, rasters, steps);
|
|
1192
1205
|
}
|
|
1193
1206
|
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
1207
|
+
/**
|
|
1208
|
+
* The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in
|
|
1209
|
+
* `rasters`, and every refinement and removal step is measured through `steps` when it is given (issue #1246).
|
|
1210
|
+
*/
|
|
1211
|
+
function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null): MeshReductionResult {
|
|
1196
1212
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1197
1213
|
const src = input.source;
|
|
1198
1214
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
@@ -1273,6 +1289,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters): MeshRe
|
|
|
1273
1289
|
...protectionOf(input, work, sourceEdges),
|
|
1274
1290
|
sourceEdges,
|
|
1275
1291
|
rasters,
|
|
1292
|
+
stepRasters: steps,
|
|
1276
1293
|
};
|
|
1277
1294
|
|
|
1278
1295
|
const refined = refineRegions(run);
|