rig-c 2.26.0 → 2.28.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 +79 -4
- package/src/meshreduce.ts +346 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.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
|
@@ -252,6 +252,56 @@ export interface MeshReductionInput {
|
|
|
252
252
|
* 2 or more, and has no default; `null` is refused like any value that is not that object.
|
|
253
253
|
*/
|
|
254
254
|
boundaryRuns?: BoundaryRuns;
|
|
255
|
+
/**
|
|
256
|
+
* The triangulation post-pass (issue #1283, §8 Q3/Q9) — opt-in. Left out = the returned mesh carries the triangles
|
|
257
|
+
* the removals left, and the call is the one it was before the field existed. `'delaunay'`: once the reduction has
|
|
258
|
+
* ended (or a replay has stopped), the kept vertices are re-triangulated by Delaunay edge flips — no vertex added,
|
|
259
|
+
* moved or removed; no outline edge, protected edge or edge a region holds is flipped, and no flip makes an edge a
|
|
260
|
+
* region holds or one over `protect.weightJump` — and the pass is taken whole only when every required row still
|
|
261
|
+
* passes on its result, else the mesh is returned as the removals left it and the report names the row
|
|
262
|
+
* (`ReductionChanges.retriangulation`). It is not a step: it tries no candidate and is not counted in the budget.
|
|
263
|
+
* Any other value, `null` included, is refused.
|
|
264
|
+
*/
|
|
265
|
+
retriangulate?: 'delaunay';
|
|
266
|
+
/**
|
|
267
|
+
* The order single removals are attempted in (issue #1283, §8 Q11) — opt-in. Left out = ascending source index.
|
|
268
|
+
* `'deformation-load'`: at the start of each pass's single removals, every candidate is ranked by the deformation
|
|
269
|
+
* load its removal would add — the largest L · Δshare over the edges the re-triangulated hole adds, L the edge's
|
|
270
|
+
* length in px and Δshare half the L1 difference of its ends' weight vectors — and attempted in ascending load, ties
|
|
271
|
+
* by source index; a removal that cannot be made ranks last. Boundary runs keep their own order. Reads weights only:
|
|
272
|
+
* on an unweighted source every load is 0 and the order is the default's. Any other value, `null` included, is
|
|
273
|
+
* refused.
|
|
274
|
+
*/
|
|
275
|
+
removalOrder?: 'deformation-load';
|
|
276
|
+
/**
|
|
277
|
+
* The motion amplitude the two weight-aware allocation rows read (issue #1287) — `MeshMeasureInput.motionAmplitude`,
|
|
278
|
+
* the same shape and the same handling: left out, it is not declared; `null` declares it absent; anything else that
|
|
279
|
+
* is not a `MotionAmplitude` in full is refused `REDUCE_INPUT_MISSING` naming its path, before any work. Set, the
|
|
280
|
+
* result's measurement reads it, so `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD` are measured on the report rather
|
|
281
|
+
* than `not-measurable`; it is echoed in `effective` (`null` included). It declares no bound — both rows stay
|
|
282
|
+
* `undeclared` — and no step reads it: the admission, every step and the post-pass are measured without it, so the
|
|
283
|
+
* mesh, `acceptedAt`, the termination and every other row are the call's without it (docs/MESH_REDUCTION.md §8).
|
|
284
|
+
*/
|
|
285
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Issue #1283: which triangulation a reduced mesh carries, written in `ReductionChanges` only when the input set
|
|
290
|
+
* `retriangulate`.
|
|
291
|
+
*/
|
|
292
|
+
export interface Retriangulation {
|
|
293
|
+
/** The input's `retriangulate`. */
|
|
294
|
+
method: 'delaunay';
|
|
295
|
+
/** Whether the returned mesh carries the pass's triangles (true) or the removals' (false, `refusedBy` says why). */
|
|
296
|
+
taken: boolean;
|
|
297
|
+
/** Edge flips the pass made — on the returned mesh when taken, on the one it refused otherwise. */
|
|
298
|
+
flips: number;
|
|
299
|
+
/** Sweeps over the interior edges, the last of them flipping none. */
|
|
300
|
+
sweeps: number;
|
|
301
|
+
/** Null when taken; else the first required row that failed on the pass's result, or the bound the pass reached. */
|
|
302
|
+
refusedBy: string | null;
|
|
303
|
+
/** What the operation does not promise about the accepted steps under the pass — the same sentence on every report. */
|
|
304
|
+
monotonicity: string;
|
|
255
305
|
}
|
|
256
306
|
|
|
257
307
|
/** Issue #1279: the opt-in to boundary runs — the most consecutive outline vertices one chord may replace. */
|
|
@@ -495,6 +545,8 @@ export interface ReductionChanges {
|
|
|
495
545
|
* `stopAfterAccepted: k` replays to the operation at `acceptedAt[k - 1]`.
|
|
496
546
|
*/
|
|
497
547
|
acceptedAt: AcceptedOperation[];
|
|
548
|
+
/** Issue #1283: which triangulation the mesh carries — present only when the input set `retriangulate`, written last. */
|
|
549
|
+
retriangulation?: Retriangulation;
|
|
498
550
|
}
|
|
499
551
|
|
|
500
552
|
export interface MeshQualityReport {
|
|
@@ -542,8 +594,12 @@ export interface EffectiveSettings {
|
|
|
542
594
|
stopAfterAccepted?: number;
|
|
543
595
|
/** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
|
|
544
596
|
boundaryRuns?: BoundaryRuns;
|
|
545
|
-
/** A measurement's `motionAmplitude`, echoed when the input set it — `null` included (
|
|
597
|
+
/** A measurement's or a reduction's `motionAmplitude`, echoed when the input set it — `null` included (issues #1280, #1287); absent otherwise. */
|
|
546
598
|
motionAmplitude?: MotionAmplitude | null;
|
|
599
|
+
/** A reduction's `retriangulate`, echoed when the input set it (issue #1283); absent otherwise. */
|
|
600
|
+
retriangulate?: 'delaunay';
|
|
601
|
+
/** A reduction's `removalOrder`, echoed when the input set it (issue #1283); absent otherwise — the order was ascending source index. */
|
|
602
|
+
removalOrder?: 'deformation-load';
|
|
547
603
|
}
|
|
548
604
|
|
|
549
605
|
export interface MeshCounts {
|
|
@@ -796,11 +852,16 @@ function validateInput(input: MeshMeasureInput): void {
|
|
|
796
852
|
if (input.preset !== null && (!isObject(input.preset) || typeof input.preset.name !== 'string' || typeof input.preset.version !== 'string')) {
|
|
797
853
|
refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
|
|
798
854
|
}
|
|
799
|
-
|
|
855
|
+
validateMotionAmplitude(who, input.motionAmplitude);
|
|
800
856
|
}
|
|
801
857
|
|
|
802
|
-
/**
|
|
803
|
-
|
|
858
|
+
/**
|
|
859
|
+
* Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in
|
|
860
|
+
* full, else refused `REDUCE_INPUT_MISSING` naming the path. `reduceMesh` refuses its own field with it before any work
|
|
861
|
+
* (issue #1287), so a reduction and a measurement refuse one value in the same words. Internal, as
|
|
862
|
+
* `measureMeshQualityWith` is: on `rig-c/mesh` only through `export *`.
|
|
863
|
+
*/
|
|
864
|
+
export function validateMotionAmplitude(who: string, amplitude: unknown): void {
|
|
804
865
|
if (amplitude === undefined || amplitude === null) return;
|
|
805
866
|
const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
|
|
806
867
|
if (!isObject(amplitude) || !Array.isArray(amplitude.tracks)) refuse('REDUCE_INPUT_MISSING', `${who}: motionAmplitude is ${JSON.stringify(amplitude)}; required ${shape}, null, or the field left out`);
|
|
@@ -2259,6 +2320,8 @@ function effectiveJson(e: EffectiveSettings): Json {
|
|
|
2259
2320
|
...(e.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: e.stopAfterAccepted }),
|
|
2260
2321
|
...(e.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: e.boundaryRuns.maxVertices } }),
|
|
2261
2322
|
...(e.motionAmplitude === undefined ? {} : { motionAmplitude: amplitudeJson(e.motionAmplitude) }),
|
|
2323
|
+
...(e.retriangulate === undefined ? {} : { retriangulate: e.retriangulate }),
|
|
2324
|
+
...(e.removalOrder === undefined ? {} : { removalOrder: e.removalOrder }),
|
|
2262
2325
|
};
|
|
2263
2326
|
}
|
|
2264
2327
|
|
|
@@ -2360,6 +2423,18 @@ function candidateJson(c: CandidateReport): Json {
|
|
|
2360
2423
|
deformReevaluated: k.deformReevaluated.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key })),
|
|
2361
2424
|
linkedMeshes: k.linkedMeshes.map(attachmentJson),
|
|
2362
2425
|
acceptedAt: k.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
|
|
2426
|
+
...(k.retriangulation === undefined
|
|
2427
|
+
? {}
|
|
2428
|
+
: {
|
|
2429
|
+
retriangulation: {
|
|
2430
|
+
method: k.retriangulation.method,
|
|
2431
|
+
taken: k.retriangulation.taken,
|
|
2432
|
+
flips: k.retriangulation.flips,
|
|
2433
|
+
sweeps: k.retriangulation.sweeps,
|
|
2434
|
+
refusedBy: k.retriangulation.refusedBy,
|
|
2435
|
+
monotonicity: k.retriangulation.monotonicity,
|
|
2436
|
+
},
|
|
2437
|
+
}),
|
|
2363
2438
|
};
|
|
2364
2439
|
}
|
|
2365
2440
|
return out;
|
package/src/meshreduce.ts
CHANGED
|
@@ -55,6 +55,30 @@
|
|
|
55
55
|
* Every accepted operation is one `acceptedAt` entry and one unit of
|
|
56
56
|
* `stopAfterAccepted`.
|
|
57
57
|
*
|
|
58
|
+
* With `removalOrder: 'deformation-load'` (issue #1283, opt-in) each pass's
|
|
59
|
+
* single removals are attempted in ascending predicted deformation load, ties
|
|
60
|
+
* by source index, ranked once at the start of those singles (`loadOrder`);
|
|
61
|
+
* every attempt is still a candidate, held exactly as before.
|
|
62
|
+
*
|
|
63
|
+
* ## The triangulation post-pass (issue #1283, opt-in)
|
|
64
|
+
*
|
|
65
|
+
* With `retriangulate: 'delaunay'`, once the reduction ends — a replay's stop
|
|
66
|
+
* included — the kept vertices are re-triangulated by Delaunay edge flips that
|
|
67
|
+
* never touch the outline, a protected edge or an edge a region holds
|
|
68
|
+
* (`delaunayFlips`), and the result is taken whole only when every required
|
|
69
|
+
* row passes on it, else refused whole and named (`retriangulateResult`). It
|
|
70
|
+
* is not a step: no candidate, no `acceptedAt` entry, so a replay and the
|
|
71
|
+
* budget cut at the same step end on the same state and the same pass.
|
|
72
|
+
*
|
|
73
|
+
* ## The amplitude (issue #1287, opt-in)
|
|
74
|
+
*
|
|
75
|
+
* With `motionAmplitude`, the result's own measurement reads it, so the
|
|
76
|
+
* report's `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD` are measured rather
|
|
77
|
+
* than `not-measurable`. No other measurement of the call is handed it — no
|
|
78
|
+
* step is decided by either row, and no report carries the other
|
|
79
|
+
* measurements' rows (`amplitudeOf`) — so the mesh and every step are the
|
|
80
|
+
* call's without it.
|
|
81
|
+
*
|
|
58
82
|
* A removal whose deviation floor — the furthest removed source-hull vertex
|
|
59
83
|
* from the candidate's outline, which `MQ_BOUNDARY_DEVIATION` can only exceed —
|
|
60
84
|
* is over the bound is refused without the measurement; its name, when read,
|
|
@@ -104,6 +128,7 @@ import {
|
|
|
104
128
|
edgeIsHeldByRegion,
|
|
105
129
|
measureMeshQualityStep,
|
|
106
130
|
measureMeshQualityWith,
|
|
131
|
+
validateMotionAmplitude,
|
|
107
132
|
type AcceptedOperation,
|
|
108
133
|
type AttachmentRef,
|
|
109
134
|
type ArtFitBounds,
|
|
@@ -115,8 +140,10 @@ import {
|
|
|
115
140
|
type MeshMeasureInput,
|
|
116
141
|
type MeshQualityReport,
|
|
117
142
|
type MeshReductionInput,
|
|
143
|
+
type MotionAmplitude,
|
|
118
144
|
type RefinementRegion,
|
|
119
145
|
type ReductionChanges,
|
|
146
|
+
type Retriangulation,
|
|
120
147
|
type SourceMesh,
|
|
121
148
|
type StopNotReached,
|
|
122
149
|
type Termination,
|
|
@@ -258,7 +285,7 @@ function artBoundAbsent(fit: ArtFitBounds, row: MeasureRow): boolean {
|
|
|
258
285
|
* by `measureMeshQuality`'s own validation when the source is first measured,
|
|
259
286
|
* with the same codes.
|
|
260
287
|
*/
|
|
261
|
-
function validateReduction(input: MeshReductionInput): void {
|
|
288
|
+
function validateReduction(input: MeshReductionInput, plant: ReductionPlant | null = null): void {
|
|
262
289
|
if (!isObject(input)) refuse('REDUCE_INPUT_MISSING', `the input is ${JSON.stringify(input)}; required a MeshReductionInput object`);
|
|
263
290
|
if (!isRef(input.attachment)) refuse('REDUCE_INPUT_MISSING', `attachment is ${JSON.stringify(input.attachment)}; required { skin: string | null, slot: string, attachment: string }`);
|
|
264
291
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
@@ -283,6 +310,15 @@ function validateReduction(input: MeshReductionInput): void {
|
|
|
283
310
|
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
311
|
}
|
|
285
312
|
}
|
|
313
|
+
if ('retriangulate' in input && input.retriangulate !== undefined && input.retriangulate !== 'delaunay') {
|
|
314
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: retriangulate is ${JSON.stringify(input.retriangulate)}; required "delaunay" — the kept vertices re-triangulated by Delaunay edge flips once the reduction ends — or the field left out (issue #1283: the triangles the removals leave)`);
|
|
315
|
+
}
|
|
316
|
+
if ('removalOrder' in input && input.removalOrder !== undefined && input.removalOrder !== 'deformation-load') {
|
|
317
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: removalOrder is ${JSON.stringify(input.removalOrder)}; required "deformation-load" — single removals in ascending predicted deformation load, ties by source index — or the field left out (issue #1283: ascending source index)`);
|
|
318
|
+
}
|
|
319
|
+
// Issue #1287: refused here, before the admission measurement and any step, in the measurement's own words — the
|
|
320
|
+
// admission does not carry the field, and a call whose source is refused never reaches the one measurement that does.
|
|
321
|
+
if (plant !== 'amplitude-unvalidated') validateMotionAmplitude(who, input.motionAmplitude);
|
|
286
322
|
const src = input.source;
|
|
287
323
|
if (!isObject(src) || !Array.isArray(src.points)) refuse('REDUCE_INPUT_MISSING', `${who}: source is not { points, uvs, triangles, hull, weights }`);
|
|
288
324
|
const n = src.points.length;
|
|
@@ -769,8 +805,11 @@ interface Run {
|
|
|
769
805
|
stepRasters: StepRasters | null;
|
|
770
806
|
}
|
|
771
807
|
|
|
772
|
-
/**
|
|
773
|
-
|
|
808
|
+
/**
|
|
809
|
+
* A measurement of a canonical mesh against the result's full contract. `final` marks the result's own measurement —
|
|
810
|
+
* the one whose rows the report carries, and the only one handed the input's `motionAmplitude` (`amplitudeOf`).
|
|
811
|
+
*/
|
|
812
|
+
function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string, final = false): MeshQualityReport {
|
|
774
813
|
const { input } = run;
|
|
775
814
|
const targets = input.targets;
|
|
776
815
|
const measureInput: MeshMeasureInput = {
|
|
@@ -786,10 +825,33 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string): MeshQual
|
|
|
786
825
|
influences: input.influences,
|
|
787
826
|
boneOrder: input.boneOrder,
|
|
788
827
|
preset: input.preset,
|
|
828
|
+
...amplitudeOf(run.input, run.plant, final),
|
|
789
829
|
};
|
|
790
830
|
return run.stepRasters === null ? measureMeshQualityWith(measureInput, run.rasters) : measureMeshQualityStep(measureInput, run.stepRasters);
|
|
791
831
|
}
|
|
792
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Issue #1287: what one measurement of a reduction is handed as `motionAmplitude` — the input's, on the result's own
|
|
835
|
+
* measurement (`final`) only. The admission, every refinement and removal step and the post-pass are measured without
|
|
836
|
+
* it: no row it adds is read by a step (both are `undeclared`, so `firstBlockingRow` never names one) and no report
|
|
837
|
+
* carries those measurements' rows, so carrying it there would buy nothing and cost Δ's own silhouette search at every
|
|
838
|
+
* step (docs/MESH_REDUCTION.md §8, *Stage B — the amplitude on a reduction*, measured). A field left out is passed as
|
|
839
|
+
* left out and `null` as `null`, so the rows' reasons say which.
|
|
840
|
+
*/
|
|
841
|
+
function amplitudeOf(input: MeshReductionInput, plant: ReductionPlant | null, final: boolean): { motionAmplitude?: MotionAmplitude | null } {
|
|
842
|
+
const amplitude = input.motionAmplitude;
|
|
843
|
+
if (!final && plant !== 'amplitude-every-measurement' && plant !== 'amplitude-gates-steps') return {};
|
|
844
|
+
if (plant === 'amplitude-not-carried') return {};
|
|
845
|
+
if (plant === 'amplitude-invented' && (amplitude === undefined || amplitude === null)) {
|
|
846
|
+
const bones = [...new Set((input.source.weights ?? []).flatMap((v) => v.map((b) => b.bone)))].sort();
|
|
847
|
+
const pairs: Array<{ bones: [string, string]; theta: number }> = [];
|
|
848
|
+
for (let i = 0; i < bones.length; i++) for (let j = i + 1; j < bones.length; j++) pairs.push({ bones: [bones[i], bones[j]], theta: 1 });
|
|
849
|
+
return { motionAmplitude: { tracks: [{ track: 'invented', pairs, epsilon: 1 }], gradation: 0 } };
|
|
850
|
+
}
|
|
851
|
+
if (plant === 'null-read-as-left-out' && amplitude === null) return {};
|
|
852
|
+
return amplitude === undefined ? {} : { motionAmplitude: amplitude };
|
|
853
|
+
}
|
|
854
|
+
|
|
793
855
|
/** The order constraints are named in when several fail on one step: structure, then shape, then art, then density. */
|
|
794
856
|
const BLOCKING_ORDER = ['MQ_ORIENTATION', 'MQ_DEGENERATE', 'MQ_BOUNDARY_DEVIATION', 'MQ_COVERAGE', 'MQ_OVERSHOOT', 'MQ_UNDERCUT', 'MQ_MIN_ANGLE', 'MQ_MAX_EDGE', 'MQ_TRANSITION'];
|
|
795
857
|
|
|
@@ -1021,7 +1083,45 @@ export type ReplayPlant = 'stop-one-early' | 'stop-one-late';
|
|
|
1021
1083
|
* measured, with no deviation floor (the path the floor is held equal to); and the floor read half a pixel short
|
|
1022
1084
|
* of the bound, so it refuses steps the rows would take.
|
|
1023
1085
|
*/
|
|
1024
|
-
export type ReductionPlant =
|
|
1086
|
+
export type ReductionPlant =
|
|
1087
|
+
| ReplayPlant
|
|
1088
|
+
| 'runs-without-opt-in'
|
|
1089
|
+
| 'run-split-per-vertex'
|
|
1090
|
+
| 'run-skips-rows'
|
|
1091
|
+
| 'measure-every-candidate'
|
|
1092
|
+
| 'floor-half-a-pixel-short'
|
|
1093
|
+
| RetriangulationPlant
|
|
1094
|
+
| OrderPlant
|
|
1095
|
+
| AmplitudePlant;
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Issue #1283's faults in the triangulation post-pass: the pass run on a call that did not opt in; the pass skipped
|
|
1099
|
+
* when a replay stops the run, so a replay is not the budget cut at the same step; the flip
|
|
1100
|
+
* criterion inverted, so the pass flips edges that are locally Delaunay — the flips a declared row can refuse — with
|
|
1101
|
+
* its result measured as the pass's is, or taken without the measurement; and the pass flipping edges a region holds.
|
|
1102
|
+
*/
|
|
1103
|
+
export type RetriangulationPlant = 'flips-without-opt-in' | 'flips-not-on-replay' | 'flips-against-delaunay' | 'flips-against-delaunay-unmeasured' | 'flips-ignore-regions';
|
|
1104
|
+
|
|
1105
|
+
/** Issue #1283's faults in the removal order: the load order used by a call that did not opt in, and the loads ranked descending. */
|
|
1106
|
+
export type OrderPlant = 'order-without-opt-in' | 'order-descending';
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Issue #1287's faults in carrying `motionAmplitude`: the amplitude not handed to the result's measurement; an
|
|
1110
|
+
* amplitude invented (every pair of the source's bones at θ 1, ε 1, gradation 0) when the field is left out or
|
|
1111
|
+
* `null`; `null` passed to the measurement as the field left out; the field not validated by the reduction; a
|
|
1112
|
+
* measured allocation row read as blocking a step; and `effective` echoing `null` for a field left out. And one path
|
|
1113
|
+
* that is not a fault, held equal to the call: the amplitude handed to every measurement of the call — the admission,
|
|
1114
|
+
* each step, the post-pass — which is what the decision to carry it on the result's measurement only is measured
|
|
1115
|
+
* against.
|
|
1116
|
+
*/
|
|
1117
|
+
export type AmplitudePlant =
|
|
1118
|
+
| 'amplitude-not-carried'
|
|
1119
|
+
| 'amplitude-invented'
|
|
1120
|
+
| 'null-read-as-left-out'
|
|
1121
|
+
| 'amplitude-unvalidated'
|
|
1122
|
+
| 'amplitude-gates-steps'
|
|
1123
|
+
| 'echo-when-unset'
|
|
1124
|
+
| 'amplitude-every-measurement';
|
|
1025
1125
|
|
|
1026
1126
|
/** One removal-phase attempt as a control reads it (issue #1279): what was tried, and what refused it or null when taken. */
|
|
1027
1127
|
export interface AttemptRecord {
|
|
@@ -1032,6 +1132,10 @@ export interface AttemptRecord {
|
|
|
1032
1132
|
refusedBy: (() => string) | null;
|
|
1033
1133
|
/** Whether the deviation floor refused it without a measurement. */
|
|
1034
1134
|
decidedByFloor: boolean;
|
|
1135
|
+
/** The removal pass it was tried in, 1-based (issue #1283). */
|
|
1136
|
+
pass: number;
|
|
1137
|
+
/** A single removal under `removalOrder: 'deformation-load'`: the load it was ranked by (Infinity when the removal cannot be made); else null (issue #1283). */
|
|
1138
|
+
predictedLoad: number | null;
|
|
1035
1139
|
}
|
|
1036
1140
|
|
|
1037
1141
|
/** Called once per removal-phase attempt, after it. */
|
|
@@ -1179,6 +1283,46 @@ function weightJump(work: Work, a: number, b: number): number {
|
|
|
1179
1283
|
return sum;
|
|
1180
1284
|
}
|
|
1181
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Issue #1283, §8's deformation load of one edge: its length in px times half the L1 difference of its ends' weight
|
|
1288
|
+
* vectors (Δshare, the share of the field that changes along it). 0 on an unweighted mesh.
|
|
1289
|
+
*/
|
|
1290
|
+
function edgeLoad(work: Work, a: number, b: number): number {
|
|
1291
|
+
const dx = work.pos[a][0] - work.pos[b][0];
|
|
1292
|
+
const dy = work.pos[a][1] - work.pos[b][1];
|
|
1293
|
+
return Math.sqrt(dx * dx + dy * dy) * (weightJump(work, a, b) / 2);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Issue #1283 (`removalOrder: 'deformation-load'`): the surviving, unprotected source vertices in the order a pass
|
|
1298
|
+
* attempts their single removals — ascending by the predicted load of each removal, the largest `edgeLoad` over the
|
|
1299
|
+
* edges its re-triangulated hole adds (0 when it adds none), a removal `removalOf` cannot make ranked Infinity; ties
|
|
1300
|
+
* by source index. Nothing is measured and nothing is counted as a candidate: one `removalOf` per vertex and one
|
|
1301
|
+
* sort. The plant `order-descending` ranks the loads the other way.
|
|
1302
|
+
*/
|
|
1303
|
+
function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
|
|
1304
|
+
const { work } = run;
|
|
1305
|
+
const load = new Map<number, number>();
|
|
1306
|
+
const order: number[] = [];
|
|
1307
|
+
for (let v = 0; v < work.nSource; v++) {
|
|
1308
|
+
if (!work.alive[v] || run.protectedVertices.has(v)) continue;
|
|
1309
|
+
const step = removalOf(work, v);
|
|
1310
|
+
let most = 0;
|
|
1311
|
+
if ('blocked' in step) most = Infinity;
|
|
1312
|
+
else for (const [a, b] of step.added) most = Math.max(most, edgeLoad(work, a, b));
|
|
1313
|
+
load.set(v, most);
|
|
1314
|
+
order.push(v);
|
|
1315
|
+
}
|
|
1316
|
+
const sign = run.plant === 'order-descending' ? -1 : 1;
|
|
1317
|
+
order.sort((p, q) => {
|
|
1318
|
+
const lp = load.get(p)!;
|
|
1319
|
+
const lq = load.get(q)!;
|
|
1320
|
+
if (lp !== lq) return lp < lq ? -sign : sign;
|
|
1321
|
+
return p - q;
|
|
1322
|
+
});
|
|
1323
|
+
return { order, load };
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1182
1326
|
/**
|
|
1183
1327
|
* Several boundary vertices removed one after another as one operation (issue #1279): the working triangles after
|
|
1184
1328
|
* the last and the edges they hold that the triangles before the first did not, in the order they appear — or the
|
|
@@ -1250,12 +1394,15 @@ function outlineWalk(run: Run): number[] {
|
|
|
1250
1394
|
function removeVertices(run: Run): PhaseEnd {
|
|
1251
1395
|
const { work, input } = run;
|
|
1252
1396
|
const runs = run.plant === 'runs-without-opt-in' ? { maxVertices: 2 } : input.boundaryRuns;
|
|
1397
|
+
const byLoad = input.removalOrder === 'deformation-load' || run.plant === 'order-without-opt-in';
|
|
1253
1398
|
const hull = input.source.hull;
|
|
1254
1399
|
let lastBlock: { refusal: Refusal; what: string } | null = null;
|
|
1255
1400
|
const lastName = (): string => (lastBlock === null ? '' : `${refusalText(lastBlock.refusal)}, ${lastBlock.what}`);
|
|
1401
|
+
let pass = 0;
|
|
1256
1402
|
for (;;) {
|
|
1257
1403
|
let taken = 0;
|
|
1258
1404
|
let tried = 0;
|
|
1405
|
+
pass++;
|
|
1259
1406
|
if (runs !== undefined) {
|
|
1260
1407
|
let walk: number[] | null = null;
|
|
1261
1408
|
for (let s = 0; s < hull; s++) {
|
|
@@ -1275,7 +1422,7 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1275
1422
|
run.steps++;
|
|
1276
1423
|
tried++;
|
|
1277
1424
|
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' });
|
|
1425
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'boundary-run', sourceVertices: [...members], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function', pass, predictedLoad: null });
|
|
1279
1426
|
if (block === null) {
|
|
1280
1427
|
taken++;
|
|
1281
1428
|
walk = null;
|
|
@@ -1286,13 +1433,18 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1286
1433
|
}
|
|
1287
1434
|
}
|
|
1288
1435
|
}
|
|
1289
|
-
|
|
1436
|
+
// Issue #1283: with the load order the pass's singles are ranked once, here, over the mesh the runs left; without
|
|
1437
|
+
// it they are every source index ascending, exactly as before the field.
|
|
1438
|
+
const ranked = byLoad ? loadOrder(run) : null;
|
|
1439
|
+
const singles = ranked === null ? null : ranked.order;
|
|
1440
|
+
for (let i = 0, end = singles === null ? work.nSource : singles.length; i < end; i++) {
|
|
1441
|
+
const v = singles === null ? i : singles[i];
|
|
1290
1442
|
if (!work.alive[v] || run.protectedVertices.has(v)) continue;
|
|
1291
1443
|
if (run.steps >= input.budget.maxCandidates) return { kind: 'budget' };
|
|
1292
1444
|
run.steps++;
|
|
1293
1445
|
tried++;
|
|
1294
1446
|
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' });
|
|
1447
|
+
if (run.observe !== null) run.observe({ step: run.steps, kind: 'removal', sourceVertices: [v], refusedBy: block === null ? null : () => refusalText(block), decidedByFloor: typeof block === 'function', pass, predictedLoad: ranked === null ? null : ranked.load.get(v)! });
|
|
1296
1448
|
if (block === null) {
|
|
1297
1449
|
taken++;
|
|
1298
1450
|
if (accept(run, 'removal', [v])) return { kind: 'replayed' };
|
|
@@ -1391,7 +1543,10 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1391
1543
|
};
|
|
1392
1544
|
}
|
|
1393
1545
|
}
|
|
1394
|
-
const
|
|
1546
|
+
const measured = measureAgainstTargets(run, canon.mesh, 'candidate');
|
|
1547
|
+
const blocking =
|
|
1548
|
+
firstBlockingRow(measured, run.input.targets.artFit) ??
|
|
1549
|
+
(run.plant === 'amplitude-gates-steps' ? ((measured.candidates[0]?.geometry?.rows ?? []).find((r) => (r.code === 'MQ_DEFORM_LOAD' || r.code === 'MQ_ALLOCATION_CONTRAST') && r.state === 'undeclared') ?? null) : null);
|
|
1395
1550
|
if (blocking !== null) {
|
|
1396
1551
|
undo();
|
|
1397
1552
|
return rowName(blocking);
|
|
@@ -1399,6 +1554,178 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1399
1554
|
return null;
|
|
1400
1555
|
}
|
|
1401
1556
|
|
|
1557
|
+
// ---------------------------------------------------------------------------
|
|
1558
|
+
// the triangulation post-pass (issue #1283)
|
|
1559
|
+
// ---------------------------------------------------------------------------
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* How far past π the two angles opposite an edge have to sum before the edge is flipped, read as the sine of their
|
|
1563
|
+
* sum: the flip is made when that sine is below `-FLIP_MARGIN`. The flip turns the sum into 2π minus it, so its sine
|
|
1564
|
+
* changes sign and the new edge is never flipped back in the same or a later sweep; cocircular quads (sine within the
|
|
1565
|
+
* margin) are left as they are.
|
|
1566
|
+
*/
|
|
1567
|
+
const FLIP_MARGIN = 1e-9;
|
|
1568
|
+
|
|
1569
|
+
/** Twice the signed area of p, q, r in drawing px (y down), the orientation test the flip reads. */
|
|
1570
|
+
function orient(p: Pt, q: Pt, r: Pt): number {
|
|
1571
|
+
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* sin(α + β) for the angles α at `c` and β at `d` opposite the edge a–b — negative exactly when α + β > π, the edge
|
|
1576
|
+
* is then not locally Delaunay. From cross and dot products and square roots only, so it reads the same wherever IEEE
|
|
1577
|
+
* arithmetic does (no libm call).
|
|
1578
|
+
*/
|
|
1579
|
+
function oppositeAngleSine(a: Pt, b: Pt, c: Pt, d: Pt): number {
|
|
1580
|
+
const angle = (o: Pt): { sin: number; cos: number } => {
|
|
1581
|
+
const ux = a[0] - o[0];
|
|
1582
|
+
const uy = a[1] - o[1];
|
|
1583
|
+
const vx = b[0] - o[0];
|
|
1584
|
+
const vy = b[1] - o[1];
|
|
1585
|
+
const lengths = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
|
1586
|
+
return { sin: Math.abs(ux * vy - uy * vx) / lengths, cos: (ux * vx + uy * vy) / lengths };
|
|
1587
|
+
};
|
|
1588
|
+
const p = angle(c);
|
|
1589
|
+
const q = angle(d);
|
|
1590
|
+
return p.sin * q.cos + p.cos * q.sin;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** The sentence every report of a call with `retriangulate` carries (§8 Q4, [agreed, rig-parts#126]). */
|
|
1594
|
+
const MONOTONICITY_NOT_PROMISED =
|
|
1595
|
+
'not promised: the pass re-triangulates whatever mesh the removals leave, so along acceptedAt a step whose result passes a comparison may be followed by one that fails and then by one that passes again; a bisection over acceptedAt finds a passing step, not necessarily the last (§8 Q4, agreed on rig-parts#126)';
|
|
1596
|
+
|
|
1597
|
+
type FlipResult = { triangles: Array<[number, number, number]>; flips: number; sweeps: number } | { bound: string; flips: number; sweeps: number };
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Lawson's edge flips over the working mesh's interior edges, toward the Delaunay triangulation of the same vertices
|
|
1601
|
+
* constrained by what the mesh must keep. A sweep visits every interior edge (two triangles) in ascending (smaller id,
|
|
1602
|
+
* larger id), skipping an edge whose triangles a flip of this sweep already replaced, and flips it when:
|
|
1603
|
+
*
|
|
1604
|
+
* - the quad its two triangles form is strictly convex, so both new triangles keep the winding;
|
|
1605
|
+
* - the angles opposite it sum past π by `FLIP_MARGIN` (`oppositeAngleSine`) — the Delaunay criterion;
|
|
1606
|
+
* - it is not a protected edge (`protect.edges`, and every source edge over `protect.weightJump`);
|
|
1607
|
+
* - no declared region holds it, and none would hold the new edge (`edgeIsHeldByRegion`), so every region row reads
|
|
1608
|
+
* the same edges and lengths after the pass as before it;
|
|
1609
|
+
* - the new edge is a source edge, or its ends' weight vectors are within `protect.weightJump` (condition (b), as a
|
|
1610
|
+
* removal's new edges are held).
|
|
1611
|
+
*
|
|
1612
|
+
* The outline is never touched (an outline edge has one triangle) and no vertex is added, moved or removed. Sweeps
|
|
1613
|
+
* continue until one flips nothing. **Bounded work:** every flip made is one the criterion allows, which lowers the
|
|
1614
|
+
* triangulation's lifting onto the paraboloid strictly, so an edge flipped away never comes back and a pass makes at
|
|
1615
|
+
* most n(n − 1)/2 flips over n vertices; a sweep that continues has flipped at least one, so there are at most one more
|
|
1616
|
+
* sweeps than flips, and each visits at most 3n interior edges. The bound is also held by count: reaching it ends the
|
|
1617
|
+
* pass with the bound named rather than the triangles, which the criterion makes unreachable.
|
|
1618
|
+
*/
|
|
1619
|
+
function delaunayFlips(run: Run, plant: ReductionPlant | null): FlipResult {
|
|
1620
|
+
const { work, input } = run;
|
|
1621
|
+
const P = work.pos;
|
|
1622
|
+
const tris = work.triangles.map((t): [number, number, number] => [t[0], t[1], t[2]]);
|
|
1623
|
+
const alive = work.alive.reduce((n, a) => n + (a ? 1 : 0), 0);
|
|
1624
|
+
const maxFlips = (alive * (alive - 1)) / 2;
|
|
1625
|
+
const guarded = new Set(run.protectedEdges.map(([a, b]) => edgeKey(a, b)));
|
|
1626
|
+
const jump = input.protect.weightJump;
|
|
1627
|
+
const regions = plant === 'flips-ignore-regions' ? [] : input.targets.regions;
|
|
1628
|
+
const heldCache = new Map<string, boolean>();
|
|
1629
|
+
const held = (a: number, b: number): boolean => {
|
|
1630
|
+
if (regions.length === 0) return false;
|
|
1631
|
+
const key = edgeKey(a, b);
|
|
1632
|
+
let hit = heldCache.get(key);
|
|
1633
|
+
if (hit === undefined) {
|
|
1634
|
+
hit = regions.some((r) => edgeIsHeldByRegion(P[a], P[b], r));
|
|
1635
|
+
heldCache.set(key, hit);
|
|
1636
|
+
}
|
|
1637
|
+
return hit;
|
|
1638
|
+
};
|
|
1639
|
+
const against = plant === 'flips-against-delaunay' || plant === 'flips-against-delaunay-unmeasured';
|
|
1640
|
+
let flips = 0;
|
|
1641
|
+
let sweeps = 0;
|
|
1642
|
+
for (;;) {
|
|
1643
|
+
sweeps++;
|
|
1644
|
+
const sides = new Map<string, number[]>();
|
|
1645
|
+
tris.forEach((t, i) => {
|
|
1646
|
+
for (let k = 0; k < 3; k++) {
|
|
1647
|
+
const key = edgeKey(t[k], t[(k + 1) % 3]);
|
|
1648
|
+
const list = sides.get(key);
|
|
1649
|
+
if (list === undefined) sides.set(key, [i]);
|
|
1650
|
+
else list.push(i);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
const interior: Array<[number, number]> = [];
|
|
1654
|
+
for (const [key, list] of sides) {
|
|
1655
|
+
if (list.length !== 2) continue;
|
|
1656
|
+
const [a, b] = key.split(',').map(Number);
|
|
1657
|
+
interior.push([a, b]);
|
|
1658
|
+
}
|
|
1659
|
+
interior.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
1660
|
+
const replaced = new Set<number>();
|
|
1661
|
+
let flipped = 0;
|
|
1662
|
+
for (const [u, w] of interior) {
|
|
1663
|
+
const [i, j] = sides.get(edgeKey(u, w))!;
|
|
1664
|
+
if (replaced.has(i) || replaced.has(j)) continue;
|
|
1665
|
+
if (guarded.has(edgeKey(u, w))) continue;
|
|
1666
|
+
// Rotate so the edge is t1's a → b in its own winding; t2 then holds b → a.
|
|
1667
|
+
const t1 = tris[i];
|
|
1668
|
+
const k1 = t1.findIndex((x, k) => (x === u && t1[(k + 1) % 3] === w) || (x === w && t1[(k + 1) % 3] === u));
|
|
1669
|
+
const a = t1[k1];
|
|
1670
|
+
const b = t1[(k1 + 1) % 3];
|
|
1671
|
+
const c = t1[(k1 + 2) % 3];
|
|
1672
|
+
const t2 = tris[j];
|
|
1673
|
+
const k2 = t2.findIndex((x, k) => x === b && t2[(k + 1) % 3] === a);
|
|
1674
|
+
if (k2 === -1) continue;
|
|
1675
|
+
const d = t2[(k2 + 2) % 3];
|
|
1676
|
+
if (c === d) continue;
|
|
1677
|
+
const turn = Math.sign(orient(P[a], P[b], P[c]));
|
|
1678
|
+
const o1 = orient(P[a], P[d], P[c]);
|
|
1679
|
+
const o2 = orient(P[d], P[b], P[c]);
|
|
1680
|
+
if (Math.sign(o1) !== turn || Math.sign(o2) !== turn || Math.abs(o1) < 1e-9 || Math.abs(o2) < 1e-9) continue;
|
|
1681
|
+
const sine = oppositeAngleSine(P[a], P[b], P[c], P[d]);
|
|
1682
|
+
if (against ? !(sine > FLIP_MARGIN) : !(sine < -FLIP_MARGIN)) continue;
|
|
1683
|
+
if (held(a, b) || held(c, d)) continue;
|
|
1684
|
+
if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(edgeKey(c, d))) && weightJump(work, c, d) > jump) continue;
|
|
1685
|
+
if (flips >= maxFlips) return { bound: `the flip bound n(n - 1)/2 = ${maxFlips} over ${alive} vertices was reached`, flips, sweeps };
|
|
1686
|
+
tris[i] = [a, d, c];
|
|
1687
|
+
tris[j] = [d, b, c];
|
|
1688
|
+
replaced.add(i);
|
|
1689
|
+
replaced.add(j);
|
|
1690
|
+
flips++;
|
|
1691
|
+
flipped++;
|
|
1692
|
+
}
|
|
1693
|
+
// The inverted criterion is a plant: one sweep, since flipping a Delaunay edge makes a non-Delaunay one it would
|
|
1694
|
+
// flip straight back.
|
|
1695
|
+
if (flipped === 0 || against) return { triangles: tris, flips, sweeps };
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Issue #1283: the post-pass on the working mesh the reduction ended on, taken whole or refused whole — the working
|
|
1701
|
+
* triangles are the pass's only when every required row passes on its result (the measurement reads the canonical
|
|
1702
|
+
* mesh exactly as the result's own does). Undefined when the call did not opt in. One flip pass and at most one
|
|
1703
|
+
* measurement, never counted as a candidate: a replay and the budget cut at the same step end on the same state and
|
|
1704
|
+
* both are followed by it.
|
|
1705
|
+
*
|
|
1706
|
+
* Why whole rather than flip by flip: the rows a flip can move are the region rows (held edges, which the pass never
|
|
1707
|
+
* flips nor makes) and the minimum angle, which a Delaunay flip only raises; the art rows and the boundary deviation
|
|
1708
|
+
* read the outline and the union of the triangles, which no flip changes. A measurement per flip would buy nothing a
|
|
1709
|
+
* row can see and cost one measurement per flip — up to n(n − 1)/2 — where the whole pass costs one.
|
|
1710
|
+
*/
|
|
1711
|
+
function retriangulateResult(run: Run, termination: Termination): Retriangulation | undefined {
|
|
1712
|
+
const plant = run.plant;
|
|
1713
|
+
if (run.input.retriangulate === undefined && plant !== 'flips-without-opt-in') return undefined;
|
|
1714
|
+
if (plant === 'flips-not-on-replay' && termination.reason === 'replayed-to-accepted-step') return undefined;
|
|
1715
|
+
const result = delaunayFlips(run, plant);
|
|
1716
|
+
const out = (taken: boolean, refusedBy: string | null): Retriangulation => ({ method: 'delaunay', taken, flips: result.flips, sweeps: result.sweeps, refusedBy, monotonicity: MONOTONICITY_NOT_PROMISED });
|
|
1717
|
+
if (!('triangles' in result)) return out(false, result.bound);
|
|
1718
|
+
if (result.flips === 0) return out(true, null);
|
|
1719
|
+
const was = run.work.triangles;
|
|
1720
|
+
run.work.triangles = result.triangles;
|
|
1721
|
+
if (plant === 'flips-against-delaunay-unmeasured') return out(true, null);
|
|
1722
|
+
const canon = canonicalise(run.work, run.sourceTurn, run.boneRank);
|
|
1723
|
+
const blocking = firstBlockingRow(measureAgainstTargets(run, canon.mesh, 'retriangulated'), run.input.targets.artFit);
|
|
1724
|
+
if (blocking === null) return out(true, null);
|
|
1725
|
+
run.work.triangles = was;
|
|
1726
|
+
return out(false, rowName(blocking));
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1402
1729
|
// ---------------------------------------------------------------------------
|
|
1403
1730
|
// the operation
|
|
1404
1731
|
// ---------------------------------------------------------------------------
|
|
@@ -1457,7 +1784,7 @@ export function reduceMeshWith(
|
|
|
1457
1784
|
plant: ReductionPlant | null = null,
|
|
1458
1785
|
observe: AttemptObserver | null = null,
|
|
1459
1786
|
): MeshReductionResult {
|
|
1460
|
-
validateReduction(input);
|
|
1787
|
+
validateReduction(input, plant);
|
|
1461
1788
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1462
1789
|
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))`);
|
|
1463
1790
|
}
|
|
@@ -1489,9 +1816,10 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1489
1816
|
influences: input.influences,
|
|
1490
1817
|
boneOrder: input.boneOrder,
|
|
1491
1818
|
preset: input.preset,
|
|
1819
|
+
...amplitudeOf(input, plant, false),
|
|
1492
1820
|
};
|
|
1493
1821
|
const admit = steps === null ? measureMeshQualityWith(admitInput, rasters) : measureMeshQualityStep(admitInput, steps);
|
|
1494
|
-
const effective = effectiveOf(input, admit.effective, sourceHull);
|
|
1822
|
+
const effective = effectiveOf(input, admit.effective, sourceHull, plant);
|
|
1495
1823
|
const sourceCounts = admit.sourceCounts;
|
|
1496
1824
|
const noMesh = (termination: Termination): MeshReductionResult => ({
|
|
1497
1825
|
mesh: null,
|
|
@@ -1632,6 +1960,7 @@ function protectionOf(input: MeshReductionInput, work: Work, sourceEdges: Set<st
|
|
|
1632
1960
|
/** The result, canonical, with its deform keys carried and its own measurement as the report's one candidate. */
|
|
1633
1961
|
function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts | null, termination: Termination): MeshReductionResult {
|
|
1634
1962
|
const { input, work } = run;
|
|
1963
|
+
const retriangulation = retriangulateResult(run, termination);
|
|
1635
1964
|
const canon = canonicalise(work, run.sourceTurn, run.boneRank);
|
|
1636
1965
|
const indexMap: Array<number | null> = [];
|
|
1637
1966
|
for (let v = 0; v < work.nSource; v++) indexMap.push(work.alive[v] ? canon.indexOf.get(v)! : null);
|
|
@@ -1645,7 +1974,7 @@ function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts
|
|
|
1645
1974
|
}
|
|
1646
1975
|
throw err;
|
|
1647
1976
|
}
|
|
1648
|
-
const measured = measureAgainstTargets(run, canon.mesh, 'result');
|
|
1977
|
+
const measured = measureAgainstTargets(run, canon.mesh, 'result', true);
|
|
1649
1978
|
const candidate = measured.candidates[0];
|
|
1650
1979
|
const changes: ReductionChanges = {
|
|
1651
1980
|
removedVertices: indexMap.filter((r) => r === null).length,
|
|
@@ -1656,6 +1985,7 @@ function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts
|
|
|
1656
1985
|
deformReevaluated: deform.reevaluated.map((d) => ({ animation: d.animation, attachment: d.attachment, key: d.key })),
|
|
1657
1986
|
linkedMeshes: input.linkedMeshes,
|
|
1658
1987
|
acceptedAt: run.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
|
|
1988
|
+
...(retriangulation === undefined ? {} : { retriangulation }),
|
|
1659
1989
|
};
|
|
1660
1990
|
const counts = candidate.counts!;
|
|
1661
1991
|
const mesh: ReducedMesh = {
|
|
@@ -1685,7 +2015,8 @@ function reportOf(effective: EffectiveSettings, sourceCounts: MeshCounts | null,
|
|
|
1685
2015
|
}
|
|
1686
2016
|
|
|
1687
2017
|
/** Correction 1: the reduction's inputs echoed with their structure — the measurement's echo, with what a reduction adds. */
|
|
1688
|
-
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]
|
|
2018
|
+
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]>, plant: ReductionPlant | null): EffectiveSettings {
|
|
2019
|
+
const amplitude = plant === 'echo-when-unset' && input.motionAmplitude === undefined ? null : input.motionAmplitude;
|
|
1689
2020
|
const fit = (f: ArtFitBounds): ArtFitBounds => ({ minCoverage: f.minCoverage, maxOvershoot: f.maxOvershoot, maxUndercut: f.maxUndercut });
|
|
1690
2021
|
return {
|
|
1691
2022
|
...measured,
|
|
@@ -1695,5 +2026,8 @@ function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sou
|
|
|
1695
2026
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
1696
2027
|
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|
|
1697
2028
|
...(input.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: input.boundaryRuns.maxVertices } }),
|
|
2029
|
+
...(amplitude === undefined ? {} : { motionAmplitude: amplitude }),
|
|
2030
|
+
...(input.retriangulate === undefined ? {} : { retriangulate: input.retriangulate }),
|
|
2031
|
+
...(input.removalOrder === undefined ? {} : { removalOrder: input.removalOrder }),
|
|
1698
2032
|
};
|
|
1699
2033
|
}
|