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