rig-c 2.24.1 → 2.25.0
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 +44 -10
- package/src/meshreduce.ts +227 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
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
|
@@ -236,13 +236,42 @@ export interface MeshReductionInput {
|
|
|
236
236
|
/** Every linked mesh of the source (they inherit the new topology); empty when none. */
|
|
237
237
|
linkedMeshes: AttachmentRef[];
|
|
238
238
|
/**
|
|
239
|
-
* Replay (issue #1268, §7 mechanism 2): stop after the n-th accepted
|
|
240
|
-
* taken, counted as `ReductionChanges.acceptedAt` counts them — and return
|
|
241
|
-
* had
|
|
242
|
-
* number, 0 or more: 0 takes no step. A number the run never reaches leaves
|
|
243
|
-
* that termination says so (`stopAfterAccepted` on it).
|
|
239
|
+
* Replay (issue #1268, §7 mechanism 2): stop after the n-th accepted operation — a refinement insertion, a removal
|
|
240
|
+
* or a boundary run taken, one each, counted as `ReductionChanges.acceptedAt` counts them (issue #1279) — and return
|
|
241
|
+
* the mesh the same call without this field had after it, byte for byte, terminated `replayed-to-accepted-step`.
|
|
242
|
+
* Left out = today's behaviour. A whole number, 0 or more: 0 takes no step. A number the run never reaches leaves
|
|
243
|
+
* the run to end as it would have, and that termination says so (`stopAfterAccepted` on it).
|
|
244
244
|
*/
|
|
245
245
|
stopAfterAccepted?: number;
|
|
246
|
+
/**
|
|
247
|
+
* Boundary runs taken as steps (issue #1279, §8 option (iv)) — opt-in. Left out = no run is tried, and the call is
|
|
248
|
+
* the one it was before the field existed. Set, each removal pass first tries to replace a run of 2 to
|
|
249
|
+
* `maxVertices` consecutive surviving source-hull vertices of the outline with one chord, as one step held to every
|
|
250
|
+
* required row exactly as a single removal is, before that pass's single removals. `maxVertices` is a whole number,
|
|
251
|
+
* 2 or more, and has no default; `null` is refused like any value that is not that object.
|
|
252
|
+
*/
|
|
253
|
+
boundaryRuns?: BoundaryRuns;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Issue #1279: the opt-in to boundary runs — the most consecutive outline vertices one chord may replace. */
|
|
257
|
+
export interface BoundaryRuns {
|
|
258
|
+
/** A whole number, 2 or more: the longest run tried. */
|
|
259
|
+
maxVertices: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Issue #1279: one accepted operation of a reduction, in the order taken — the entry `ReductionChanges.acceptedAt`
|
|
264
|
+
* carries for it and the unit `stopAfterAccepted` counts.
|
|
265
|
+
*/
|
|
266
|
+
export interface AcceptedOperation {
|
|
267
|
+
/** The attempt number — `candidatesTried` as it stood when the operation was taken, 1-based. */
|
|
268
|
+
step: number;
|
|
269
|
+
/** A refinement insertion, one source vertex removed, or a boundary run replaced by one chord. */
|
|
270
|
+
kind: 'insertion' | 'removal' | 'boundary-run';
|
|
271
|
+
/** Vertices the operation inserted (an insertion: 1) or removed (a removal: 1; a boundary run: 2 or more). */
|
|
272
|
+
count: number;
|
|
273
|
+
/** The source indices it removed, in outline order for a run — each `null` in `indexMap`; empty for an insertion. */
|
|
274
|
+
sourceVertices: number[];
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
/**
|
|
@@ -411,11 +440,13 @@ export interface ReductionChanges {
|
|
|
411
440
|
/** Every linked mesh of the source: each inherits the new topology. */
|
|
412
441
|
linkedMeshes: AttachmentRef[];
|
|
413
442
|
/**
|
|
414
|
-
* Issue #1268
|
|
415
|
-
*
|
|
416
|
-
*
|
|
443
|
+
* Issue #1268, one entry per accepted operation since issue #1279: each refinement insertion, each removal and each
|
|
444
|
+
* boundary run taken, in order, with the attempt it was taken at (`step`, strictly ascending), its kind, how many
|
|
445
|
+
* vertices it inserted or removed and which source vertices it removed. The counts sum to
|
|
446
|
+
* `insertedVertices + removedVertices`; with no boundary run every count is 1 and the length is that sum.
|
|
447
|
+
* `stopAfterAccepted: k` replays to the operation at `acceptedAt[k - 1]`.
|
|
417
448
|
*/
|
|
418
|
-
acceptedAt:
|
|
449
|
+
acceptedAt: AcceptedOperation[];
|
|
419
450
|
}
|
|
420
451
|
|
|
421
452
|
export interface MeshQualityReport {
|
|
@@ -461,6 +492,8 @@ export interface EffectiveSettings {
|
|
|
461
492
|
budget: { maxCandidates: number } | null;
|
|
462
493
|
/** A reduction's `stopAfterAccepted`, echoed when the input set it (issue #1268); absent otherwise. */
|
|
463
494
|
stopAfterAccepted?: number;
|
|
495
|
+
/** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
|
|
496
|
+
boundaryRuns?: BoundaryRuns;
|
|
464
497
|
}
|
|
465
498
|
|
|
466
499
|
export interface MeshCounts {
|
|
@@ -1990,6 +2023,7 @@ function effectiveJson(e: EffectiveSettings): Json {
|
|
|
1990
2023
|
schedule: scheduleJson(e.schedule),
|
|
1991
2024
|
budget: e.budget === null ? null : { maxCandidates: e.budget.maxCandidates },
|
|
1992
2025
|
...(e.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: e.stopAfterAccepted }),
|
|
2026
|
+
...(e.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: e.boundaryRuns.maxVertices } }),
|
|
1993
2027
|
};
|
|
1994
2028
|
}
|
|
1995
2029
|
|
|
@@ -2067,7 +2101,7 @@ function candidateJson(c: CandidateReport): Json {
|
|
|
2067
2101
|
deformRemapped: k.deformRemapped.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key, droppedVertices: [...d.droppedVertices] })),
|
|
2068
2102
|
deformReevaluated: k.deformReevaluated.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key })),
|
|
2069
2103
|
linkedMeshes: k.linkedMeshes.map(attachmentJson),
|
|
2070
|
-
acceptedAt: [...
|
|
2104
|
+
acceptedAt: k.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
|
|
2071
2105
|
};
|
|
2072
2106
|
}
|
|
2073
2107
|
return out;
|
package/src/meshreduce.ts
CHANGED
|
@@ -48,6 +48,18 @@
|
|
|
48
48
|
* blocked the last attempt). Inserted vertices are never candidates: they exist
|
|
49
49
|
* to meet `L(R)`, and removing one is undoing the refinement.
|
|
50
50
|
*
|
|
51
|
+
* With `boundaryRuns` (issue #1279, opt-in) each pass first sweeps boundary
|
|
52
|
+
* runs — 2 to `maxVertices` consecutive source-hull vertices of the outline
|
|
53
|
+
* replaced by one chord, as one step held to every row a single removal is —
|
|
54
|
+
* and then the single removals as above (`removeVertices` states the order).
|
|
55
|
+
* Every accepted operation is one `acceptedAt` entry and one unit of
|
|
56
|
+
* `stopAfterAccepted`.
|
|
57
|
+
*
|
|
58
|
+
* A removal whose deviation floor — the furthest removed source-hull vertex
|
|
59
|
+
* from the candidate's outline, which `MQ_BOUNDARY_DEVIATION` can only exceed —
|
|
60
|
+
* is over the bound is refused without the measurement; its name, when read,
|
|
61
|
+
* is the measurement's (`tryOperation`). No result reads differently.
|
|
62
|
+
*
|
|
51
63
|
* The refinement measures, takes the first failing region row in report order
|
|
52
64
|
* (code, then region name) and splits that row's worst edge. When an end of
|
|
53
65
|
* the edge lies outside the region and its band, the split is where the edge
|
|
@@ -92,6 +104,7 @@ import {
|
|
|
92
104
|
edgeIsHeldByRegion,
|
|
93
105
|
measureMeshQualityStep,
|
|
94
106
|
measureMeshQualityWith,
|
|
107
|
+
type AcceptedOperation,
|
|
95
108
|
type AttachmentRef,
|
|
96
109
|
type ArtFitBounds,
|
|
97
110
|
type CandidateReport,
|
|
@@ -264,6 +277,12 @@ function validateReduction(input: MeshReductionInput): void {
|
|
|
264
277
|
if ('stopAfterAccepted' in input && input.stopAfterAccepted !== undefined && (!Number.isInteger(input.stopAfterAccepted) || input.stopAfterAccepted < 0)) {
|
|
265
278
|
refuse('REDUCE_INPUT_MISSING', `${who}: stopAfterAccepted is ${JSON.stringify(input.stopAfterAccepted)}; required a whole number of accepted steps, 0 or more, or the field left out (issue #1268: replay to that step)`);
|
|
266
279
|
}
|
|
280
|
+
if ('boundaryRuns' in input && input.boundaryRuns !== undefined) {
|
|
281
|
+
const runs: unknown = input.boundaryRuns;
|
|
282
|
+
if (!isObject(runs) || !Number.isInteger(runs.maxVertices) || (runs.maxVertices as number) < 2) {
|
|
283
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: boundaryRuns is ${JSON.stringify(runs)}; required { maxVertices: a whole number, 2 or more } — the longest run one chord may replace, which has no default — or the field left out (issue #1279: no run is tried)`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
267
286
|
const src = input.source;
|
|
268
287
|
if (!isObject(src) || !Array.isArray(src.points)) refuse('REDUCE_INPUT_MISSING', `${who}: source is not { points, uvs, triangles, hull, weights }`);
|
|
269
288
|
const n = src.points.length;
|
|
@@ -733,10 +752,12 @@ interface Run {
|
|
|
733
752
|
sourceHull: Array<[number, number]>;
|
|
734
753
|
/** Steps tried so far, refinement insertions and removal attempts alike. */
|
|
735
754
|
steps: number;
|
|
736
|
-
/** Issue #1268
|
|
737
|
-
acceptedAt:
|
|
738
|
-
/** The
|
|
739
|
-
plant:
|
|
755
|
+
/** Issue #1268, per operation since #1279: every insertion made, removal taken and boundary run taken, in order. */
|
|
756
|
+
acceptedAt: AcceptedOperation[];
|
|
757
|
+
/** The fault planted for a control, or null. */
|
|
758
|
+
plant: ReductionPlant | null;
|
|
759
|
+
/** What a control reads of every removal-phase attempt, or null. */
|
|
760
|
+
observe: AttemptObserver | null;
|
|
740
761
|
tally: Tally;
|
|
741
762
|
keyed: Map<number, { animation: string; ref: AttachmentRef; key: number; time: number }>;
|
|
742
763
|
protectedVertices: Set<number>;
|
|
@@ -975,8 +996,12 @@ type PhaseEnd =
|
|
|
975
996
|
* straight after the step, before the budget is read again, so the replay stops in exactly the state the
|
|
976
997
|
* unreplayed run held after that step.
|
|
977
998
|
*/
|
|
978
|
-
function accept(run: Run): boolean {
|
|
979
|
-
run.
|
|
999
|
+
function accept(run: Run, kind: AcceptedOperation['kind'], sourceVertices: number[]): boolean {
|
|
1000
|
+
if (kind === 'boundary-run' && run.plant === 'run-split-per-vertex') {
|
|
1001
|
+
for (const v of sourceVertices) run.acceptedAt.push({ step: run.steps, kind: 'removal', count: 1, sourceVertices: [v] });
|
|
1002
|
+
} else {
|
|
1003
|
+
run.acceptedAt.push({ step: run.steps, kind, count: kind === 'insertion' ? 1 : sourceVertices.length, sourceVertices: [...sourceVertices] });
|
|
1004
|
+
}
|
|
980
1005
|
const stop = run.input.stopAfterAccepted;
|
|
981
1006
|
if (stop === undefined) return false;
|
|
982
1007
|
const at = run.plant === 'stop-one-early' ? stop - 1 : run.plant === 'stop-one-late' ? stop + 1 : stop;
|
|
@@ -989,6 +1014,29 @@ function accept(run: Run): boolean {
|
|
|
989
1014
|
*/
|
|
990
1015
|
export type ReplayPlant = 'stop-one-early' | 'stop-one-late';
|
|
991
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Every fault a control plants in a reduction (`reduceMeshWith`'s `plant`); `reduceMesh` plants none. Beside the
|
|
1019
|
+
* replay's two (issue #1268), issue #1279's: boundary runs tried by a call that did not opt in; a run recorded as one
|
|
1020
|
+
* `removal` entry per vertex (the shape before #1279); a run taken without its rows measured; every candidate
|
|
1021
|
+
* measured, with no deviation floor (the path the floor is held equal to); and the floor read half a pixel short
|
|
1022
|
+
* of the bound, so it refuses steps the rows would take.
|
|
1023
|
+
*/
|
|
1024
|
+
export type ReductionPlant = ReplayPlant | 'runs-without-opt-in' | 'run-split-per-vertex' | 'run-skips-rows' | 'measure-every-candidate' | 'floor-half-a-pixel-short';
|
|
1025
|
+
|
|
1026
|
+
/** One removal-phase attempt as a control reads it (issue #1279): what was tried, and what refused it or null when taken. */
|
|
1027
|
+
export interface AttemptRecord {
|
|
1028
|
+
step: number;
|
|
1029
|
+
kind: 'removal' | 'boundary-run';
|
|
1030
|
+
sourceVertices: number[];
|
|
1031
|
+
/** Null when taken; else reads the name of what refused it — measuring the attempt in full when the floor decided it. Read it only inside the observer, while the working mesh is the one the attempt was tried on. */
|
|
1032
|
+
refusedBy: (() => string) | null;
|
|
1033
|
+
/** Whether the deviation floor refused it without a measurement. */
|
|
1034
|
+
decidedByFloor: boolean;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** Called once per removal-phase attempt, after it. */
|
|
1038
|
+
export type AttemptObserver = (attempt: AttemptRecord) => void;
|
|
1039
|
+
|
|
992
1040
|
/**
|
|
993
1041
|
* Refine the working mesh until every region's `MQ_MAX_EDGE` and
|
|
994
1042
|
* `MQ_TRANSITION` that the refinement can act on passes. One insertion per
|
|
@@ -1011,7 +1059,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1011
1059
|
if (target.state === 'not-measurable') {
|
|
1012
1060
|
const p: Pt = [r6(region.polygon[0][0]), r6(region.polygon[0][1])];
|
|
1013
1061
|
if (!insertPoint(run, p)) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no triangle holding the region's first vertex)` };
|
|
1014
|
-
if (accept(run)) return { kind: 'replayed' };
|
|
1062
|
+
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1015
1063
|
continue;
|
|
1016
1064
|
}
|
|
1017
1065
|
const [ra, rb] = target.worst!.at.edge!;
|
|
@@ -1030,7 +1078,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1030
1078
|
break;
|
|
1031
1079
|
}
|
|
1032
1080
|
if (exited) {
|
|
1033
|
-
if (accept(run)) return { kind: 'replayed' };
|
|
1081
|
+
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1034
1082
|
continue;
|
|
1035
1083
|
}
|
|
1036
1084
|
// An end outside the region and its band further beyond it than the edge's own bound, with no split on
|
|
@@ -1055,7 +1103,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1055
1103
|
const p = splitPoint(run.work.pos[a], run.work.pos[b], region);
|
|
1056
1104
|
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)` };
|
|
1057
1105
|
splitEdge(run, a, b, p);
|
|
1058
|
-
if (accept(run)) return { kind: 'replayed' };
|
|
1106
|
+
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1059
1107
|
}
|
|
1060
1108
|
}
|
|
1061
1109
|
|
|
@@ -1131,39 +1179,169 @@ function weightJump(work: Work, a: number, b: number): number {
|
|
|
1131
1179
|
return sum;
|
|
1132
1180
|
}
|
|
1133
1181
|
|
|
1182
|
+
/**
|
|
1183
|
+
* Several boundary vertices removed one after another as one operation (issue #1279): the working triangles after
|
|
1184
|
+
* the last and the edges they hold that the triangles before the first did not, in the order they appear — or the
|
|
1185
|
+
* structural reason one of the removals cannot be made. The outline loses the run and gains the chord from the
|
|
1186
|
+
* vertex before it to the vertex after it; nothing is measured in between.
|
|
1187
|
+
*/
|
|
1188
|
+
function runRemovalOf(work: Work, vertices: readonly number[]): { triangles: Array<[number, number, number]>; added: Array<[number, number]> } | { blocked: string } {
|
|
1189
|
+
const was = work.triangles;
|
|
1190
|
+
let after: Array<[number, number, number]>;
|
|
1191
|
+
try {
|
|
1192
|
+
for (const v of vertices) {
|
|
1193
|
+
const step = removalOf(work, v);
|
|
1194
|
+
if ('blocked' in step) return step;
|
|
1195
|
+
work.triangles = step.triangles;
|
|
1196
|
+
}
|
|
1197
|
+
after = work.triangles;
|
|
1198
|
+
} finally {
|
|
1199
|
+
work.triangles = was;
|
|
1200
|
+
}
|
|
1201
|
+
const old = new Set<string>();
|
|
1202
|
+
for (const t of was) for (let k = 0; k < 3; k++) old.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
1203
|
+
const added: Array<[number, number]> = [];
|
|
1204
|
+
const seen = new Set<string>();
|
|
1205
|
+
for (const t of after) {
|
|
1206
|
+
for (let k = 0; k < 3; k++) {
|
|
1207
|
+
const key = edgeKey(t[k], t[(k + 1) % 3]);
|
|
1208
|
+
if (old.has(key) || seen.has(key)) continue;
|
|
1209
|
+
seen.add(key);
|
|
1210
|
+
added.push([t[k], t[(k + 1) % 3]]);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return { triangles: after, added };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Why an attempt was refused: the constraint's name, or — when the deviation floor refused it without a
|
|
1218
|
+
* measurement — a thunk that measures the attempt in full and returns the name the measurement gives, so whatever
|
|
1219
|
+
* reads it reads what an unfloored run would have written.
|
|
1220
|
+
*/
|
|
1221
|
+
type Refusal = string | (() => string);
|
|
1222
|
+
|
|
1223
|
+
function refusalText(refusal: Refusal): string {
|
|
1224
|
+
return typeof refusal === 'string' ? refusal : refusal();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* The surviving source vertices on the current outline, walked the way the source's own hull listing turns (the
|
|
1229
|
+
* canonical order's hull) — the order a boundary run follows.
|
|
1230
|
+
*/
|
|
1231
|
+
function outlineWalk(run: Run): number[] {
|
|
1232
|
+
const canon = canonicalise(run.work, run.sourceTurn, run.boneRank);
|
|
1233
|
+
return canon.order.slice(0, canon.mesh.hull);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1134
1236
|
/**
|
|
1135
1237
|
* Remove surviving source vertices in ascending source index, pass after pass,
|
|
1136
1238
|
* taking a step only when every structural condition and every required row
|
|
1137
1239
|
* holds after it. Ends when a pass takes no step, or when the budget is spent.
|
|
1240
|
+
*
|
|
1241
|
+
* With `boundaryRuns` (issue #1279) each pass first sweeps the boundary runs:
|
|
1242
|
+
* for each surviving, unprotected source-hull vertex in ascending source index,
|
|
1243
|
+
* the runs starting at it and following the outline the way the source hull is
|
|
1244
|
+
* listed, longest first — `maxVertices` down to 2, never leaving fewer than three
|
|
1245
|
+
* outline vertices, every member a surviving, unprotected source-hull vertex —
|
|
1246
|
+
* each one candidate; the first taken ends that start's sweep, and the sweep
|
|
1247
|
+
* goes on to the next index over the outline the run left. Then the pass's
|
|
1248
|
+
* single removals, exactly as without the field.
|
|
1138
1249
|
*/
|
|
1139
1250
|
function removeVertices(run: Run): PhaseEnd {
|
|
1140
1251
|
const { work, input } = run;
|
|
1141
|
-
|
|
1252
|
+
const runs = run.plant === 'runs-without-opt-in' ? { maxVertices: 2 } : input.boundaryRuns;
|
|
1253
|
+
const hull = input.source.hull;
|
|
1254
|
+
let lastBlock: { refusal: Refusal; what: string } | null = null;
|
|
1255
|
+
const lastName = (): string => (lastBlock === null ? '' : `${refusalText(lastBlock.refusal)}, ${lastBlock.what}`);
|
|
1142
1256
|
for (;;) {
|
|
1143
1257
|
let taken = 0;
|
|
1144
1258
|
let tried = 0;
|
|
1259
|
+
if (runs !== undefined) {
|
|
1260
|
+
let walk: number[] | null = null;
|
|
1261
|
+
for (let s = 0; s < hull; s++) {
|
|
1262
|
+
if (!work.alive[s] || run.protectedVertices.has(s)) continue;
|
|
1263
|
+
walk ??= outlineWalk(run);
|
|
1264
|
+
const at = walk.indexOf(s);
|
|
1265
|
+
if (at === -1) continue;
|
|
1266
|
+
for (let r = Math.min(runs.maxVertices, walk.length - 3); r >= 2; r--) {
|
|
1267
|
+
const members: number[] = [];
|
|
1268
|
+
for (let i = 0; i < r; i++) {
|
|
1269
|
+
const v = walk[(at + i) % walk.length];
|
|
1270
|
+
if (v >= hull || !work.alive[v] || run.protectedVertices.has(v)) break;
|
|
1271
|
+
members.push(v);
|
|
1272
|
+
}
|
|
1273
|
+
if (members.length < r) continue;
|
|
1274
|
+
if (run.steps >= input.budget.maxCandidates) return { kind: 'budget' };
|
|
1275
|
+
run.steps++;
|
|
1276
|
+
tried++;
|
|
1277
|
+
const block = tryOperation(run, members, false);
|
|
1278
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'boundary-run', sourceVertices: [...members], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function' });
|
|
1279
|
+
if (block === null) {
|
|
1280
|
+
taken++;
|
|
1281
|
+
walk = null;
|
|
1282
|
+
if (accept(run, 'boundary-run', members)) return { kind: 'replayed' };
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
lastBlock = { refusal: block, what: `removing source vertices ${members.join(', ')} as one boundary run` };
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1145
1289
|
for (let v = 0; v < work.nSource; v++) {
|
|
1146
1290
|
if (!work.alive[v] || run.protectedVertices.has(v)) continue;
|
|
1147
1291
|
if (run.steps >= input.budget.maxCandidates) return { kind: 'budget' };
|
|
1148
1292
|
run.steps++;
|
|
1149
1293
|
tried++;
|
|
1150
|
-
const block =
|
|
1294
|
+
const block = tryOperation(run, [v], false);
|
|
1295
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'removal', sourceVertices: [v], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function' });
|
|
1151
1296
|
if (block === null) {
|
|
1152
1297
|
taken++;
|
|
1153
|
-
if (accept(run)) return { kind: 'replayed' };
|
|
1154
|
-
} else lastBlock =
|
|
1298
|
+
if (accept(run, 'removal', [v])) return { kind: 'replayed' };
|
|
1299
|
+
} else lastBlock = { refusal: block, what: `removing source vertex ${v}` };
|
|
1155
1300
|
}
|
|
1156
1301
|
if (taken === 0) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1302
|
+
// A pass that took nothing left the working mesh as it found it, so a refusal the floor decided is measured
|
|
1303
|
+
// now in the state it was decided in.
|
|
1304
|
+
const constraint = tried === 0 ? 'protect: every surviving source vertex is protected (protect.hull, protect.vertices, protect.edges, protect.regionBoundaries or a weightJump edge)' : lastName();
|
|
1305
|
+
return { kind: 'stuck', constraint };
|
|
1159
1306
|
}
|
|
1160
1307
|
}
|
|
1161
1308
|
}
|
|
1162
1309
|
|
|
1163
|
-
/**
|
|
1164
|
-
|
|
1310
|
+
/**
|
|
1311
|
+
* The deviation floor (issue #1279): the furthest any removed source-hull vertex lies from the candidate's outline.
|
|
1312
|
+
* `MQ_BOUNDARY_DEVIATION` is the symmetric Hausdorff distance between that outline and the source hull, and its
|
|
1313
|
+
* backward half evaluates every source-hull vertex's distance to the outline exactly, so the row's value is at least
|
|
1314
|
+
* this — a step whose floor is over the bound fails that row whatever else it measures. The distance is
|
|
1315
|
+
* `distanceToSegment` over the candidate's hull edges, the function and polygon the row reads.
|
|
1316
|
+
*/
|
|
1317
|
+
function deviationFloor(run: Run, mesh: SourceMesh, vertices: readonly number[]): number {
|
|
1318
|
+
const poly = mesh.points.slice(0, mesh.hull);
|
|
1319
|
+
let floor = 0;
|
|
1320
|
+
for (const v of vertices) {
|
|
1321
|
+
if (v >= run.input.source.hull) continue;
|
|
1322
|
+
const p = run.work.pos[v];
|
|
1323
|
+
let d = Infinity;
|
|
1324
|
+
for (let i = 0; i < poly.length; i++) d = Math.min(d, distanceToSegment(p, poly[i], poly[(i + 1) % poly.length]));
|
|
1325
|
+
floor = Math.max(floor, d);
|
|
1326
|
+
}
|
|
1327
|
+
return floor;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* The margin the floor has to clear the bound by before it refuses without measuring: ten units of the `r6` grid
|
|
1332
|
+
* the row's value is put on, far above any difference the order the row walks an edge in can make to a distance.
|
|
1333
|
+
*/
|
|
1334
|
+
const FLOOR_MARGIN = 1e-5;
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* One operation — a single removal, or a boundary run of two or more — tried: null when it was taken (the working
|
|
1338
|
+
* mesh then holds it), else what refused it (the working mesh as it was). `full` measures every candidate that
|
|
1339
|
+
* reaches the rows; otherwise a candidate whose deviation floor is over the bound by `FLOOR_MARGIN` is refused
|
|
1340
|
+
* without the measurement, by a thunk that measures it when its name is read.
|
|
1341
|
+
*/
|
|
1342
|
+
function tryOperation(run: Run, vertices: readonly number[], full: boolean): Refusal | null {
|
|
1165
1343
|
const { work, input } = run;
|
|
1166
|
-
const step = removalOf(work,
|
|
1344
|
+
const step = vertices.length === 1 ? removalOf(work, vertices[0]) : runRemovalOf(work, vertices);
|
|
1167
1345
|
if ('blocked' in step) return step.blocked;
|
|
1168
1346
|
const jump = input.protect.weightJump;
|
|
1169
1347
|
if (jump !== null) {
|
|
@@ -1175,10 +1353,10 @@ function tryRemoval(run: Run, v: number): string | null {
|
|
|
1175
1353
|
}
|
|
1176
1354
|
const was = { triangles: work.triangles };
|
|
1177
1355
|
work.triangles = step.triangles;
|
|
1178
|
-
work.alive[v] = false;
|
|
1356
|
+
for (const v of vertices) work.alive[v] = false;
|
|
1179
1357
|
const undo = (): void => {
|
|
1180
1358
|
work.triangles = was.triangles;
|
|
1181
|
-
work.alive[v] = true;
|
|
1359
|
+
for (const v of vertices) work.alive[v] = true;
|
|
1182
1360
|
};
|
|
1183
1361
|
const edges = new Set<string>();
|
|
1184
1362
|
for (const t of work.triangles) for (let k = 0; k < 3; k++) edges.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
@@ -1196,6 +1374,23 @@ function tryRemoval(run: Run, v: number): string | null {
|
|
|
1196
1374
|
undo();
|
|
1197
1375
|
return `outline: ${err.message}`;
|
|
1198
1376
|
}
|
|
1377
|
+
if (vertices.length > 1 && run.plant === 'run-skips-rows') return null;
|
|
1378
|
+
if (!full && run.plant !== 'measure-every-candidate') {
|
|
1379
|
+
const margin = run.plant === 'floor-half-a-pixel-short' ? -0.5 : FLOOR_MARGIN;
|
|
1380
|
+
if (deviationFloor(run, canon.mesh, vertices) > input.targets.maxBoundaryDeviation + margin) {
|
|
1381
|
+
undo();
|
|
1382
|
+
const members = [...vertices];
|
|
1383
|
+
return () => {
|
|
1384
|
+
const measured = tryOperation(run, members, true);
|
|
1385
|
+
if (measured === null) {
|
|
1386
|
+
// Unreachable while the floor is a floor: the step was refused on a bound its own rows then pass. Loud,
|
|
1387
|
+
// because the run already went on as if it had been refused.
|
|
1388
|
+
throw new Error(`the deviation floor refused removing ${members.join(', ')}, which the rows accept`);
|
|
1389
|
+
}
|
|
1390
|
+
return refusalText(measured);
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1199
1394
|
const blocking = firstBlockingRow(measureAgainstTargets(run, canon.mesh, 'candidate'), run.input.targets.artFit);
|
|
1200
1395
|
if (blocking !== null) {
|
|
1201
1396
|
undo();
|
|
@@ -1255,19 +1450,25 @@ export function reduceMesh(input: MeshReductionInput): MeshReductionResult {
|
|
|
1255
1450
|
* module with `export *`, and a symbol that is merely exported is not promised
|
|
1256
1451
|
* (RELEASING.md, *The import surface*).
|
|
1257
1452
|
*/
|
|
1258
|
-
export function reduceMeshWith(
|
|
1453
|
+
export function reduceMeshWith(
|
|
1454
|
+
input: MeshReductionInput,
|
|
1455
|
+
rasters: ArtRasters,
|
|
1456
|
+
steps: StepRasters | null = stepRastersOf(rasters),
|
|
1457
|
+
plant: ReductionPlant | null = null,
|
|
1458
|
+
observe: AttemptObserver | null = null,
|
|
1459
|
+
): MeshReductionResult {
|
|
1259
1460
|
validateReduction(input);
|
|
1260
1461
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1261
1462
|
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))`);
|
|
1262
1463
|
}
|
|
1263
|
-
return reduceValidated(input, rasters, steps, plant);
|
|
1464
|
+
return reduceValidated(input, rasters, steps, plant, observe);
|
|
1264
1465
|
}
|
|
1265
1466
|
|
|
1266
1467
|
/**
|
|
1267
1468
|
* The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in
|
|
1268
1469
|
* `rasters`, and every refinement and removal step is measured through `steps` when it is given (issue #1246).
|
|
1269
1470
|
*/
|
|
1270
|
-
function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null, plant:
|
|
1471
|
+
function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null, plant: ReductionPlant | null = null, observe: AttemptObserver | null = null): MeshReductionResult {
|
|
1271
1472
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1272
1473
|
const src = input.source;
|
|
1273
1474
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
@@ -1344,6 +1545,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1344
1545
|
steps: 0,
|
|
1345
1546
|
acceptedAt: [],
|
|
1346
1547
|
plant,
|
|
1548
|
+
observe,
|
|
1347
1549
|
tally: { sharesDroppedOnGrid: 0, sharesPruned: 0 },
|
|
1348
1550
|
keyed: keyedSourceVertices(input),
|
|
1349
1551
|
...protectionOf(input, work, sourceEdges),
|
|
@@ -1453,7 +1655,7 @@ function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts
|
|
|
1453
1655
|
deformRemapped: deform.remapped.map((d) => ({ animation: d.animation, attachment: d.attachment, key: d.key, droppedVertices: d.droppedVertices })),
|
|
1454
1656
|
deformReevaluated: deform.reevaluated.map((d) => ({ animation: d.animation, attachment: d.attachment, key: d.key })),
|
|
1455
1657
|
linkedMeshes: input.linkedMeshes,
|
|
1456
|
-
acceptedAt: [...
|
|
1658
|
+
acceptedAt: run.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
|
|
1457
1659
|
};
|
|
1458
1660
|
const counts = candidate.counts!;
|
|
1459
1661
|
const mesh: ReducedMesh = {
|
|
@@ -1492,5 +1694,6 @@ function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sou
|
|
|
1492
1694
|
referenceHull: sourceHull,
|
|
1493
1695
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
1494
1696
|
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|
|
1697
|
+
...(input.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: input.boundaryRuns.maxVertices } }),
|
|
1495
1698
|
};
|
|
1496
1699
|
}
|