spine-rigc 2.20.3 → 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.
- package/package.json +1 -1
- package/src/meshquality.ts +63 -17
- package/src/meshrasters.ts +326 -3
- package/src/meshreduce.ts +17 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.20.
|
|
3
|
+
"version": "2.20.4",
|
|
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": {
|
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, type CoverageReading, type OutlineMemo, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
|
|
72
|
+
import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type RegionEdgeReading, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
|
|
73
73
|
import { cropToSpineY } from './transform.ts';
|
|
74
74
|
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
@@ -1427,7 +1427,7 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
|
|
|
1427
1427
|
);
|
|
1428
1428
|
|
|
1429
1429
|
// --- regions (§5) ------------------------------------------------------------------------
|
|
1430
|
-
rows.push(...regionRows(input, outline, hullPolygon, rasters));
|
|
1430
|
+
rows.push(...regionRows(input, outline, hullPolygon, rasters, steps));
|
|
1431
1431
|
|
|
1432
1432
|
const ordered = rows.slice().sort((a, b) => compareRows(a.row, b.row));
|
|
1433
1433
|
const geometry = sectionOf(ordered);
|
|
@@ -1477,7 +1477,7 @@ function sectionOf(built: readonly Built[]): EvidenceSection {
|
|
|
1477
1477
|
// ---------------------------------------------------------------------------
|
|
1478
1478
|
|
|
1479
1479
|
/** A region's own refusal (§2's `refused`: this measurement's input is invalid), or null when it is measurable. */
|
|
1480
|
-
function regionRefusal(region: RefinementRegion, pageScale: number, who: string): { code: string; message: string } | null {
|
|
1480
|
+
function regionRefusal(region: RefinementRegion, pageScale: number, who: string, steps: StepRasters | null): { code: string; message: string } | null {
|
|
1481
1481
|
const numbers: Array<[string, unknown]> = [
|
|
1482
1482
|
['maxEdgeLength', region.maxEdgeLength],
|
|
1483
1483
|
['transition', region.transition],
|
|
@@ -1495,7 +1495,8 @@ function regionRefusal(region: RefinementRegion, pageScale: number, who: string)
|
|
|
1495
1495
|
message: `${who}, region "${region.name}": maxEdgeLength is ${region.maxEdgeLength} px; required at least one texel, ${r6(1 / pageScale)} px at pageScale ${pageScale}`,
|
|
1496
1496
|
};
|
|
1497
1497
|
}
|
|
1498
|
-
|
|
1498
|
+
// A function of the polygon alone: a reduction step has it handed back by its `StepRasters` (issue #1253).
|
|
1499
|
+
const crossing = steps === null ? findSelfIntersection(region.polygon) : steps.polygonCrossing(region.polygon, () => findSelfIntersection(region.polygon));
|
|
1499
1500
|
if (crossing !== null) {
|
|
1500
1501
|
return {
|
|
1501
1502
|
code: 'REGION_SELF_INTERSECTS',
|
|
@@ -1524,8 +1525,13 @@ interface Edge {
|
|
|
1524
1525
|
* distance from that region. A row's value is the edge whose
|
|
1525
1526
|
* length most exceeds its own bound — so the row fails exactly when some edge
|
|
1526
1527
|
* it covers is over — and the row names that edge and that bound.
|
|
1528
|
+
*
|
|
1529
|
+
* `MQ_FILL_DISTANCE` reads the art pixels whose centre lies in the region's
|
|
1530
|
+
* closed polygon, a function of the art and the polygon alone: a reduction
|
|
1531
|
+
* step has it handed back by its `StepRasters` (`regionPixels`, issue #1253)
|
|
1532
|
+
* rather than testing every art pixel against the polygon again.
|
|
1527
1533
|
*/
|
|
1528
|
-
function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon: readonly Pt[], rasters: ArtRasters): Built[] {
|
|
1534
|
+
function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon: readonly Pt[], rasters: ArtRasters, steps: StepRasters | null): Built[] {
|
|
1529
1535
|
const { attachment, source, art } = input;
|
|
1530
1536
|
const who = `attachment ${nameOf(attachment)}`;
|
|
1531
1537
|
const scale = art.frame.pageScale;
|
|
@@ -1541,14 +1547,35 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
|
|
|
1541
1547
|
}
|
|
1542
1548
|
edges.sort((p, q) => p.a - q.a || p.b - q.b);
|
|
1543
1549
|
|
|
1550
|
+
// Each edge's answers for a region: computed here, or — on a reduction step — carried by its `StepRasters` from
|
|
1551
|
+
// the last reading of the region for an edge with the same two ends (`regionEdges`, issue #1253).
|
|
1552
|
+
const readings = new Map<RefinementRegion, RegionEdgeReading | null>();
|
|
1553
|
+
const readingOf = (region: RefinementRegion): RegionEdgeReading | null => {
|
|
1554
|
+
if (!readings.has(region)) readings.set(region, steps === null ? null : steps.regionEdges(region.polygon, region.transition));
|
|
1555
|
+
return readings.get(region)!;
|
|
1556
|
+
};
|
|
1557
|
+
const meetsOf = (region: RefinementRegion, e: Edge): boolean => {
|
|
1558
|
+
const record = readingOf(region)?.entry(points[e.a], points[e.b]);
|
|
1559
|
+
if (record === undefined) return segmentMeetsPolygon(points[e.a], points[e.b], region.polygon);
|
|
1560
|
+
if (record.meets === undefined) record.meets = segmentMeetsPolygon(points[e.a], points[e.b], region.polygon);
|
|
1561
|
+
return record.meets;
|
|
1562
|
+
};
|
|
1563
|
+
const heldOf = (region: RefinementRegion, e: Edge): number | null => {
|
|
1564
|
+
const held = (): number | null => (edgeIsHeldByRegion(points[e.a], points[e.b], region) ? segmentToPolygon(points[e.a], points[e.b], region.polygon) : null);
|
|
1565
|
+
const record = readingOf(region)?.entry(points[e.a], points[e.b]);
|
|
1566
|
+
if (record === undefined) return held();
|
|
1567
|
+
if (record.held === undefined) record.held = held();
|
|
1568
|
+
return record.held;
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1544
1571
|
// Which regions are measurable, and the one refusal of each that is not.
|
|
1545
1572
|
const live: RefinementRegion[] = [];
|
|
1546
1573
|
const refusals = new Map<string, string>();
|
|
1547
1574
|
for (const region of input.targets.regions) {
|
|
1548
|
-
let refusal = regionRefusal(region, scale, who);
|
|
1575
|
+
let refusal = regionRefusal(region, scale, who, steps);
|
|
1549
1576
|
if (refusal === null) {
|
|
1550
1577
|
const poly: Pt[] = region.polygon;
|
|
1551
|
-
const meets = edges.some((e) =>
|
|
1578
|
+
const meets = edges.some((e) => meetsOf(region, e)) || poly.some((p) => inClosedPolygon(p, hullPolygon));
|
|
1552
1579
|
if (!meets) {
|
|
1553
1580
|
refusal = {
|
|
1554
1581
|
code: 'REGION_OUTSIDE_ART',
|
|
@@ -1569,7 +1596,7 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
|
|
|
1569
1596
|
for (const region of live) {
|
|
1570
1597
|
holds.set(
|
|
1571
1598
|
region,
|
|
1572
|
-
edges.map((e) => (
|
|
1599
|
+
edges.map((e) => heldOf(region, e)),
|
|
1573
1600
|
);
|
|
1574
1601
|
}
|
|
1575
1602
|
const edgeIndex = new Map(edges.map((e, i) => [e, i]));
|
|
@@ -1632,23 +1659,42 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
|
|
|
1632
1659
|
}
|
|
1633
1660
|
}
|
|
1634
1661
|
|
|
1635
|
-
// h(R): art pixel centres inside the region clipped to the hull, each to its nearest mesh vertex.
|
|
1662
|
+
// h(R): art pixel centres inside the region clipped to the hull, each to its nearest mesh vertex. The art
|
|
1663
|
+
// pixels inside the region come in ascending order, so the scan below visits what a scan of every art pixel
|
|
1664
|
+
// would keep, in the same order.
|
|
1665
|
+
const centreOf = (i: number): Pt => [((i % mask.width) + 0.5) / scale, (Math.floor(i / mask.width) + 0.5) / scale];
|
|
1666
|
+
const inRegion = (): Int32Array => {
|
|
1667
|
+
const kept: number[] = [];
|
|
1668
|
+
for (let i = 0; i < artBits.length; i++) if (artBits[i] && inClosedPolygon(centreOf(i), poly)) kept.push(i);
|
|
1669
|
+
return Int32Array.from(kept);
|
|
1670
|
+
};
|
|
1671
|
+
const regionPixels = steps === null ? inRegion() : steps.regionPixels(poly, inRegion);
|
|
1636
1672
|
let count = 0;
|
|
1637
1673
|
let worst = -1;
|
|
1638
1674
|
let worstAt = -1;
|
|
1639
|
-
|
|
1640
|
-
if (!artBits[i]) continue;
|
|
1641
|
-
const x = i % mask.width;
|
|
1642
|
-
const y = Math.floor(i / mask.width);
|
|
1643
|
-
const centre: Pt = [(x + 0.5) / scale, (y + 0.5) / scale];
|
|
1644
|
-
if (!inClosedPolygon(centre, poly) || !inClosedPolygon(centre, hullPolygon)) continue;
|
|
1675
|
+
const visit = (i: number, nearest: number): void => {
|
|
1645
1676
|
count++;
|
|
1646
|
-
let nearest = Infinity;
|
|
1647
|
-
for (const p of points) nearest = Math.min(nearest, Math.hypot(p[0] - centre[0], p[1] - centre[1]));
|
|
1648
1677
|
if (nearest > worst) {
|
|
1649
1678
|
worst = nearest;
|
|
1650
1679
|
worstAt = i;
|
|
1651
1680
|
}
|
|
1681
|
+
};
|
|
1682
|
+
const distance = (i: number, p: Pt): number => {
|
|
1683
|
+
const centre = centreOf(i);
|
|
1684
|
+
return Math.hypot(p[0] - centre[0], p[1] - centre[1]);
|
|
1685
|
+
};
|
|
1686
|
+
if (steps === null) {
|
|
1687
|
+
for (const i of regionPixels) {
|
|
1688
|
+
const centre = centreOf(i);
|
|
1689
|
+
if (!inClosedPolygon(centre, hullPolygon)) continue;
|
|
1690
|
+
let nearest = Infinity;
|
|
1691
|
+
for (const p of points) nearest = Math.min(nearest, Math.hypot(p[0] - centre[0], p[1] - centre[1]));
|
|
1692
|
+
visit(i, nearest);
|
|
1693
|
+
}
|
|
1694
|
+
} else {
|
|
1695
|
+
// A reduction step: the pixels in the hull and their nearest distances carried from the last reading (issue #1253).
|
|
1696
|
+
const carried = steps.regionFill(poly, { pixels: regionPixels, centre: centreOf, hull: hullPolygon, points, inHull: (i) => inClosedPolygon(centreOf(i), hullPolygon), distance });
|
|
1697
|
+
for (let j = 0; j < carried.inside.length; j++) visit(carried.inside[j], carried.nearest[j]);
|
|
1652
1698
|
}
|
|
1653
1699
|
const fillSpec = spec('MQ_FILL_DISTANCE', 'px', region.name);
|
|
1654
1700
|
const floor = floorOf(region.name);
|
package/src/meshrasters.ts
CHANGED
|
@@ -215,15 +215,38 @@ export interface StepRasterTally {
|
|
|
215
215
|
/** Edges a carried list took from the last list of its point, and edges it computed because the edge is new. */
|
|
216
216
|
edgesCarried: number;
|
|
217
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;
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
/**
|
|
221
239
|
* A fault planted on purpose in the carried path, for the mesh-quality
|
|
222
240
|
* controls to show they catch it: one added triangle not drawn, one changed
|
|
223
|
-
* column or row of the distance transform not redone,
|
|
224
|
-
* reused when only the vertex counts match
|
|
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.
|
|
225
248
|
*/
|
|
226
|
-
export type StepRasterPlant = 'skip-a-triangle' | 'skip-a-column' | 'skip-a-row' | 'stale-outline' | 'skip-an-edge';
|
|
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';
|
|
227
250
|
|
|
228
251
|
/**
|
|
229
252
|
* One outline slot's carried distances (`src/meshquality.ts`, `hausdorff`): a
|
|
@@ -283,6 +306,38 @@ export interface OutlineMemo {
|
|
|
283
306
|
* with a zero's sign, to the last ones of that slot — the same pure function
|
|
284
307
|
* of the same input.
|
|
285
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
|
+
*
|
|
286
341
|
* A reading whose triangles differ from the last in more triangles than they
|
|
287
342
|
* share is built from nothing, as the first one is; which way a reading is
|
|
288
343
|
* built changes its cost and never its values.
|
|
@@ -300,6 +355,64 @@ export interface StepRasters {
|
|
|
300
355
|
* distances, which `measure` reads and keeps current.
|
|
301
356
|
*/
|
|
302
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;
|
|
303
416
|
}
|
|
304
417
|
|
|
305
418
|
/** A coordinate as a key: its shortest round-trip decimal, with a negative zero told from a positive one. */
|
|
@@ -325,6 +438,19 @@ function flat(p: ReadonlyArray<readonly [number, number]>): Float64Array {
|
|
|
325
438
|
|
|
326
439
|
type Corners = [readonly [number, number], readonly [number, number], readonly [number, number]];
|
|
327
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
|
+
|
|
328
454
|
interface HeldTriangles {
|
|
329
455
|
corners: Corners;
|
|
330
456
|
n: number;
|
|
@@ -367,11 +493,28 @@ export function stepRastersOf(rasters: ArtRasters, plant: StepRasterPlant | null
|
|
|
367
493
|
distanceListsCarried: 0,
|
|
368
494
|
edgesCarried: 0,
|
|
369
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,
|
|
370
509
|
};
|
|
371
510
|
const { width: w, height: h } = rasters.art.mask;
|
|
372
511
|
let state: CoverageState | null = null;
|
|
373
512
|
const outlines = new Map<string, { outline: Float64Array; against: Float64Array; value: unknown }>();
|
|
374
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[] = [];
|
|
375
518
|
|
|
376
519
|
/** Add `sign` to the count of every centre the triangle covers, pushing each such centre to `touched`. */
|
|
377
520
|
const draw = (count: Int32Array, corners: Corners, sign: number, touched: number[] | null): void => {
|
|
@@ -617,5 +760,185 @@ export function stepRastersOf(rasters: ArtRasters, plant: StepRasterPlant | null
|
|
|
617
760
|
outlines.set(slot, { outline: flat(outline), against: flat(against), value });
|
|
618
761
|
return value;
|
|
619
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
|
+
},
|
|
620
943
|
};
|
|
621
944
|
}
|
package/src/meshreduce.ts
CHANGED
|
@@ -1214,24 +1214,23 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1214
1214
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
1215
1215
|
|
|
1216
1216
|
// The source, measured against its own admissibility bounds (correction 3) — this is also what refuses a
|
|
1217
|
-
// malformed art, mesh, region list or sample floor, by measureMeshQuality's own codes.
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
);
|
|
1217
|
+
// malformed art, mesh, region list or sample floor, by measureMeshQuality's own codes. Through the step
|
|
1218
|
+
// rasters when the call carries them, so what a region reads of the art alone is computed once (issue #1253).
|
|
1219
|
+
const admitInput: MeshMeasureInput = {
|
|
1220
|
+
id: 'source',
|
|
1221
|
+
attachment: input.attachment,
|
|
1222
|
+
art: input.art,
|
|
1223
|
+
source: src,
|
|
1224
|
+
targets: { artFit: input.sourceBounds, maxBoundaryDeviation: null, regions: input.targets.regions },
|
|
1225
|
+
referenceHull: null,
|
|
1226
|
+
minArtSamples: input.minArtSamples,
|
|
1227
|
+
regionArtSamples: input.regionArtSamples,
|
|
1228
|
+
protect: input.protect,
|
|
1229
|
+
influences: input.influences,
|
|
1230
|
+
boneOrder: input.boneOrder,
|
|
1231
|
+
preset: input.preset,
|
|
1232
|
+
};
|
|
1233
|
+
const admit = steps === null ? measureMeshQualityWith(admitInput, rasters) : measureMeshQualityStep(admitInput, steps);
|
|
1235
1234
|
const effective = effectiveOf(input, admit.effective, sourceHull);
|
|
1236
1235
|
const sourceCounts = admit.sourceCounts;
|
|
1237
1236
|
const noMesh = (termination: Termination): MeshReductionResult => ({
|