spine-rigc 2.32.0 → 2.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/meshcarriers.ts +80 -8
- package/src/meshcompare.ts +11 -5
- package/src/meshreduce.ts +161 -25
- package/src/meshskinning.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.32.
|
|
3
|
+
"version": "2.32.2",
|
|
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/meshcarriers.ts
CHANGED
|
@@ -30,12 +30,27 @@ export interface Carrier {
|
|
|
30
30
|
bary: [number, number, number];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Issue #1323's faults in resolving several carriers, for the `mesh-compare` suite's controls: the rule before the
|
|
35
|
+
* issue (the per-coordinate support alone, `slackSupport` never read); the slack read as the narrowest triangle's
|
|
36
|
+
* rather than the widest's; and any shared vertex excused whatever the distance, so two triangles that both hold a
|
|
37
|
+
* sample well inside — an overlap — carry it by the first.
|
|
38
|
+
*/
|
|
39
|
+
export type CarrierPlant = 'slack-support-unread' | 'narrowest-slack' | 'any-shared-vertex';
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
|
|
35
43
|
* Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
|
|
36
|
-
* other than across a vertex or edge they share
|
|
44
|
+
* other than across a vertex or edge they share — where "across" is read, since
|
|
45
|
+
* issue #1323, to the distance the containment slack itself allows
|
|
46
|
+
* (`slackSupport`).
|
|
37
47
|
*/
|
|
38
48
|
export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
|
|
49
|
+
return uvCarriersWith(uvs, triangles, samples, who, null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `uvCarriers` with a fault planted in resolving several carriers (`CarrierPlant`). Internal: on no entry. */
|
|
53
|
+
export function uvCarriersWith(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string, plant: CarrierPlant | null): Array<Carrier | null> {
|
|
39
54
|
const areas = triangleAreas(uvs, triangles);
|
|
40
55
|
const band = areaBand(areas, uvs);
|
|
41
56
|
const live: number[] = [];
|
|
@@ -101,7 +116,8 @@ export function uvCarriers(uvs: readonly number[], triangles: readonly number[],
|
|
|
101
116
|
}
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
|
-
|
|
119
|
+
const uvOf = (v: number): readonly [number, number] => [uvs[v * 2], uvs[v * 2 + 1]];
|
|
120
|
+
return samples.map((s, j) => resolveCarriers(s.uv[0], s.uv[1], hits[j] ?? [], who, uvOf, plant));
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
/**
|
|
@@ -143,21 +159,32 @@ export function carrierIn(
|
|
|
143
159
|
|
|
144
160
|
/**
|
|
145
161
|
* The carrier of a sample from every triangle that contains it: none, the one, or — several — the first when they all
|
|
146
|
-
* meet there at a shared vertex or edge; else `COMPARE_UV_CARRIER_NOT_UNIQUE` (correction 4).
|
|
162
|
+
* meet there at a shared vertex or edge; else `COMPARE_UV_CARRIER_NOT_UNIQUE` (correction 4). Where the per-coordinate
|
|
163
|
+
* support refuses, the support is read again to the slack's distance (issue #1323, {@link slackSupport}) when the
|
|
164
|
+
* corners' UVs are given (`uvOf`).
|
|
147
165
|
*/
|
|
148
|
-
export function resolveCarriers(
|
|
166
|
+
export function resolveCarriers(
|
|
167
|
+
px: number,
|
|
168
|
+
py: number,
|
|
169
|
+
hits: readonly Carrier[],
|
|
170
|
+
who: string,
|
|
171
|
+
uvOf: ((v: number) => readonly [number, number]) | null = null,
|
|
172
|
+
plant: CarrierPlant | null = null,
|
|
173
|
+
): Carrier | null {
|
|
149
174
|
if (hits.length === 0) return null;
|
|
150
175
|
if (hits.length === 1) return hits[0];
|
|
151
176
|
// Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
|
|
152
177
|
// it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
|
|
153
|
-
const
|
|
178
|
+
const shared = (supportOf: (c: Carrier) => string): boolean => {
|
|
179
|
+
const first = supportOf(hits[0]);
|
|
180
|
+
return first.split(',').length <= 2 && hits.every((c) => supportOf(c) === first);
|
|
181
|
+
};
|
|
182
|
+
const byCoordinate = (c: Carrier): string =>
|
|
154
183
|
c.corners
|
|
155
184
|
.filter((_v, k) => c.bary[k] > CONTAINS)
|
|
156
185
|
.sort((a, b) => a - b)
|
|
157
186
|
.join(',');
|
|
158
|
-
|
|
159
|
-
const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
|
|
160
|
-
if (!shared) {
|
|
187
|
+
if (!shared(byCoordinate) && (uvOf === null || plant === 'slack-support-unread' || !shared(slackSupport(hits, uvOf, plant)))) {
|
|
161
188
|
refuse(
|
|
162
189
|
'COMPARE_UV_CARRIER_NOT_UNIQUE',
|
|
163
190
|
`${who}: the sample at uv (${px}, ${py}) lies in ${hits.length} UV triangles — ${hits.map((c) => `triangle ${c.triangle} (vertices ${c.corners.join(', ')})`).join(' and ')} — that do not meet there at a shared vertex or edge; required one carrier per sample (correction 4: overlapping or folded UV triangles would carry it by an arbitrary one)`,
|
|
@@ -165,3 +192,48 @@ export function resolveCarriers(px: number, py: number, hits: readonly Carrier[]
|
|
|
165
192
|
}
|
|
166
193
|
return hits[0];
|
|
167
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Issue #1323: a carrier's support read to the slack's distance rather than per coordinate — the corners the sample
|
|
198
|
+
* lies further than `CONTAINS × H` (UV units) inside of, measured from each corner's opposite edge, with `H` the widest
|
|
199
|
+
* triangle height among the hits: the furthest outside a triangle the containment test (`carrierIn`) admitted any of
|
|
200
|
+
* them at this sample.
|
|
201
|
+
*
|
|
202
|
+
* Why: the slack is per barycentric coordinate, so in distance it is `CONTAINS` times the height of the corner it is
|
|
203
|
+
* read against. A sample on a shared edge, a hair to one side — a pixel centre on a 45° or axis-parallel edge between
|
|
204
|
+
* whole-pixel vertices, put off it by the UVs' rounding — is admitted by the triangle it is outside of, while the
|
|
205
|
+
* triangle it is inside of, if it is a sliver, reads its third corner's coordinate above `CONTAINS` because its height
|
|
206
|
+
* is small: two per-coordinate supports, read as an overlap though the sample is within the slack of the edge both
|
|
207
|
+
* share. Measured on demo/bottomwear's reduction at source spacing 18: 1.35e-10 uv from the edge, coordinates −3.2e-10
|
|
208
|
+
* and +6.5e-8, heights 0.42 and 0.0021. Read in distance, both corners are within the slack and the support is the
|
|
209
|
+
* shared edge. A sample two triangles both hold further inside than that — an overlap or a fold — is still refused.
|
|
210
|
+
*/
|
|
211
|
+
function slackSupport(hits: readonly Carrier[], uvOf: (v: number) => readonly [number, number], plant: CarrierPlant | null): (c: Carrier) => string {
|
|
212
|
+
if (plant === 'any-shared-vertex') {
|
|
213
|
+
const common = hits[0].corners.filter((v) => hits.every((c) => c.corners.includes(v)));
|
|
214
|
+
return () => (common.length > 0 ? [...common].sort((a, b) => a - b).slice(0, 1).join(',') : 'none');
|
|
215
|
+
}
|
|
216
|
+
const heights = new Map<Carrier, [number, number, number]>();
|
|
217
|
+
let widest = 0;
|
|
218
|
+
let narrowest = Infinity;
|
|
219
|
+
for (const c of hits) {
|
|
220
|
+
const p = c.corners.map(uvOf);
|
|
221
|
+
const twice = Math.abs((p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]));
|
|
222
|
+
const h = [0, 1, 2].map((k) => {
|
|
223
|
+
const a = p[(k + 1) % 3];
|
|
224
|
+
const b = p[(k + 2) % 3];
|
|
225
|
+
return twice / Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
226
|
+
}) as [number, number, number];
|
|
227
|
+
heights.set(c, h);
|
|
228
|
+
widest = Math.max(widest, ...h);
|
|
229
|
+
narrowest = Math.min(narrowest, ...h);
|
|
230
|
+
}
|
|
231
|
+
const slack = CONTAINS * (plant === 'narrowest-slack' ? narrowest : widest);
|
|
232
|
+
return (c: Carrier): string => {
|
|
233
|
+
const h = heights.get(c)!;
|
|
234
|
+
return c.corners
|
|
235
|
+
.filter((_v, k) => c.bary[k] * h[k] > slack)
|
|
236
|
+
.sort((a, b) => a - b)
|
|
237
|
+
.join(',');
|
|
238
|
+
};
|
|
239
|
+
}
|
package/src/meshcompare.ts
CHANGED
|
@@ -104,7 +104,7 @@ import {
|
|
|
104
104
|
type WorstSample,
|
|
105
105
|
} from './meshquality.ts';
|
|
106
106
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
107
|
-
import { uvCarriers, type Carrier } from './meshcarriers.ts';
|
|
107
|
+
import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
|
|
108
108
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
109
109
|
import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
110
110
|
import { sampleTime } from './core/animation.ts';
|
|
@@ -186,7 +186,9 @@ export interface MotionComparisonInput {
|
|
|
186
186
|
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
187
187
|
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
188
188
|
* before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
|
|
189
|
-
* amplitude moves a verdict. `compareMeshesInMotion` plants none.
|
|
189
|
+
* amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1323 adds `CarrierPlant`
|
|
190
|
+
* (`src/meshcarriers.ts`): a fault in resolving a sample's several carriers, planted in every `uvCarriers` the
|
|
191
|
+
* comparison reads.
|
|
190
192
|
*/
|
|
191
193
|
export type ComparePlant =
|
|
192
194
|
| 'amplitude-not-carried'
|
|
@@ -194,7 +196,8 @@ export type ComparePlant =
|
|
|
194
196
|
| 'amplitude-unvalidated'
|
|
195
197
|
| 'echo-when-unset'
|
|
196
198
|
| 'amplitude-gates-acceptance'
|
|
197
|
-
| OverBoundPlant
|
|
199
|
+
| OverBoundPlant
|
|
200
|
+
| CarrierPlant;
|
|
198
201
|
|
|
199
202
|
/**
|
|
200
203
|
* Issue #1315's faults in `overBound`, for the `mesh-compare` suite's negative controls: a listed sample's triangle
|
|
@@ -995,11 +998,14 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
995
998
|
}
|
|
996
999
|
return { samples: out, art };
|
|
997
1000
|
});
|
|
998
|
-
const
|
|
1001
|
+
const carrierPlant: CarrierPlant | null = plant === 'slack-support-unread' || plant === 'narrowest-slack' || plant === 'any-shared-vertex' ? plant : null;
|
|
1002
|
+
const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
|
|
1003
|
+
uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
|
|
1004
|
+
const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
999
1005
|
const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks);
|
|
1000
1006
|
|
|
1001
1007
|
motionOf = (b: Build, isReference: boolean) => {
|
|
1002
|
-
const carriers = isReference ? refCarriers : order.map((i, k) =>
|
|
1008
|
+
const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
|
|
1003
1009
|
const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks);
|
|
1004
1010
|
const built: Built[] = [];
|
|
1005
1011
|
const perFrame: NonNullable<CandidateReport['perFrame']> = [];
|
package/src/meshreduce.ts
CHANGED
|
@@ -160,6 +160,8 @@ import {
|
|
|
160
160
|
type Termination,
|
|
161
161
|
} from './meshquality.ts';
|
|
162
162
|
import { artRastersOf, stepRastersOf, type ArtRasters, type StepRasters } from './meshrasters.ts';
|
|
163
|
+
import { areaBand, triangleAreas } from './areaband.ts';
|
|
164
|
+
import { cropToSpineY } from './transform.ts';
|
|
163
165
|
import {
|
|
164
166
|
skinningContract,
|
|
165
167
|
SkinningCarry,
|
|
@@ -1262,43 +1264,154 @@ function addVertex(run: Run, p: Pt): number {
|
|
|
1262
1264
|
return id;
|
|
1263
1265
|
}
|
|
1264
1266
|
|
|
1267
|
+
/**
|
|
1268
|
+
* The two triangles a split of the edge a–b at the new vertex `m` makes of `tri`, winding kept, or null when `tri`
|
|
1269
|
+
* does not have that edge — the one rule `splitEdge` writes and `degenerateInsertion` reads (issue #1311).
|
|
1270
|
+
*/
|
|
1271
|
+
function splitPieces(tri: readonly [number, number, number], a: number, b: number, m: number): [[number, number, number], [number, number, number]] | null {
|
|
1272
|
+
const i = tri.indexOf(a);
|
|
1273
|
+
const j = tri.indexOf(b);
|
|
1274
|
+
if (i === -1 || j === -1) return null;
|
|
1275
|
+
// Rotate so the edge is the triangle's first two corners, in its own winding.
|
|
1276
|
+
const r = (j - i + 3) % 3 === 1 ? i : j;
|
|
1277
|
+
const x = tri[r];
|
|
1278
|
+
const y = tri[(r + 1) % 3];
|
|
1279
|
+
const c = tri[(r + 2) % 3];
|
|
1280
|
+
return [[x, m, c], [m, y, c]];
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1265
1283
|
/** Split the edge between working ids a and b at `p`: each triangle on it becomes two, winding kept. */
|
|
1266
1284
|
function splitEdge(run: Run, a: number, b: number, p: Pt): void {
|
|
1267
1285
|
const m = addVertex(run, p);
|
|
1268
1286
|
const next: Array<[number, number, number]> = [];
|
|
1269
1287
|
for (const tri of run.work.triangles) {
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
next.push(tri);
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
// Rotate so the edge is the triangle's first two corners, in its own winding.
|
|
1277
|
-
const r = (j - i + 3) % 3 === 1 ? i : j;
|
|
1278
|
-
const x = tri[r];
|
|
1279
|
-
const y = tri[(r + 1) % 3];
|
|
1280
|
-
const c = tri[(r + 2) % 3];
|
|
1281
|
-
next.push([x, m, c], [m, y, c]);
|
|
1288
|
+
const pieces = splitPieces(tri, a, b, m);
|
|
1289
|
+
if (pieces === null) next.push(tri);
|
|
1290
|
+
else next.push(...pieces);
|
|
1282
1291
|
}
|
|
1283
1292
|
run.work.triangles = next;
|
|
1284
1293
|
}
|
|
1285
1294
|
|
|
1286
|
-
/**
|
|
1287
|
-
|
|
1295
|
+
/** Where `insertPoint` puts `p`: on an edge of the working triangle that holds it, inside that triangle, or nowhere. */
|
|
1296
|
+
type Placement = { kind: 'edge'; a: number; b: number } | { kind: 'inside'; tri: [number, number, number] } | null;
|
|
1297
|
+
|
|
1298
|
+
function placementOf(run: Run, p: Pt): Placement {
|
|
1288
1299
|
const { work } = run;
|
|
1289
1300
|
for (const tri of work.triangles) {
|
|
1290
1301
|
const l = barycentric(p, work.pos[tri[0]], work.pos[tri[1]], work.pos[tri[2]]);
|
|
1291
1302
|
if (l === null || Math.min(...l) < -1e-9) continue;
|
|
1292
1303
|
const onEdge = l.findIndex((v) => Math.abs(v) <= 1e-9);
|
|
1293
|
-
if (onEdge !== -1) {
|
|
1294
|
-
|
|
1295
|
-
return true;
|
|
1296
|
-
}
|
|
1297
|
-
const m = addVertex(run, p);
|
|
1298
|
-
work.triangles = work.triangles.flatMap((t): Array<[number, number, number]> => (t === tri ? [[tri[0], tri[1], m], [tri[1], tri[2], m], [tri[2], tri[0], m]] : [t]));
|
|
1299
|
-
return true;
|
|
1304
|
+
if (onEdge !== -1) return { kind: 'edge', a: tri[(onEdge + 1) % 3], b: tri[(onEdge + 2) % 3] };
|
|
1305
|
+
return { kind: 'inside', tri };
|
|
1300
1306
|
}
|
|
1301
|
-
return
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** The three triangles an insertion of the new vertex `m` inside `tri` makes of it — `insertPoint`'s rule. */
|
|
1311
|
+
function fanPieces(tri: readonly [number, number, number], m: number): Array<[number, number, number]> {
|
|
1312
|
+
return [[tri[0], tri[1], m], [tri[1], tri[2], m], [tri[2], tri[0], m]];
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Insert `p` where `placementOf` put it: on an edge, that edge is split; inside, the triangle becomes three. */
|
|
1316
|
+
function insertPoint(run: Run, p: Pt, at: Exclude<Placement, null>): void {
|
|
1317
|
+
const { work } = run;
|
|
1318
|
+
if (at.kind === 'edge') {
|
|
1319
|
+
splitEdge(run, at.a, at.b, p);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const m = addVertex(run, p);
|
|
1323
|
+
work.triangles = work.triangles.flatMap((t): Array<[number, number, number]> => (t === at.tri ? fanPieces(at.tri, m) : [t]));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** The vertex id the degenerate check reads for the vertex an insertion would add, before it exists. */
|
|
1327
|
+
const NEW_VERTEX = -1;
|
|
1328
|
+
|
|
1329
|
+
/** A triangle an insertion would write that `MQ_DEGENERATE` reads as degenerate (issue #1311). */
|
|
1330
|
+
interface DegenerateWrite {
|
|
1331
|
+
/** Working ids, `NEW_VERTEX` for the inserted one, in the triangle's own winding. */
|
|
1332
|
+
corners: [number, number, number];
|
|
1333
|
+
/** The smallest magnitude of its area over the three corners it can be read from, px². */
|
|
1334
|
+
area: number;
|
|
1335
|
+
/** `areaBand` of the mesh the insertion would leave, px². */
|
|
1336
|
+
band: number;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Issue #1311: the first triangle an insertion of `p` would write whose area `MQ_DEGENERATE` reads as degenerate, or
|
|
1341
|
+
* null. Read as the row reads it, over the mesh the insertion would leave: every point in the art frame's world (y up,
|
|
1342
|
+
* `cropToSpineY` on the frame's height), every triangle's area by `triangleAreas`, and the band by `areaBand` over all of
|
|
1343
|
+
* them — the larger of 1e-6 of the largest triangle and the float32 noise bound of the coordinates. The row reads a
|
|
1344
|
+
* triangle from its smallest canonical index, which is not known before the mesh is canonicalised, so a fresh triangle
|
|
1345
|
+
* is read from each of its three corners and the smallest magnitude is the one compared: a triangle that any reading
|
|
1346
|
+
* puts within the band is refused. `fresh` uses `NEW_VERTEX` for the inserted vertex; `replaced` are the triangles it
|
|
1347
|
+
* takes the place of.
|
|
1348
|
+
*/
|
|
1349
|
+
function degenerateInsertion(run: Run, p: Pt, replaced: ReadonlySet<readonly number[]>, fresh: ReadonlyArray<[number, number, number]>): DegenerateWrite | null {
|
|
1350
|
+
const { work } = run;
|
|
1351
|
+
const height = run.input.art.frame.height;
|
|
1352
|
+
const slot = new Map<number, number>();
|
|
1353
|
+
const world: number[] = [];
|
|
1354
|
+
for (let id = 0; id < work.pos.length; id++) {
|
|
1355
|
+
if (!work.alive[id]) continue;
|
|
1356
|
+
slot.set(id, world.length / 2);
|
|
1357
|
+
world.push(work.pos[id][0], cropToSpineY(work.pos[id][1], height));
|
|
1358
|
+
}
|
|
1359
|
+
slot.set(NEW_VERTEX, world.length / 2);
|
|
1360
|
+
world.push(p[0], cropToSpineY(p[1], height));
|
|
1361
|
+
const flat: number[] = [];
|
|
1362
|
+
for (const tri of work.triangles) if (!replaced.has(tri)) for (const id of tri) flat.push(slot.get(id)!);
|
|
1363
|
+
for (const tri of fresh) for (const id of tri) flat.push(slot.get(id)!);
|
|
1364
|
+
const band = areaBand(triangleAreas(world, flat), world) * (run.plant === 'degenerate-band-widened' ? 1000 : 1);
|
|
1365
|
+
for (const tri of fresh) {
|
|
1366
|
+
const [i, j, k] = tri.map((id) => slot.get(id)!);
|
|
1367
|
+
const area = Math.min(...triangleAreas(world, [i, j, k, j, k, i, k, i, j]).map(Math.abs));
|
|
1368
|
+
if (area <= band) return { corners: tri, area, band };
|
|
1369
|
+
}
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Issue #1311: the refinement's stop when the insertion its rule chose would write a triangle `MQ_DEGENERATE` refuses —
|
|
1375
|
+
* by name: the target row, the region, the insertion, the triangle's corners with their distance from the region's
|
|
1376
|
+
* polygon, its area and the band required. The refinement writes no triangle the row refuses: a refined source that
|
|
1377
|
+
* failed that row could take no step at all, and the failure would name a triangle rather than the insertion that made
|
|
1378
|
+
* it.
|
|
1379
|
+
*/
|
|
1380
|
+
function degenerateStop(run: Run, canon: Canonical, target: MeasureRow, region: RefinementRegion, what: string, p: Pt, found: DegenerateWrite): PhaseEnd {
|
|
1381
|
+
const corner = (id: number): string => {
|
|
1382
|
+
const at = id === NEW_VERTEX ? p : run.work.pos[id];
|
|
1383
|
+
const name = id === NEW_VERTEX ? 'the inserted vertex' : `vertex ${canon.indexOf.get(id)!}`;
|
|
1384
|
+
const where = inClosedPolygon(at, region.polygon) ? 'in the polygon' : `${r6(distanceToBoundary(at, region.polygon))} px from the polygon`;
|
|
1385
|
+
return `${name} (${at[0]}, ${at[1]}), ${where}`;
|
|
1386
|
+
};
|
|
1387
|
+
return {
|
|
1388
|
+
kind: 'stuck',
|
|
1389
|
+
constraint:
|
|
1390
|
+
`${rowName(target)} (${what} at (${p[0]}, ${p[1]}) would write the triangle of ${found.corners.map(corner).join('; ')} — ` +
|
|
1391
|
+
`area ${r6(found.area)} px², which MQ_DEGENERATE reads as degenerate: required an area above the row's band ${r6(found.band)} px² ` +
|
|
1392
|
+
`(the larger of the float32 noise bound of the mesh's coordinates and 1e-6 of its largest triangle); ` +
|
|
1393
|
+
`region "${region.name}" has a ${region.transition} px band, and the refinement writes no triangle the row refuses)`,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** The triangles a split of a–b replaces, and what it writes in their place with `NEW_VERTEX` inserted. */
|
|
1398
|
+
function splitPlan(run: Run, a: number, b: number): { replaced: Set<readonly number[]>; fresh: Array<[number, number, number]> } {
|
|
1399
|
+
const replaced = new Set<readonly number[]>();
|
|
1400
|
+
const fresh: Array<[number, number, number]> = [];
|
|
1401
|
+
for (const tri of run.work.triangles) {
|
|
1402
|
+
const pieces = splitPieces(tri, a, b, NEW_VERTEX);
|
|
1403
|
+
if (pieces === null) continue;
|
|
1404
|
+
replaced.add(tri);
|
|
1405
|
+
fresh.push(...pieces);
|
|
1406
|
+
}
|
|
1407
|
+
return { replaced, fresh };
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Issue #1311: what a split of a–b at `p` would write that the row refuses, or null — always null under the control's plant. */
|
|
1411
|
+
function degenerateSplit(run: Run, a: number, b: number, p: Pt): DegenerateWrite | null {
|
|
1412
|
+
if (run.plant === 'refinement-writes-degenerate') return null;
|
|
1413
|
+
const { replaced, fresh } = splitPlan(run, a, b);
|
|
1414
|
+
return degenerateInsertion(run, p, replaced, fresh);
|
|
1302
1415
|
}
|
|
1303
1416
|
|
|
1304
1417
|
type PhaseEnd =
|
|
@@ -1346,7 +1459,15 @@ export type ReductionPlant =
|
|
|
1346
1459
|
| 'floor-half-a-pixel-short'
|
|
1347
1460
|
| RetriangulationPlant
|
|
1348
1461
|
| OrderPlant
|
|
1349
|
-
| AmplitudePlant
|
|
1462
|
+
| AmplitudePlant
|
|
1463
|
+
| RefinementPlant;
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Issue #1311's faults in the refinement: the insertion its rule chose written without asking whether it writes a
|
|
1467
|
+
* triangle `MQ_DEGENERATE` refuses — the refinement as it was before the check; and the check reading the row's band
|
|
1468
|
+
* a thousand times as wide, so it refuses insertions the row would pass.
|
|
1469
|
+
*/
|
|
1470
|
+
export type RefinementPlant = 'refinement-writes-degenerate' | 'degenerate-band-widened';
|
|
1350
1471
|
|
|
1351
1472
|
/**
|
|
1352
1473
|
* Issue #1283's faults in the triangulation post-pass: the pass run on a call that did not opt in; the pass skipped
|
|
@@ -1400,7 +1521,9 @@ export type AttemptObserver = (attempt: AttemptRecord) => void;
|
|
|
1400
1521
|
* `MQ_TRANSITION` that the refinement can act on passes. One insertion per
|
|
1401
1522
|
* measurement: the first failing region row in report order has its worst edge
|
|
1402
1523
|
* split. A band no edge lies in leaves `MQ_TRANSITION` not-measurable (B1's
|
|
1403
|
-
* definition) and is not something an insertion is aimed at.
|
|
1524
|
+
* definition) and is not something an insertion is aimed at. An insertion that
|
|
1525
|
+
* would write a triangle `MQ_DEGENERATE` refuses is not made: the refinement
|
|
1526
|
+
* stops there by name (issue #1311, `degenerateStop`).
|
|
1404
1527
|
*/
|
|
1405
1528
|
function refineRegions(run: Run): PhaseEnd {
|
|
1406
1529
|
const regions = new Map(run.input.targets.regions.map((r) => [r.name, r]));
|
|
@@ -1416,7 +1539,16 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1416
1539
|
const region = regions.get(target.object.region!)!;
|
|
1417
1540
|
if (target.state === 'not-measurable') {
|
|
1418
1541
|
const p: Pt = [r6(region.polygon[0][0]), r6(region.polygon[0][1])];
|
|
1419
|
-
|
|
1542
|
+
const at = placementOf(run, p);
|
|
1543
|
+
if (at === null) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no triangle holding the region's first vertex)` };
|
|
1544
|
+
const found =
|
|
1545
|
+
run.plant === 'refinement-writes-degenerate'
|
|
1546
|
+
? null
|
|
1547
|
+
: at.kind === 'edge'
|
|
1548
|
+
? degenerateSplit(run, at.a, at.b, p)
|
|
1549
|
+
: degenerateInsertion(run, p, new Set([at.tri]), fanPieces(at.tri, NEW_VERTEX));
|
|
1550
|
+
if (found !== null) return degenerateStop(run, canon, target, region, `inserting region "${region.name}"'s first vertex`, p, found);
|
|
1551
|
+
insertPoint(run, p, at);
|
|
1420
1552
|
followSkinning(run, 'insertion');
|
|
1421
1553
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1422
1554
|
continue;
|
|
@@ -1432,6 +1564,8 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1432
1564
|
if (inRegionOrBand(run.work.pos[outer], region)) continue;
|
|
1433
1565
|
const q = outerSplit(run.work.pos[inner], run.work.pos[outer], region);
|
|
1434
1566
|
if (q === null) continue;
|
|
1567
|
+
const found = degenerateSplit(run, a, b, q);
|
|
1568
|
+
if (found !== null) return degenerateStop(run, canon, target, region, `splitting edge ${ra}–${rb} where it leaves region "${region.name}"'s band`, q, found);
|
|
1435
1569
|
splitEdge(run, a, b, q);
|
|
1436
1570
|
followSkinning(run, 'insertion');
|
|
1437
1571
|
exited = true;
|
|
@@ -1462,6 +1596,8 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1462
1596
|
}
|
|
1463
1597
|
const p = splitPoint(run.work.pos[a], run.work.pos[b], region);
|
|
1464
1598
|
if (p === null) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no point of edge ${ra}–${rb} strictly between its ends inside region "${region.name}" or its band)` };
|
|
1599
|
+
const found = degenerateSplit(run, a, b, p);
|
|
1600
|
+
if (found !== null) return degenerateStop(run, canon, target, region, `splitting edge ${ra}–${rb} inside region "${region.name}" or its band`, p, found);
|
|
1465
1601
|
splitEdge(run, a, b, p);
|
|
1466
1602
|
followSkinning(run, 'insertion');
|
|
1467
1603
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
package/src/meshskinning.ts
CHANGED
|
@@ -1235,7 +1235,7 @@ export class SkinningCarry {
|
|
|
1235
1235
|
}
|
|
1236
1236
|
let carrier: Carrier | null;
|
|
1237
1237
|
try {
|
|
1238
|
-
carrier = resolveCarriers(this.su[j], this.sv[j], hits, `${this.who}: candidate (working vertex ids)
|
|
1238
|
+
carrier = resolveCarriers(this.su[j], this.sv[j], hits, `${this.who}: candidate (working vertex ids)`, (v) => this.mesh.uv[v]);
|
|
1239
1239
|
} catch (err) {
|
|
1240
1240
|
if (!(err instanceof MeshReductionError)) throw err;
|
|
1241
1241
|
if (pending.refused === null) pending.refused = `SKINNING_UV_CARRIER_NOT_UNIQUE: ${err.message}`;
|