rig-c 2.30.0 → 2.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/meshcarriers.ts +116 -42
- package/src/meshquality.ts +16 -1
- package/src/meshreduce.ts +462 -59
- package/src/meshskinning.ts +735 -44
package/src/meshreduce.ts
CHANGED
|
@@ -79,6 +79,17 @@
|
|
|
79
79
|
* measurements' rows (`amplitudeOf`) — so the mesh and every step are the
|
|
80
80
|
* call's without it.
|
|
81
81
|
*
|
|
82
|
+
* ## The skinning residual (issue #1295, opt-in)
|
|
83
|
+
*
|
|
84
|
+
* With `targets.skinning` and a number for its `maxResidual`, every removal,
|
|
85
|
+
* boundary run and post-pass whose required rows all pass is also held to
|
|
86
|
+
* `MQ_SKINNING_RESIDUAL` against the call's own source — the original, never a
|
|
87
|
+
* previous step — and refused by name when it is over; the refinement's outcome
|
|
88
|
+
* is held to it before any removal. The value is carried per sample
|
|
89
|
+
* (`SkinningCarry`, `src/meshskinning.ts`), so a step recomputes only the samples
|
|
90
|
+
* of the triangles it changed; the result's own measurement reads the row in
|
|
91
|
+
* full. Left out, nothing here reads it and the call is the one it was.
|
|
92
|
+
*
|
|
82
93
|
* A removal whose deviation floor — the furthest removed source-hull vertex
|
|
83
94
|
* from the candidate's outline, which `MQ_BOUNDARY_DEVIATION` can only exceed —
|
|
84
95
|
* is over the bound is refused without the measurement; its name, when read,
|
|
@@ -149,6 +160,18 @@ import {
|
|
|
149
160
|
type Termination,
|
|
150
161
|
} from './meshquality.ts';
|
|
151
162
|
import { artRastersOf, stepRastersOf, type ArtRasters, type StepRasters } from './meshrasters.ts';
|
|
163
|
+
import {
|
|
164
|
+
skinningContract,
|
|
165
|
+
SkinningCarry,
|
|
166
|
+
validateReductionSkinning,
|
|
167
|
+
skinningResidual,
|
|
168
|
+
type CarryCheck,
|
|
169
|
+
type CarryPlant,
|
|
170
|
+
type CarryReading,
|
|
171
|
+
type CarryTally,
|
|
172
|
+
type SkinningMeasureArgs,
|
|
173
|
+
type SkinningResidualInput,
|
|
174
|
+
} from './meshskinning.ts';
|
|
152
175
|
|
|
153
176
|
// ---------------------------------------------------------------------------
|
|
154
177
|
// the result
|
|
@@ -319,6 +342,8 @@ function validateReduction(input: MeshReductionInput, plant: ReductionPlant | nu
|
|
|
319
342
|
// Issue #1287: refused here, before the admission measurement and any step, in the measurement's own words — the
|
|
320
343
|
// admission does not carry the field, and a call whose source is refused never reaches the one measurement that does.
|
|
321
344
|
if (plant !== 'amplitude-unvalidated') validateMotionAmplitude(who, input.motionAmplitude);
|
|
345
|
+
// Issue #1295: refused before any work in the measurement's own words, under `targets.skinning`.
|
|
346
|
+
validateReductionSkinning(who, t.skinning);
|
|
322
347
|
const src = input.source;
|
|
323
348
|
if (!isObject(src) || !Array.isArray(src.points)) refuse('REDUCE_INPUT_MISSING', `${who}: source is not { points, uvs, triangles, hull, weights }`);
|
|
324
349
|
const n = src.points.length;
|
|
@@ -405,6 +430,98 @@ function edgeKey(a: number, b: number): string {
|
|
|
405
430
|
return a < b ? `${a},${b}` : `${b},${a}`;
|
|
406
431
|
}
|
|
407
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Issue #1307: an undirected edge as one number, `min * n + max`, for ids below `n` — the key every edge set on the
|
|
435
|
+
* removal and boundary-run paths is held in, where `edgeKey` built a string per edge. Exact below 2^53, so for any
|
|
436
|
+
* `n` up to 94,906,265.
|
|
437
|
+
*/
|
|
438
|
+
function pairKey(a: number, b: number, n: number): number {
|
|
439
|
+
return a < b ? a * n + b : b * n + a;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Issue #1307: the edges `fresh`'s triangles hold that no triangle of `old` holds, each once, in the order `fresh`
|
|
444
|
+
* holds them (triangle by triangle, corner k to k + 1) — the order condition (b) reads them in and names the first
|
|
445
|
+
* over `weightJump`. Keyed by number over ids below `n`, and only the keys `fresh` holds are looked for in `old`.
|
|
446
|
+
* `addedEdgesByString` is the construction it replaced, kept as the one it is held equal to.
|
|
447
|
+
*/
|
|
448
|
+
function addedEdges(old: ReadonlyArray<readonly number[]>, fresh: ReadonlyArray<readonly number[]>, n: number): Array<[number, number]> {
|
|
449
|
+
const wanted = new Set<number>();
|
|
450
|
+
for (const t of fresh) for (let k = 0; k < 3; k++) wanted.add(pairKey(t[k], t[(k + 1) % 3], n));
|
|
451
|
+
const held = new Set<number>();
|
|
452
|
+
for (const t of old) {
|
|
453
|
+
for (let k = 0; k < 3; k++) {
|
|
454
|
+
const key = pairKey(t[k], t[(k + 1) % 3], n);
|
|
455
|
+
if (wanted.has(key)) held.add(key);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const added: Array<[number, number]> = [];
|
|
459
|
+
const seen = new Set<number>();
|
|
460
|
+
for (const t of fresh) {
|
|
461
|
+
for (let k = 0; k < 3; k++) {
|
|
462
|
+
const key = pairKey(t[k], t[(k + 1) % 3], n);
|
|
463
|
+
if (held.has(key) || seen.has(key)) continue;
|
|
464
|
+
seen.add(key);
|
|
465
|
+
added.push([t[k], t[(k + 1) % 3]]);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return added;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** The string-keyed construction `addedEdges` replaced (issue #1307), run only by the edge-set audit. */
|
|
472
|
+
function addedEdgesByString(old: ReadonlyArray<readonly number[]>, fresh: ReadonlyArray<readonly number[]>): Array<[number, number]> {
|
|
473
|
+
const had = new Set<string>();
|
|
474
|
+
for (const t of old) for (let k = 0; k < 3; k++) had.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
475
|
+
const added: Array<[number, number]> = [];
|
|
476
|
+
const seen = new Set<string>();
|
|
477
|
+
for (const t of fresh) {
|
|
478
|
+
for (let k = 0; k < 3; k++) {
|
|
479
|
+
const key = edgeKey(t[k], t[(k + 1) % 3]);
|
|
480
|
+
if (had.has(key) || seen.has(key)) continue;
|
|
481
|
+
seen.add(key);
|
|
482
|
+
added.push([t[k], t[(k + 1) % 3]]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return added;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Issue #1307's audit, for the `mesh-compare` suite's control: every edge set the removal and boundary-run paths
|
|
490
|
+
* build is built a second time the string-keyed way it was before the issue and compared — the added edges as a list
|
|
491
|
+
* (membership and order, the order condition (b) reads), the triangulation's edge set the protected edges are looked
|
|
492
|
+
* up in as a set (its order is never read). A difference is recorded, not thrown, so the control reads every one.
|
|
493
|
+
* `plant` is the fault applied to the numeric construction before the comparison: one edge dropped from each list
|
|
494
|
+
* and each set, or each added list of two or more reversed. With the audit and no plant, the call's result is the
|
|
495
|
+
* call's without it.
|
|
496
|
+
*/
|
|
497
|
+
export interface EdgeSetAudit {
|
|
498
|
+
plant?: 'drop-one' | 'reorder';
|
|
499
|
+
/** Added-edge lists compared, and the edges in them. */
|
|
500
|
+
addedLists: number;
|
|
501
|
+
addedEdges: number;
|
|
502
|
+
/** Lists among them whose order a difference could move — two edges or more. */
|
|
503
|
+
orderedLists: number;
|
|
504
|
+
/** Triangulation edge sets compared (one per operation reaching the protected-edge check), and the edges in them. */
|
|
505
|
+
edgeSets: number;
|
|
506
|
+
edgeSetEdges: number;
|
|
507
|
+
/** Each difference found, named; the first few in full. */
|
|
508
|
+
differences: string[];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** One added-edge list read, through the audit when there is one: the plant applied, then the string construction compared. */
|
|
512
|
+
function auditedAdded(audit: EdgeSetAudit | null, where: string, added: Array<[number, number]>, byString: () => Array<[number, number]>): Array<[number, number]> {
|
|
513
|
+
if (audit === null) return added;
|
|
514
|
+
let read = added;
|
|
515
|
+
if (audit.plant === 'drop-one' && read.length > 0) read = read.slice(0, -1);
|
|
516
|
+
if (audit.plant === 'reorder' && read.length > 1) read = [...read].reverse();
|
|
517
|
+
const want = byString();
|
|
518
|
+
audit.addedLists++;
|
|
519
|
+
audit.addedEdges += want.length;
|
|
520
|
+
if (want.length > 1) audit.orderedLists++;
|
|
521
|
+
if (JSON.stringify(read) !== JSON.stringify(want)) audit.differences.push(`${where}: added edges ${JSON.stringify(read)} against the string construction's ${JSON.stringify(want)}`);
|
|
522
|
+
return read;
|
|
523
|
+
}
|
|
524
|
+
|
|
408
525
|
// ---------------------------------------------------------------------------
|
|
409
526
|
// the working mesh — vertices by stable id: source index, then insertion order
|
|
410
527
|
// ---------------------------------------------------------------------------
|
|
@@ -798,11 +915,61 @@ interface Run {
|
|
|
798
915
|
keyed: Map<number, { animation: string; ref: AttachmentRef; key: number; time: number }>;
|
|
799
916
|
protectedVertices: Set<number>;
|
|
800
917
|
protectedEdges: Array<[number, number]>;
|
|
801
|
-
|
|
918
|
+
/** The source's edges, `pairKey` over `work.nSource` (issue #1307). */
|
|
919
|
+
sourceEdges: Set<number>;
|
|
920
|
+
/** Issue #1307's audit of the edge sets, or null. */
|
|
921
|
+
edgeAudit: EdgeSetAudit | null;
|
|
802
922
|
/** The art's rasters, taken once for the call and read by every measurement in it (issue #1240). */
|
|
803
923
|
rasters: ArtRasters;
|
|
804
924
|
/** The step state each removal and insertion is measured through, carried from the last measurement (issue #1246); null measures every step in full. */
|
|
805
925
|
stepRasters: StepRasters | null;
|
|
926
|
+
/** Issue #1295: the residual every step is held to, or null when the call did not declare a bound for it. */
|
|
927
|
+
skinning: SkinningRun | null;
|
|
928
|
+
/** Issue #1295: the controls' faults and inspector, or null. */
|
|
929
|
+
hooks: SkinningHooks | null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Issue #1295: the residual a reduction's steps are vetoed by — the bound, the measurement input the result's own row
|
|
934
|
+
* is read with, and either the carried state (`SkinningCarry`), a reading the admission could not measure (every step
|
|
935
|
+
* is then blocked by it), or neither, under the full-recompute control, which measures each candidate whole.
|
|
936
|
+
*/
|
|
937
|
+
interface SkinningRun {
|
|
938
|
+
bound: number;
|
|
939
|
+
input: SkinningResidualInput;
|
|
940
|
+
carry: SkinningCarry | null;
|
|
941
|
+
unmeasured: CarryReading | null;
|
|
942
|
+
args: Omit<SkinningMeasureArgs, 'candidate'>;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Issue #1295's faults, for the `mesh-compare` suite's controls: the carried state's own (`CarryPlant`); the residual
|
|
947
|
+
* measured whole on every trial — not a fault, the path the carried state is held equal to and the cost is measured
|
|
948
|
+
* against; the post-pass taken without the residual's recheck; `effective` echoing `targets.skinning: null` for a
|
|
949
|
+
* call that left it out; and a residual refusal read as the end of the search rather than one refusal among others.
|
|
950
|
+
*/
|
|
951
|
+
export type SkinningVetoPlant = CarryPlant | 'full-recompute' | 'post-pass-unchecked' | 'echo-when-unset' | 'veto-ends-search';
|
|
952
|
+
|
|
953
|
+
/** One decision the residual took part in, as a control reads it (issue #1295). */
|
|
954
|
+
export interface SkinningInspection {
|
|
955
|
+
phase: 'insertion' | 'removal' | 'boundary-run' | 'delaunay';
|
|
956
|
+
/** `candidatesTried` when it was decided. */
|
|
957
|
+
step: number;
|
|
958
|
+
/** Whether the working mesh now holds the trial (an insertion always does). */
|
|
959
|
+
accepted: boolean;
|
|
960
|
+
/** The residual's reading of the trial. */
|
|
961
|
+
reading: CarryReading;
|
|
962
|
+
/** The carried state's work so far; null under the full-recompute control. */
|
|
963
|
+
tally: CarryTally | null;
|
|
964
|
+
memoryBytes: () => number;
|
|
965
|
+
/** The carried state as it now stands held to the full recompute over the working mesh as it now stands; null under the full-recompute control. */
|
|
966
|
+
check: () => CarryCheck | null;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** What a control hands `reduceMeshWith` for issue #1295: a fault, and a callback after every decision the residual took part in. */
|
|
970
|
+
export interface SkinningHooks {
|
|
971
|
+
plant?: SkinningVetoPlant;
|
|
972
|
+
inspect?: (inspection: SkinningInspection) => void;
|
|
806
973
|
}
|
|
807
974
|
|
|
808
975
|
/**
|
|
@@ -826,10 +993,76 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string, final = f
|
|
|
826
993
|
boneOrder: input.boneOrder,
|
|
827
994
|
preset: input.preset,
|
|
828
995
|
...amplitudeOf(run.input, run.plant, final),
|
|
996
|
+
...(final ? skinningOfResult(run) : {}),
|
|
829
997
|
};
|
|
830
998
|
return run.stepRasters === null ? measureMeshQualityWith(measureInput, run.rasters) : measureMeshQualityStep(measureInput, run.stepRasters);
|
|
831
999
|
}
|
|
832
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Issue #1295: what the result's own measurement is handed as `skinning` — the input's envelope and bound against the
|
|
1003
|
+
* call's own source, so the report's row is the measurement's, written as `measureMeshQuality` writes it; `null` as
|
|
1004
|
+
* `null`; nothing when the field was left out. No other measurement of the call carries it: the steps read the
|
|
1005
|
+
* carried state.
|
|
1006
|
+
*/
|
|
1007
|
+
function skinningOfResult(run: Run): { skinning?: SkinningResidualInput | null } {
|
|
1008
|
+
const declared = run.input.targets.skinning;
|
|
1009
|
+
if (declared === undefined) return {};
|
|
1010
|
+
if (declared === null) return { skinning: null };
|
|
1011
|
+
return { skinning: skinningInputOf(run.input) };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function skinningInputOf(input: MeshReductionInput): SkinningResidualInput {
|
|
1015
|
+
const declared = input.targets.skinning!;
|
|
1016
|
+
return { source: { id: 'source', mesh: input.source }, envelope: declared.envelope, maxResidual: declared.maxResidual, deform: input.deform };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Issue #1295: the residual of the working mesh as it now stands — a carried trial, left pending until `settleSkinning`;
|
|
1021
|
+
* under the full-recompute control, the measurement over the canonical candidate; or the admission's reading when it
|
|
1022
|
+
* could not measure.
|
|
1023
|
+
*/
|
|
1024
|
+
function skinningTrial(run: Run, canonical: () => SourceMesh): CarryReading {
|
|
1025
|
+
const sk = run.skinning!;
|
|
1026
|
+
if (sk.unmeasured !== null) return sk.unmeasured;
|
|
1027
|
+
if (sk.carry === null) {
|
|
1028
|
+
const reading = skinningResidual({ ...sk.args, candidate: canonical() });
|
|
1029
|
+
return reading.state === 'measured' ? { state: 'measured', value: reading.value } : { state: reading.state, reason: reading.reason };
|
|
1030
|
+
}
|
|
1031
|
+
return sk.carry.trial(run.work.triangles);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** Issue #1295: the residual's reading as the name a refusal carries — `rowName`'s form — or null when it is within the bound. */
|
|
1035
|
+
function skinningVeto(sk: SkinningRun, reading: CarryReading): string | null {
|
|
1036
|
+
if (reading.state === 'measured') return reading.value <= sk.bound ? null : `MQ_SKINNING_RESIDUAL: ${reading.value} against <= ${sk.bound}`;
|
|
1037
|
+
return `MQ_SKINNING_RESIDUAL: ${reading.state} — ${reading.reason}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/** Issue #1295: keep or undo a pending trial, then let a control read the decision. The working mesh is already the one decided on. */
|
|
1041
|
+
function settleSkinning(run: Run, keep: boolean, phase: SkinningInspection['phase'], reading: CarryReading): void {
|
|
1042
|
+
const carry = run.skinning?.carry ?? null;
|
|
1043
|
+
if (carry !== null && run.skinning!.unmeasured === null) {
|
|
1044
|
+
if (keep) carry.commit();
|
|
1045
|
+
else carry.rollback();
|
|
1046
|
+
}
|
|
1047
|
+
const inspect = run.hooks?.inspect;
|
|
1048
|
+
if (inspect === undefined) return;
|
|
1049
|
+
inspect({
|
|
1050
|
+
phase,
|
|
1051
|
+
step: run.steps,
|
|
1052
|
+
accepted: keep,
|
|
1053
|
+
reading,
|
|
1054
|
+
tally: carry === null ? null : { ...carry.tally },
|
|
1055
|
+
memoryBytes: () => (carry === null ? 0 : carry.memoryBytes()),
|
|
1056
|
+
check: () => (carry === null ? null : carry.compareWithFull(canonicalise(run.work, run.sourceTurn, run.boneRank).mesh)),
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Issue #1295: an operation the residual does not decide — a refinement insertion, or a step a plant takes unmeasured — carried so the state follows the working mesh. */
|
|
1061
|
+
function followSkinning(run: Run, phase: SkinningInspection['phase']): void {
|
|
1062
|
+
if (run.skinning === null || run.skinning.carry === null || run.skinning.unmeasured !== null) return;
|
|
1063
|
+
settleSkinning(run, true, phase, run.skinning.carry.trial(run.work.triangles));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
833
1066
|
/**
|
|
834
1067
|
* Issue #1287: what one measurement of a reduction is handed as `motionAmplitude` — the input's, on the result's own
|
|
835
1068
|
* measurement (`final`) only. The admission, every refinement and removal step and the post-pass are measured without
|
|
@@ -1163,6 +1396,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1163
1396
|
if (target.state === 'not-measurable') {
|
|
1164
1397
|
const p: Pt = [r6(region.polygon[0][0]), r6(region.polygon[0][1])];
|
|
1165
1398
|
if (!insertPoint(run, p)) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no triangle holding the region's first vertex)` };
|
|
1399
|
+
followSkinning(run, 'insertion');
|
|
1166
1400
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1167
1401
|
continue;
|
|
1168
1402
|
}
|
|
@@ -1178,6 +1412,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1178
1412
|
const q = outerSplit(run.work.pos[inner], run.work.pos[outer], region);
|
|
1179
1413
|
if (q === null) continue;
|
|
1180
1414
|
splitEdge(run, a, b, q);
|
|
1415
|
+
followSkinning(run, 'insertion');
|
|
1181
1416
|
exited = true;
|
|
1182
1417
|
break;
|
|
1183
1418
|
}
|
|
@@ -1207,6 +1442,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1207
1442
|
const p = splitPoint(run.work.pos[a], run.work.pos[b], region);
|
|
1208
1443
|
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)` };
|
|
1209
1444
|
splitEdge(run, a, b, p);
|
|
1445
|
+
followSkinning(run, 'insertion');
|
|
1210
1446
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1211
1447
|
}
|
|
1212
1448
|
}
|
|
@@ -1215,8 +1451,17 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1215
1451
|
// reduction (§1, §6)
|
|
1216
1452
|
// ---------------------------------------------------------------------------
|
|
1217
1453
|
|
|
1218
|
-
/**
|
|
1219
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* One removal tried: the working triangles after it and the edges it added — read through `added()`, built only when
|
|
1456
|
+
* read (issue #1307) — or the structural reason it cannot be made.
|
|
1457
|
+
*/
|
|
1458
|
+
type Removal = { triangles: Array<[number, number, number]>; added: () => Array<[number, number]> } | { blocked: string };
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* One removal made on `work`'s triangles: the triangles after it — every triangle not touching `v` as it was, the
|
|
1462
|
+
* same tuple, then the hole's new ones, `fresh` — or the structural reason it cannot be made.
|
|
1463
|
+
*/
|
|
1464
|
+
function removalStep(work: Work, v: number): { triangles: Array<[number, number, number]>; fresh: Array<[number, number, number]> } | { blocked: string } {
|
|
1220
1465
|
const star = work.triangles.filter((t) => t.includes(v));
|
|
1221
1466
|
const next = new Map<number, number>();
|
|
1222
1467
|
const incoming = new Set<number>();
|
|
@@ -1257,19 +1502,16 @@ function removalOf(work: Work, v: number): { triangles: Array<[number, number, n
|
|
|
1257
1502
|
} else if (!boundary) {
|
|
1258
1503
|
return { blocked: `retriangulation: vertex ${v} has fewer than three neighbours` };
|
|
1259
1504
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return { triangles: [...rest, ...fresh], added };
|
|
1505
|
+
return { triangles: [...rest, ...fresh], fresh };
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/** `removalStep` with the edges it adds: those of the hole's new triangles no triangle before it holds, in their order. */
|
|
1509
|
+
function removalOf(work: Work, v: number, audit: EdgeSetAudit | null = null): Removal {
|
|
1510
|
+
const step = removalStep(work, v);
|
|
1511
|
+
if ('blocked' in step) return step;
|
|
1512
|
+
const old = work.triangles;
|
|
1513
|
+
const n = work.alive.length;
|
|
1514
|
+
return { triangles: step.triangles, added: () => auditedAdded(audit, `removing ${v}`, addedEdges(old, step.fresh, n), () => addedEdgesByString(old, step.fresh)) };
|
|
1273
1515
|
}
|
|
1274
1516
|
|
|
1275
1517
|
/** L1 difference of two weight vectors over the bones they name. */
|
|
@@ -1306,10 +1548,10 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
|
|
|
1306
1548
|
const order: number[] = [];
|
|
1307
1549
|
for (let v = 0; v < work.nSource; v++) {
|
|
1308
1550
|
if (!work.alive[v] || run.protectedVertices.has(v)) continue;
|
|
1309
|
-
const step = removalOf(work, v);
|
|
1551
|
+
const step = removalOf(work, v, run.edgeAudit);
|
|
1310
1552
|
let most = 0;
|
|
1311
1553
|
if ('blocked' in step) most = Infinity;
|
|
1312
|
-
else for (const [a, b] of step.added) most = Math.max(most, edgeLoad(work, a, b));
|
|
1554
|
+
else for (const [a, b] of step.added()) most = Math.max(most, edgeLoad(work, a, b));
|
|
1313
1555
|
load.set(v, most);
|
|
1314
1556
|
order.push(v);
|
|
1315
1557
|
}
|
|
@@ -1329,32 +1571,27 @@ function loadOrder(run: Run): { order: number[]; load: Map<number, number> } {
|
|
|
1329
1571
|
* structural reason one of the removals cannot be made. The outline loses the run and gains the chord from the
|
|
1330
1572
|
* vertex before it to the vertex after it; nothing is measured in between.
|
|
1331
1573
|
*/
|
|
1332
|
-
function runRemovalOf(work: Work, vertices: readonly number[]
|
|
1574
|
+
function runRemovalOf(work: Work, vertices: readonly number[], audit: EdgeSetAudit | null = null): Removal {
|
|
1333
1575
|
const was = work.triangles;
|
|
1576
|
+
// Issue #1307: each removal's own added edges are never read here, so none is built; what the run adds is read off
|
|
1577
|
+
// the triangles its removals made. A triangle of `after` that no removal made is the same tuple as one of `was`
|
|
1578
|
+
// (`removalStep` keeps every untouched triangle as it was), so every edge it holds is old and it adds nothing.
|
|
1579
|
+
const made = new Set<readonly number[]>();
|
|
1334
1580
|
let after: Array<[number, number, number]>;
|
|
1335
1581
|
try {
|
|
1336
1582
|
for (const v of vertices) {
|
|
1337
|
-
const step =
|
|
1583
|
+
const step = removalStep(work, v);
|
|
1338
1584
|
if ('blocked' in step) return step;
|
|
1585
|
+
for (const t of step.fresh) made.add(t);
|
|
1339
1586
|
work.triangles = step.triangles;
|
|
1340
1587
|
}
|
|
1341
1588
|
after = work.triangles;
|
|
1342
1589
|
} finally {
|
|
1343
1590
|
work.triangles = was;
|
|
1344
1591
|
}
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
const seen = new Set<string>();
|
|
1349
|
-
for (const t of after) {
|
|
1350
|
-
for (let k = 0; k < 3; k++) {
|
|
1351
|
-
const key = edgeKey(t[k], t[(k + 1) % 3]);
|
|
1352
|
-
if (old.has(key) || seen.has(key)) continue;
|
|
1353
|
-
seen.add(key);
|
|
1354
|
-
added.push([t[k], t[(k + 1) % 3]]);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
return { triangles: after, added };
|
|
1592
|
+
const n = work.alive.length;
|
|
1593
|
+
const label = `removing ${vertices.join(', ')} as one boundary run`;
|
|
1594
|
+
return { triangles: after, added: () => auditedAdded(audit, label, addedEdges(was, after.filter((t) => made.has(t)), n), () => addedEdgesByString(was, after)) };
|
|
1358
1595
|
}
|
|
1359
1596
|
|
|
1360
1597
|
/**
|
|
@@ -1448,7 +1685,10 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1448
1685
|
if (block === null) {
|
|
1449
1686
|
taken++;
|
|
1450
1687
|
if (accept(run, 'removal', [v])) return { kind: 'replayed' };
|
|
1451
|
-
} else
|
|
1688
|
+
} else {
|
|
1689
|
+
lastBlock = { refusal: block, what: `removing source vertex ${v}` };
|
|
1690
|
+
if (run.hooks?.plant === 'veto-ends-search' && typeof block === 'string' && block.startsWith('MQ_SKINNING_RESIDUAL')) return { kind: 'stuck', constraint: lastName() };
|
|
1691
|
+
}
|
|
1452
1692
|
}
|
|
1453
1693
|
if (taken === 0) {
|
|
1454
1694
|
// A pass that took nothing left the working mesh as it found it, so a refusal the floor decided is measured
|
|
@@ -1485,6 +1725,28 @@ function deviationFloor(run: Run, mesh: SourceMesh, vertices: readonly number[])
|
|
|
1485
1725
|
*/
|
|
1486
1726
|
const FLOOR_MARGIN = 1e-5;
|
|
1487
1727
|
|
|
1728
|
+
/**
|
|
1729
|
+
* Issue #1307's audit of the triangulation's edge set (`EdgeSetAudit`): the plant applied to `edges` in place — so
|
|
1730
|
+
* the decision reads the planted set — then the set held to the string-keyed one, member for member.
|
|
1731
|
+
*/
|
|
1732
|
+
function auditEdgeSet(audit: EdgeSetAudit, edges: Set<number>, triangles: ReadonlyArray<readonly number[]>, n: number, vertices: readonly number[]): void {
|
|
1733
|
+
if (audit.plant === 'drop-one') {
|
|
1734
|
+
const first = edges.values().next();
|
|
1735
|
+
if (first.done !== true) edges.delete(first.value);
|
|
1736
|
+
}
|
|
1737
|
+
const want = new Set<string>();
|
|
1738
|
+
for (const t of triangles) for (let k = 0; k < 3; k++) want.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
1739
|
+
audit.edgeSets++;
|
|
1740
|
+
audit.edgeSetEdges += want.size;
|
|
1741
|
+
const missing = [...want].filter((key) => {
|
|
1742
|
+
const [a, b] = key.split(',').map(Number);
|
|
1743
|
+
return !edges.has(pairKey(a, b, n));
|
|
1744
|
+
});
|
|
1745
|
+
if (missing.length > 0 || edges.size !== want.size) {
|
|
1746
|
+
audit.differences.push(`removing ${vertices.join(', ')}: the triangulation's edge set holds ${edges.size} edge(s) against the string construction's ${want.size}${missing.length > 0 ? `, missing ${missing.slice(0, 3).join(' ')}` : ''}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1488
1750
|
/**
|
|
1489
1751
|
* One operation — a single removal, or a boundary run of two or more — tried: null when it was taken (the working
|
|
1490
1752
|
* mesh then holds it), else what refused it (the working mesh as it was). `full` measures every candidate that
|
|
@@ -1493,12 +1755,12 @@ const FLOOR_MARGIN = 1e-5;
|
|
|
1493
1755
|
*/
|
|
1494
1756
|
function tryOperation(run: Run, vertices: readonly number[], full: boolean): Refusal | null {
|
|
1495
1757
|
const { work, input } = run;
|
|
1496
|
-
const step = vertices.length === 1 ? removalOf(work, vertices[0]) : runRemovalOf(work, vertices);
|
|
1758
|
+
const step = vertices.length === 1 ? removalOf(work, vertices[0], run.edgeAudit) : runRemovalOf(work, vertices, run.edgeAudit);
|
|
1497
1759
|
if ('blocked' in step) return step.blocked;
|
|
1498
1760
|
const jump = input.protect.weightJump;
|
|
1499
1761
|
if (jump !== null) {
|
|
1500
|
-
for (const [a, b] of step.added) {
|
|
1501
|
-
if (a < work.nSource && b < work.nSource && run.sourceEdges.has(
|
|
1762
|
+
for (const [a, b] of step.added()) {
|
|
1763
|
+
if (a < work.nSource && b < work.nSource && run.sourceEdges.has(pairKey(a, b, work.nSource))) continue;
|
|
1502
1764
|
const d = weightJump(work, a, b);
|
|
1503
1765
|
if (d > jump) return `weightJump (b): the new edge ${a}–${b} joins weight vectors ${r6(d)} apart, over ${jump}`;
|
|
1504
1766
|
}
|
|
@@ -1510,12 +1772,17 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1510
1772
|
work.triangles = was.triangles;
|
|
1511
1773
|
for (const v of vertices) work.alive[v] = true;
|
|
1512
1774
|
};
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1775
|
+
// Issue #1307: the triangulation's edge set is built only when a protected edge is looked up in it, keyed by number.
|
|
1776
|
+
if (run.protectedEdges.length > 0) {
|
|
1777
|
+
const n = work.alive.length;
|
|
1778
|
+
const edges = new Set<number>();
|
|
1779
|
+
for (const t of work.triangles) for (let k = 0; k < 3; k++) edges.add(pairKey(t[k], t[(k + 1) % 3], n));
|
|
1780
|
+
if (run.edgeAudit !== null) auditEdgeSet(run.edgeAudit, edges, work.triangles, n, vertices);
|
|
1781
|
+
for (const [a, b] of run.protectedEdges) {
|
|
1782
|
+
if (!edges.has(pairKey(a, b, n))) {
|
|
1783
|
+
undo();
|
|
1784
|
+
return `protect (a): the protected source edge ${a}–${b} is no longer an edge`;
|
|
1785
|
+
}
|
|
1519
1786
|
}
|
|
1520
1787
|
}
|
|
1521
1788
|
let canon: Canonical;
|
|
@@ -1526,7 +1793,11 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1526
1793
|
undo();
|
|
1527
1794
|
return `outline: ${err.message}`;
|
|
1528
1795
|
}
|
|
1529
|
-
|
|
1796
|
+
const phase: SkinningInspection['phase'] = vertices.length > 1 ? 'boundary-run' : 'removal';
|
|
1797
|
+
if (vertices.length > 1 && run.plant === 'run-skips-rows') {
|
|
1798
|
+
followSkinning(run, phase);
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1530
1801
|
if (!full && run.plant !== 'measure-every-candidate') {
|
|
1531
1802
|
const margin = run.plant === 'floor-half-a-pixel-short' ? -0.5 : FLOOR_MARGIN;
|
|
1532
1803
|
if (deviationFloor(run, canon.mesh, vertices) > input.targets.maxBoundaryDeviation + margin) {
|
|
@@ -1551,6 +1822,18 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1551
1822
|
undo();
|
|
1552
1823
|
return rowName(blocking);
|
|
1553
1824
|
}
|
|
1825
|
+
// Issue #1295: every required row passes, so the candidate has no reversed or degenerate triangle; now the residual
|
|
1826
|
+
// against the original source. A veto is a refusal like any other: the mesh and the carried state as they were.
|
|
1827
|
+
if (run.skinning !== null) {
|
|
1828
|
+
const reading = skinningTrial(run, () => canon.mesh);
|
|
1829
|
+
const veto = skinningVeto(run.skinning, reading);
|
|
1830
|
+
if (veto !== null) {
|
|
1831
|
+
undo();
|
|
1832
|
+
settleSkinning(run, false, phase, reading);
|
|
1833
|
+
return veto;
|
|
1834
|
+
}
|
|
1835
|
+
settleSkinning(run, true, phase, reading);
|
|
1836
|
+
}
|
|
1554
1837
|
return null;
|
|
1555
1838
|
}
|
|
1556
1839
|
|
|
@@ -1681,7 +1964,7 @@ function delaunayFlips(run: Run, plant: ReductionPlant | null): FlipResult {
|
|
|
1681
1964
|
const sine = oppositeAngleSine(P[a], P[b], P[c], P[d]);
|
|
1682
1965
|
if (against ? !(sine > FLIP_MARGIN) : !(sine < -FLIP_MARGIN)) continue;
|
|
1683
1966
|
if (held(a, b) || held(c, d)) continue;
|
|
1684
|
-
if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(
|
|
1967
|
+
if (jump !== null && !(c < work.nSource && d < work.nSource && run.sourceEdges.has(pairKey(c, d, work.nSource))) && weightJump(work, c, d) > jump) continue;
|
|
1685
1968
|
if (flips >= maxFlips) return { bound: `the flip bound n(n - 1)/2 = ${maxFlips} over ${alive} vertices was reached`, flips, sweeps };
|
|
1686
1969
|
tris[i] = [a, d, c];
|
|
1687
1970
|
tris[j] = [d, b, c];
|
|
@@ -1718,12 +2001,29 @@ function retriangulateResult(run: Run, termination: Termination): Retriangulatio
|
|
|
1718
2001
|
if (result.flips === 0) return out(true, null);
|
|
1719
2002
|
const was = run.work.triangles;
|
|
1720
2003
|
run.work.triangles = result.triangles;
|
|
1721
|
-
if (plant === 'flips-against-delaunay-unmeasured')
|
|
2004
|
+
if (plant === 'flips-against-delaunay-unmeasured') {
|
|
2005
|
+
followSkinning(run, 'delaunay');
|
|
2006
|
+
return out(true, null);
|
|
2007
|
+
}
|
|
1722
2008
|
const canon = canonicalise(run.work, run.sourceTurn, run.boneRank);
|
|
1723
2009
|
const blocking = firstBlockingRow(measureAgainstTargets(run, canon.mesh, 'retriangulated'), run.input.targets.artFit);
|
|
1724
|
-
if (blocking
|
|
1725
|
-
|
|
1726
|
-
|
|
2010
|
+
if (blocking !== null) {
|
|
2011
|
+
run.work.triangles = was;
|
|
2012
|
+
return out(false, rowName(blocking));
|
|
2013
|
+
}
|
|
2014
|
+
// Issue #1295: the returned triangulation is held to the residual as every step was — it is not in acceptedAt, and
|
|
2015
|
+
// that is no reason for it to escape the bound. Refused whole, naming the row.
|
|
2016
|
+
if (run.skinning !== null) {
|
|
2017
|
+
const reading = skinningTrial(run, () => canon.mesh);
|
|
2018
|
+
const veto = run.hooks?.plant === 'post-pass-unchecked' ? null : skinningVeto(run.skinning, reading);
|
|
2019
|
+
if (veto !== null) {
|
|
2020
|
+
run.work.triangles = was;
|
|
2021
|
+
settleSkinning(run, false, 'delaunay', reading);
|
|
2022
|
+
return out(false, veto);
|
|
2023
|
+
}
|
|
2024
|
+
settleSkinning(run, true, 'delaunay', reading);
|
|
2025
|
+
}
|
|
2026
|
+
return out(true, null);
|
|
1727
2027
|
}
|
|
1728
2028
|
|
|
1729
2029
|
// ---------------------------------------------------------------------------
|
|
@@ -1783,19 +2083,29 @@ export function reduceMeshWith(
|
|
|
1783
2083
|
steps: StepRasters | null = stepRastersOf(rasters),
|
|
1784
2084
|
plant: ReductionPlant | null = null,
|
|
1785
2085
|
observe: AttemptObserver | null = null,
|
|
2086
|
+
skinning: SkinningHooks | null = null,
|
|
2087
|
+
edgeAudit: EdgeSetAudit | null = null,
|
|
1786
2088
|
): MeshReductionResult {
|
|
1787
2089
|
validateReduction(input, plant);
|
|
1788
2090
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1789
2091
|
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))`);
|
|
1790
2092
|
}
|
|
1791
|
-
return reduceValidated(input, rasters, steps, plant, observe);
|
|
2093
|
+
return reduceValidated(input, rasters, steps, plant, observe, skinning, edgeAudit);
|
|
1792
2094
|
}
|
|
1793
2095
|
|
|
1794
2096
|
/**
|
|
1795
2097
|
* The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in
|
|
1796
2098
|
* `rasters`, and every refinement and removal step is measured through `steps` when it is given (issue #1246).
|
|
1797
2099
|
*/
|
|
1798
|
-
function reduceValidated(
|
|
2100
|
+
function reduceValidated(
|
|
2101
|
+
input: MeshReductionInput,
|
|
2102
|
+
rasters: ArtRasters,
|
|
2103
|
+
steps: StepRasters | null,
|
|
2104
|
+
plant: ReductionPlant | null = null,
|
|
2105
|
+
observe: AttemptObserver | null = null,
|
|
2106
|
+
hooks: SkinningHooks | null = null,
|
|
2107
|
+
edgeAudit: EdgeSetAudit | null = null,
|
|
2108
|
+
): MeshReductionResult {
|
|
1799
2109
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1800
2110
|
const src = input.source;
|
|
1801
2111
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
@@ -1819,7 +2129,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1819
2129
|
...amplitudeOf(input, plant, false),
|
|
1820
2130
|
};
|
|
1821
2131
|
const admit = steps === null ? measureMeshQualityWith(admitInput, rasters) : measureMeshQualityStep(admitInput, steps);
|
|
1822
|
-
const effective = effectiveOf(input, admit.effective, sourceHull, plant);
|
|
2132
|
+
const effective = effectiveOf(input, admit.effective, sourceHull, plant, hooks);
|
|
1823
2133
|
const sourceCounts = admit.sourceCounts;
|
|
1824
2134
|
const noMesh = (termination: Termination): MeshReductionResult => ({
|
|
1825
2135
|
mesh: null,
|
|
@@ -1849,6 +2159,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1849
2159
|
}
|
|
1850
2160
|
checkIslands(input, who, rasters);
|
|
1851
2161
|
checkDeformBeforeWork(input);
|
|
2162
|
+
const skinningAdmitted = admitSkinning(input, rasters);
|
|
1852
2163
|
|
|
1853
2164
|
const boneRank = new Map((input.boneOrder ?? []).map((b, i) => [b, i]));
|
|
1854
2165
|
const work: Work = {
|
|
@@ -1861,8 +2172,12 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1861
2172
|
sourceTriangle: new Map(),
|
|
1862
2173
|
};
|
|
1863
2174
|
for (let i = 0; i + 2 < src.triangles.length; i += 3) work.triangles.push([src.triangles[i], src.triangles[i + 1], src.triangles[i + 2]]);
|
|
1864
|
-
|
|
1865
|
-
|
|
2175
|
+
// The string-keyed set is read once, by `protectionOf`, whose weightJump edges follow its sorted keys; every step
|
|
2176
|
+
// reads the numeric one (issue #1307).
|
|
2177
|
+
const sourceEdgeNames = new Set<string>();
|
|
2178
|
+
for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdgeNames.add(edgeKey(t[k], t[(k + 1) % 3]));
|
|
2179
|
+
const sourceEdges = new Set<number>();
|
|
2180
|
+
for (const t of work.triangles) for (let k = 0; k < 3; k++) sourceEdges.add(pairKey(t[k], t[(k + 1) % 3], work.nSource));
|
|
1866
2181
|
const run: Run = {
|
|
1867
2182
|
input,
|
|
1868
2183
|
who,
|
|
@@ -1876,11 +2191,15 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1876
2191
|
observe,
|
|
1877
2192
|
tally: { sharesDroppedOnGrid: 0, sharesPruned: 0 },
|
|
1878
2193
|
keyed: keyedSourceVertices(input),
|
|
1879
|
-
...protectionOf(input, work,
|
|
2194
|
+
...protectionOf(input, work, sourceEdgeNames),
|
|
1880
2195
|
sourceEdges,
|
|
2196
|
+
edgeAudit,
|
|
1881
2197
|
rasters,
|
|
1882
2198
|
stepRasters: steps,
|
|
2199
|
+
skinning: null,
|
|
2200
|
+
hooks,
|
|
1883
2201
|
};
|
|
2202
|
+
run.skinning = skinningRunOf(run, skinningAdmitted);
|
|
1884
2203
|
|
|
1885
2204
|
// Issue #1268: a replay ends in its own termination, never as an exhausted budget; a stop the run never reaches
|
|
1886
2205
|
// leaves the run's own termination, which then carries `stopAfterAccepted` saying so.
|
|
@@ -1892,10 +2211,13 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1892
2211
|
if (refined.kind === 'replayed') return finish(run, effective, sourceCounts, replayed());
|
|
1893
2212
|
if (refined.kind === 'budget') return noMesh({ reason: 'budget-exhausted', candidatesTried: run.steps, budget: input.budget.maxCandidates, result: 'none-met-the-targets', ...notReached() });
|
|
1894
2213
|
const startCanon = canonicalise(work, run.sourceTurn, boneRank);
|
|
1895
|
-
const
|
|
2214
|
+
const startRow = firstBlockingRow(measureAgainstTargets(run, startCanon.mesh, 'start'), input.targets.artFit);
|
|
2215
|
+
// Issue #1295: the refined source — the refinement's outcome — held to the residual before any removal; with no
|
|
2216
|
+
// region it is the source, which reads 0 against itself.
|
|
2217
|
+
const startBlock = startRow !== null ? rowName(startRow) : run.skinning === null ? null : skinningStart(run, () => startCanon.mesh);
|
|
1896
2218
|
let termination: Termination;
|
|
1897
2219
|
if (startBlock !== null) {
|
|
1898
|
-
const constraint = refined.kind === 'stuck' ? refined.constraint :
|
|
2220
|
+
const constraint = refined.kind === 'stuck' ? refined.constraint : startBlock;
|
|
1899
2221
|
termination = { reason: 'no-further-valid-reduction', candidatesTried: run.steps, blockingConstraint: `${constraint} — before any removal: the refined source does not meet its targets, so no step from it can`, ...notReached() };
|
|
1900
2222
|
} else {
|
|
1901
2223
|
const reduced = removeVertices(run);
|
|
@@ -1912,6 +2234,87 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1912
2234
|
}
|
|
1913
2235
|
}
|
|
1914
2236
|
|
|
2237
|
+
/**
|
|
2238
|
+
* Issue #1295: the residual's admission — what the source itself reads under the declared envelope, before any step.
|
|
2239
|
+
* Null when no step reads a residual (the field left out, `null`, or a bound declared absent). A source the
|
|
2240
|
+
* measurement refuses — a deform key that moves vertices outside skinning, a bone the envelope does not declare, an
|
|
2241
|
+
* envelope bone the source does not bind, shares that do not close, a vertex off its UV, one mesh weighted and not the
|
|
2242
|
+
* other — is refused here, `invalid-input` with the measurement's code and words; what it cannot measure (both meshes
|
|
2243
|
+
* unweighted, fewer art samples than the floor) is kept as the reading every step is then blocked by.
|
|
2244
|
+
*/
|
|
2245
|
+
function admitSkinning(input: MeshReductionInput, rasters: ArtRasters): { input: SkinningResidualInput; args: Omit<SkinningMeasureArgs, 'candidate'>; unmeasured: CarryReading | null } | null {
|
|
2246
|
+
const declared = input.targets.skinning;
|
|
2247
|
+
if (declared === undefined || declared === null || declared.maxResidual === null) return null;
|
|
2248
|
+
const skinning = skinningInputOf(input);
|
|
2249
|
+
const args: Omit<SkinningMeasureArgs, 'candidate'> = {
|
|
2250
|
+
attachment: input.attachment,
|
|
2251
|
+
frame: input.art.frame,
|
|
2252
|
+
artBits: rasters.artBits(),
|
|
2253
|
+
maskWidth: input.art.mask.width,
|
|
2254
|
+
maskHeight: input.art.mask.height,
|
|
2255
|
+
minArtSamples: input.minArtSamples,
|
|
2256
|
+
threshold: input.art.threshold,
|
|
2257
|
+
skinning,
|
|
2258
|
+
};
|
|
2259
|
+
const contract = skinningContract({ ...args, candidate: input.source });
|
|
2260
|
+
if (!('state' in contract) || contract.state === 'measured') return { input: skinning, args, unmeasured: null };
|
|
2261
|
+
if (contract.state === 'refused') {
|
|
2262
|
+
const code = contract.reason.slice(0, contract.reason.indexOf(':'));
|
|
2263
|
+
throw new Stop('invalid-input', code, contract.reason.slice(code.length + 2));
|
|
2264
|
+
}
|
|
2265
|
+
return { input: skinning, args, unmeasured: { state: 'not-measurable', reason: contract.reason } };
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
/** Issue #1295: the run's residual — the carried state over the working mesh as the reduction starts, or what stands in for it. */
|
|
2269
|
+
function skinningRunOf(run: Run, admitted: ReturnType<typeof admitSkinning>): SkinningRun | null {
|
|
2270
|
+
if (admitted === null) return null;
|
|
2271
|
+
const bound = admitted.input.maxResidual!;
|
|
2272
|
+
const base = { bound, input: admitted.input, args: admitted.args };
|
|
2273
|
+
if (admitted.unmeasured !== null) return { ...base, carry: null, unmeasured: admitted.unmeasured };
|
|
2274
|
+
const plant = run.hooks?.plant;
|
|
2275
|
+
if (plant === 'full-recompute') {
|
|
2276
|
+
const reading = skinningResidual({ ...admitted.args, candidate: run.input.source });
|
|
2277
|
+
if (reading.state === 'refused') {
|
|
2278
|
+
const code = reading.reason.slice(0, reading.reason.indexOf(':'));
|
|
2279
|
+
throw new Stop('invalid-input', code, reading.reason.slice(code.length + 2));
|
|
2280
|
+
}
|
|
2281
|
+
return { ...base, carry: null, unmeasured: null };
|
|
2282
|
+
}
|
|
2283
|
+
const carryPlant: CarryPlant | null = plant === 'stale-carrier' || plant === 'lost-maximum' || plant === 'no-rollback' || plant === 'previous-candidate' ? plant : null;
|
|
2284
|
+
try {
|
|
2285
|
+
const carry = new SkinningCarry(
|
|
2286
|
+
run.who,
|
|
2287
|
+
{ pos: run.work.pos, uv: run.work.uv, weights: run.work.weights! },
|
|
2288
|
+
run.work.nSource,
|
|
2289
|
+
run.work.triangles,
|
|
2290
|
+
run.input.source,
|
|
2291
|
+
admitted.input.envelope,
|
|
2292
|
+
admitted.args.artBits,
|
|
2293
|
+
admitted.args.maskWidth,
|
|
2294
|
+
admitted.args.maskHeight,
|
|
2295
|
+
run.input.art.frame,
|
|
2296
|
+
carryPlant,
|
|
2297
|
+
);
|
|
2298
|
+
return { ...base, carry, unmeasured: null };
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
if (err instanceof MeshReductionError && err.code === 'COMPARE_UV_CARRIER_NOT_UNIQUE') throw new Stop('invalid-input', 'SKINNING_UV_CARRIER_NOT_UNIQUE', err.message);
|
|
2301
|
+
throw err;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
/** Issue #1295: the residual's verdict on the mesh the removals start from — the refinement's outcome — as a refusal name, or null. */
|
|
2306
|
+
function skinningStart(run: Run, canonical: () => SourceMesh): string | null {
|
|
2307
|
+
const sk = run.skinning!;
|
|
2308
|
+
let reading: CarryReading;
|
|
2309
|
+
if (sk.unmeasured !== null) reading = sk.unmeasured;
|
|
2310
|
+
else if (sk.carry !== null) reading = sk.carry.reading();
|
|
2311
|
+
else {
|
|
2312
|
+
const r = skinningResidual({ ...sk.args, candidate: canonical() });
|
|
2313
|
+
reading = r.state === 'measured' ? { state: 'measured', value: r.value } : { state: r.state, reason: r.reason };
|
|
2314
|
+
}
|
|
2315
|
+
return skinningVeto(sk, reading);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1915
2318
|
/** §4: full coverage asked of a source that touches no pixel of some art island is refused, never met by deleting art. */
|
|
1916
2319
|
function checkIslands(input: MeshReductionInput, who: string, rasters: ArtRasters): void {
|
|
1917
2320
|
if (input.targets.artFit.minCoverage !== 1) return;
|
|
@@ -2015,13 +2418,13 @@ function reportOf(effective: EffectiveSettings, sourceCounts: MeshCounts | null,
|
|
|
2015
2418
|
}
|
|
2016
2419
|
|
|
2017
2420
|
/** Correction 1: the reduction's inputs echoed with their structure — the measurement's echo, with what a reduction adds. */
|
|
2018
|
-
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]>, plant: ReductionPlant | null): EffectiveSettings {
|
|
2421
|
+
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]>, plant: ReductionPlant | null, hooks: SkinningHooks | null = null): EffectiveSettings {
|
|
2019
2422
|
const amplitude = plant === 'echo-when-unset' && input.motionAmplitude === undefined ? null : input.motionAmplitude;
|
|
2020
2423
|
const fit = (f: ArtFitBounds): ArtFitBounds => ({ minCoverage: f.minCoverage, maxOvershoot: f.maxOvershoot, maxUndercut: f.maxUndercut });
|
|
2021
2424
|
return {
|
|
2022
2425
|
...measured,
|
|
2023
2426
|
sourceBounds: fit(input.sourceBounds),
|
|
2024
|
-
targets: input.targets,
|
|
2427
|
+
targets: hooks?.plant === 'echo-when-unset' && input.targets.skinning === undefined ? { ...input.targets, skinning: null } : input.targets,
|
|
2025
2428
|
referenceHull: sourceHull,
|
|
2026
2429
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
2027
2430
|
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|