rig-c 2.30.0 → 2.31.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/meshcarriers.ts +116 -42
- package/src/meshquality.ts +16 -1
- package/src/meshreduce.ts +286 -13
- 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;
|
|
@@ -803,6 +828,53 @@ interface Run {
|
|
|
803
828
|
rasters: ArtRasters;
|
|
804
829
|
/** The step state each removal and insertion is measured through, carried from the last measurement (issue #1246); null measures every step in full. */
|
|
805
830
|
stepRasters: StepRasters | null;
|
|
831
|
+
/** Issue #1295: the residual every step is held to, or null when the call did not declare a bound for it. */
|
|
832
|
+
skinning: SkinningRun | null;
|
|
833
|
+
/** Issue #1295: the controls' faults and inspector, or null. */
|
|
834
|
+
hooks: SkinningHooks | null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Issue #1295: the residual a reduction's steps are vetoed by — the bound, the measurement input the result's own row
|
|
839
|
+
* is read with, and either the carried state (`SkinningCarry`), a reading the admission could not measure (every step
|
|
840
|
+
* is then blocked by it), or neither, under the full-recompute control, which measures each candidate whole.
|
|
841
|
+
*/
|
|
842
|
+
interface SkinningRun {
|
|
843
|
+
bound: number;
|
|
844
|
+
input: SkinningResidualInput;
|
|
845
|
+
carry: SkinningCarry | null;
|
|
846
|
+
unmeasured: CarryReading | null;
|
|
847
|
+
args: Omit<SkinningMeasureArgs, 'candidate'>;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Issue #1295's faults, for the `mesh-compare` suite's controls: the carried state's own (`CarryPlant`); the residual
|
|
852
|
+
* measured whole on every trial — not a fault, the path the carried state is held equal to and the cost is measured
|
|
853
|
+
* against; the post-pass taken without the residual's recheck; `effective` echoing `targets.skinning: null` for a
|
|
854
|
+
* call that left it out; and a residual refusal read as the end of the search rather than one refusal among others.
|
|
855
|
+
*/
|
|
856
|
+
export type SkinningVetoPlant = CarryPlant | 'full-recompute' | 'post-pass-unchecked' | 'echo-when-unset' | 'veto-ends-search';
|
|
857
|
+
|
|
858
|
+
/** One decision the residual took part in, as a control reads it (issue #1295). */
|
|
859
|
+
export interface SkinningInspection {
|
|
860
|
+
phase: 'insertion' | 'removal' | 'boundary-run' | 'delaunay';
|
|
861
|
+
/** `candidatesTried` when it was decided. */
|
|
862
|
+
step: number;
|
|
863
|
+
/** Whether the working mesh now holds the trial (an insertion always does). */
|
|
864
|
+
accepted: boolean;
|
|
865
|
+
/** The residual's reading of the trial. */
|
|
866
|
+
reading: CarryReading;
|
|
867
|
+
/** The carried state's work so far; null under the full-recompute control. */
|
|
868
|
+
tally: CarryTally | null;
|
|
869
|
+
memoryBytes: () => number;
|
|
870
|
+
/** 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. */
|
|
871
|
+
check: () => CarryCheck | null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** What a control hands `reduceMeshWith` for issue #1295: a fault, and a callback after every decision the residual took part in. */
|
|
875
|
+
export interface SkinningHooks {
|
|
876
|
+
plant?: SkinningVetoPlant;
|
|
877
|
+
inspect?: (inspection: SkinningInspection) => void;
|
|
806
878
|
}
|
|
807
879
|
|
|
808
880
|
/**
|
|
@@ -826,10 +898,76 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string, final = f
|
|
|
826
898
|
boneOrder: input.boneOrder,
|
|
827
899
|
preset: input.preset,
|
|
828
900
|
...amplitudeOf(run.input, run.plant, final),
|
|
901
|
+
...(final ? skinningOfResult(run) : {}),
|
|
829
902
|
};
|
|
830
903
|
return run.stepRasters === null ? measureMeshQualityWith(measureInput, run.rasters) : measureMeshQualityStep(measureInput, run.stepRasters);
|
|
831
904
|
}
|
|
832
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Issue #1295: what the result's own measurement is handed as `skinning` — the input's envelope and bound against the
|
|
908
|
+
* call's own source, so the report's row is the measurement's, written as `measureMeshQuality` writes it; `null` as
|
|
909
|
+
* `null`; nothing when the field was left out. No other measurement of the call carries it: the steps read the
|
|
910
|
+
* carried state.
|
|
911
|
+
*/
|
|
912
|
+
function skinningOfResult(run: Run): { skinning?: SkinningResidualInput | null } {
|
|
913
|
+
const declared = run.input.targets.skinning;
|
|
914
|
+
if (declared === undefined) return {};
|
|
915
|
+
if (declared === null) return { skinning: null };
|
|
916
|
+
return { skinning: skinningInputOf(run.input) };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function skinningInputOf(input: MeshReductionInput): SkinningResidualInput {
|
|
920
|
+
const declared = input.targets.skinning!;
|
|
921
|
+
return { source: { id: 'source', mesh: input.source }, envelope: declared.envelope, maxResidual: declared.maxResidual, deform: input.deform };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Issue #1295: the residual of the working mesh as it now stands — a carried trial, left pending until `settleSkinning`;
|
|
926
|
+
* under the full-recompute control, the measurement over the canonical candidate; or the admission's reading when it
|
|
927
|
+
* could not measure.
|
|
928
|
+
*/
|
|
929
|
+
function skinningTrial(run: Run, canonical: () => SourceMesh): CarryReading {
|
|
930
|
+
const sk = run.skinning!;
|
|
931
|
+
if (sk.unmeasured !== null) return sk.unmeasured;
|
|
932
|
+
if (sk.carry === null) {
|
|
933
|
+
const reading = skinningResidual({ ...sk.args, candidate: canonical() });
|
|
934
|
+
return reading.state === 'measured' ? { state: 'measured', value: reading.value } : { state: reading.state, reason: reading.reason };
|
|
935
|
+
}
|
|
936
|
+
return sk.carry.trial(run.work.triangles);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** Issue #1295: the residual's reading as the name a refusal carries — `rowName`'s form — or null when it is within the bound. */
|
|
940
|
+
function skinningVeto(sk: SkinningRun, reading: CarryReading): string | null {
|
|
941
|
+
if (reading.state === 'measured') return reading.value <= sk.bound ? null : `MQ_SKINNING_RESIDUAL: ${reading.value} against <= ${sk.bound}`;
|
|
942
|
+
return `MQ_SKINNING_RESIDUAL: ${reading.state} — ${reading.reason}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Issue #1295: keep or undo a pending trial, then let a control read the decision. The working mesh is already the one decided on. */
|
|
946
|
+
function settleSkinning(run: Run, keep: boolean, phase: SkinningInspection['phase'], reading: CarryReading): void {
|
|
947
|
+
const carry = run.skinning?.carry ?? null;
|
|
948
|
+
if (carry !== null && run.skinning!.unmeasured === null) {
|
|
949
|
+
if (keep) carry.commit();
|
|
950
|
+
else carry.rollback();
|
|
951
|
+
}
|
|
952
|
+
const inspect = run.hooks?.inspect;
|
|
953
|
+
if (inspect === undefined) return;
|
|
954
|
+
inspect({
|
|
955
|
+
phase,
|
|
956
|
+
step: run.steps,
|
|
957
|
+
accepted: keep,
|
|
958
|
+
reading,
|
|
959
|
+
tally: carry === null ? null : { ...carry.tally },
|
|
960
|
+
memoryBytes: () => (carry === null ? 0 : carry.memoryBytes()),
|
|
961
|
+
check: () => (carry === null ? null : carry.compareWithFull(canonicalise(run.work, run.sourceTurn, run.boneRank).mesh)),
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** 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. */
|
|
966
|
+
function followSkinning(run: Run, phase: SkinningInspection['phase']): void {
|
|
967
|
+
if (run.skinning === null || run.skinning.carry === null || run.skinning.unmeasured !== null) return;
|
|
968
|
+
settleSkinning(run, true, phase, run.skinning.carry.trial(run.work.triangles));
|
|
969
|
+
}
|
|
970
|
+
|
|
833
971
|
/**
|
|
834
972
|
* Issue #1287: what one measurement of a reduction is handed as `motionAmplitude` — the input's, on the result's own
|
|
835
973
|
* measurement (`final`) only. The admission, every refinement and removal step and the post-pass are measured without
|
|
@@ -1163,6 +1301,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1163
1301
|
if (target.state === 'not-measurable') {
|
|
1164
1302
|
const p: Pt = [r6(region.polygon[0][0]), r6(region.polygon[0][1])];
|
|
1165
1303
|
if (!insertPoint(run, p)) return { kind: 'stuck', constraint: `${rowName(target)} (the refinement found no triangle holding the region's first vertex)` };
|
|
1304
|
+
followSkinning(run, 'insertion');
|
|
1166
1305
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1167
1306
|
continue;
|
|
1168
1307
|
}
|
|
@@ -1178,6 +1317,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1178
1317
|
const q = outerSplit(run.work.pos[inner], run.work.pos[outer], region);
|
|
1179
1318
|
if (q === null) continue;
|
|
1180
1319
|
splitEdge(run, a, b, q);
|
|
1320
|
+
followSkinning(run, 'insertion');
|
|
1181
1321
|
exited = true;
|
|
1182
1322
|
break;
|
|
1183
1323
|
}
|
|
@@ -1207,6 +1347,7 @@ function refineRegions(run: Run): PhaseEnd {
|
|
|
1207
1347
|
const p = splitPoint(run.work.pos[a], run.work.pos[b], region);
|
|
1208
1348
|
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
1349
|
splitEdge(run, a, b, p);
|
|
1350
|
+
followSkinning(run, 'insertion');
|
|
1210
1351
|
if (accept(run, 'insertion', [])) return { kind: 'replayed' };
|
|
1211
1352
|
}
|
|
1212
1353
|
}
|
|
@@ -1448,7 +1589,10 @@ function removeVertices(run: Run): PhaseEnd {
|
|
|
1448
1589
|
if (block === null) {
|
|
1449
1590
|
taken++;
|
|
1450
1591
|
if (accept(run, 'removal', [v])) return { kind: 'replayed' };
|
|
1451
|
-
} else
|
|
1592
|
+
} else {
|
|
1593
|
+
lastBlock = { refusal: block, what: `removing source vertex ${v}` };
|
|
1594
|
+
if (run.hooks?.plant === 'veto-ends-search' && typeof block === 'string' && block.startsWith('MQ_SKINNING_RESIDUAL')) return { kind: 'stuck', constraint: lastName() };
|
|
1595
|
+
}
|
|
1452
1596
|
}
|
|
1453
1597
|
if (taken === 0) {
|
|
1454
1598
|
// A pass that took nothing left the working mesh as it found it, so a refusal the floor decided is measured
|
|
@@ -1526,7 +1670,11 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1526
1670
|
undo();
|
|
1527
1671
|
return `outline: ${err.message}`;
|
|
1528
1672
|
}
|
|
1529
|
-
|
|
1673
|
+
const phase: SkinningInspection['phase'] = vertices.length > 1 ? 'boundary-run' : 'removal';
|
|
1674
|
+
if (vertices.length > 1 && run.plant === 'run-skips-rows') {
|
|
1675
|
+
followSkinning(run, phase);
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1530
1678
|
if (!full && run.plant !== 'measure-every-candidate') {
|
|
1531
1679
|
const margin = run.plant === 'floor-half-a-pixel-short' ? -0.5 : FLOOR_MARGIN;
|
|
1532
1680
|
if (deviationFloor(run, canon.mesh, vertices) > input.targets.maxBoundaryDeviation + margin) {
|
|
@@ -1551,6 +1699,18 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1551
1699
|
undo();
|
|
1552
1700
|
return rowName(blocking);
|
|
1553
1701
|
}
|
|
1702
|
+
// Issue #1295: every required row passes, so the candidate has no reversed or degenerate triangle; now the residual
|
|
1703
|
+
// against the original source. A veto is a refusal like any other: the mesh and the carried state as they were.
|
|
1704
|
+
if (run.skinning !== null) {
|
|
1705
|
+
const reading = skinningTrial(run, () => canon.mesh);
|
|
1706
|
+
const veto = skinningVeto(run.skinning, reading);
|
|
1707
|
+
if (veto !== null) {
|
|
1708
|
+
undo();
|
|
1709
|
+
settleSkinning(run, false, phase, reading);
|
|
1710
|
+
return veto;
|
|
1711
|
+
}
|
|
1712
|
+
settleSkinning(run, true, phase, reading);
|
|
1713
|
+
}
|
|
1554
1714
|
return null;
|
|
1555
1715
|
}
|
|
1556
1716
|
|
|
@@ -1718,12 +1878,29 @@ function retriangulateResult(run: Run, termination: Termination): Retriangulatio
|
|
|
1718
1878
|
if (result.flips === 0) return out(true, null);
|
|
1719
1879
|
const was = run.work.triangles;
|
|
1720
1880
|
run.work.triangles = result.triangles;
|
|
1721
|
-
if (plant === 'flips-against-delaunay-unmeasured')
|
|
1881
|
+
if (plant === 'flips-against-delaunay-unmeasured') {
|
|
1882
|
+
followSkinning(run, 'delaunay');
|
|
1883
|
+
return out(true, null);
|
|
1884
|
+
}
|
|
1722
1885
|
const canon = canonicalise(run.work, run.sourceTurn, run.boneRank);
|
|
1723
1886
|
const blocking = firstBlockingRow(measureAgainstTargets(run, canon.mesh, 'retriangulated'), run.input.targets.artFit);
|
|
1724
|
-
if (blocking
|
|
1725
|
-
|
|
1726
|
-
|
|
1887
|
+
if (blocking !== null) {
|
|
1888
|
+
run.work.triangles = was;
|
|
1889
|
+
return out(false, rowName(blocking));
|
|
1890
|
+
}
|
|
1891
|
+
// Issue #1295: the returned triangulation is held to the residual as every step was — it is not in acceptedAt, and
|
|
1892
|
+
// that is no reason for it to escape the bound. Refused whole, naming the row.
|
|
1893
|
+
if (run.skinning !== null) {
|
|
1894
|
+
const reading = skinningTrial(run, () => canon.mesh);
|
|
1895
|
+
const veto = run.hooks?.plant === 'post-pass-unchecked' ? null : skinningVeto(run.skinning, reading);
|
|
1896
|
+
if (veto !== null) {
|
|
1897
|
+
run.work.triangles = was;
|
|
1898
|
+
settleSkinning(run, false, 'delaunay', reading);
|
|
1899
|
+
return out(false, veto);
|
|
1900
|
+
}
|
|
1901
|
+
settleSkinning(run, true, 'delaunay', reading);
|
|
1902
|
+
}
|
|
1903
|
+
return out(true, null);
|
|
1727
1904
|
}
|
|
1728
1905
|
|
|
1729
1906
|
// ---------------------------------------------------------------------------
|
|
@@ -1783,19 +1960,27 @@ export function reduceMeshWith(
|
|
|
1783
1960
|
steps: StepRasters | null = stepRastersOf(rasters),
|
|
1784
1961
|
plant: ReductionPlant | null = null,
|
|
1785
1962
|
observe: AttemptObserver | null = null,
|
|
1963
|
+
skinning: SkinningHooks | null = null,
|
|
1786
1964
|
): MeshReductionResult {
|
|
1787
1965
|
validateReduction(input, plant);
|
|
1788
1966
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1789
1967
|
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
1968
|
}
|
|
1791
|
-
return reduceValidated(input, rasters, steps, plant, observe);
|
|
1969
|
+
return reduceValidated(input, rasters, steps, plant, observe, skinning);
|
|
1792
1970
|
}
|
|
1793
1971
|
|
|
1794
1972
|
/**
|
|
1795
1973
|
* The operation, over an input `validateReduction` accepted; the art's rasters are computed at most once, in
|
|
1796
1974
|
* `rasters`, and every refinement and removal step is measured through `steps` when it is given (issue #1246).
|
|
1797
1975
|
*/
|
|
1798
|
-
function reduceValidated(
|
|
1976
|
+
function reduceValidated(
|
|
1977
|
+
input: MeshReductionInput,
|
|
1978
|
+
rasters: ArtRasters,
|
|
1979
|
+
steps: StepRasters | null,
|
|
1980
|
+
plant: ReductionPlant | null = null,
|
|
1981
|
+
observe: AttemptObserver | null = null,
|
|
1982
|
+
hooks: SkinningHooks | null = null,
|
|
1983
|
+
): MeshReductionResult {
|
|
1799
1984
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
1800
1985
|
const src = input.source;
|
|
1801
1986
|
const sourceHull: Array<[number, number]> = src.points.slice(0, Math.max(0, src.hull)).map(([x, y]): [number, number] => [x, y]);
|
|
@@ -1819,7 +2004,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1819
2004
|
...amplitudeOf(input, plant, false),
|
|
1820
2005
|
};
|
|
1821
2006
|
const admit = steps === null ? measureMeshQualityWith(admitInput, rasters) : measureMeshQualityStep(admitInput, steps);
|
|
1822
|
-
const effective = effectiveOf(input, admit.effective, sourceHull, plant);
|
|
2007
|
+
const effective = effectiveOf(input, admit.effective, sourceHull, plant, hooks);
|
|
1823
2008
|
const sourceCounts = admit.sourceCounts;
|
|
1824
2009
|
const noMesh = (termination: Termination): MeshReductionResult => ({
|
|
1825
2010
|
mesh: null,
|
|
@@ -1849,6 +2034,7 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1849
2034
|
}
|
|
1850
2035
|
checkIslands(input, who, rasters);
|
|
1851
2036
|
checkDeformBeforeWork(input);
|
|
2037
|
+
const skinningAdmitted = admitSkinning(input, rasters);
|
|
1852
2038
|
|
|
1853
2039
|
const boneRank = new Map((input.boneOrder ?? []).map((b, i) => [b, i]));
|
|
1854
2040
|
const work: Work = {
|
|
@@ -1880,7 +2066,10 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1880
2066
|
sourceEdges,
|
|
1881
2067
|
rasters,
|
|
1882
2068
|
stepRasters: steps,
|
|
2069
|
+
skinning: null,
|
|
2070
|
+
hooks,
|
|
1883
2071
|
};
|
|
2072
|
+
run.skinning = skinningRunOf(run, skinningAdmitted);
|
|
1884
2073
|
|
|
1885
2074
|
// Issue #1268: a replay ends in its own termination, never as an exhausted budget; a stop the run never reaches
|
|
1886
2075
|
// leaves the run's own termination, which then carries `stopAfterAccepted` saying so.
|
|
@@ -1892,10 +2081,13 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1892
2081
|
if (refined.kind === 'replayed') return finish(run, effective, sourceCounts, replayed());
|
|
1893
2082
|
if (refined.kind === 'budget') return noMesh({ reason: 'budget-exhausted', candidatesTried: run.steps, budget: input.budget.maxCandidates, result: 'none-met-the-targets', ...notReached() });
|
|
1894
2083
|
const startCanon = canonicalise(work, run.sourceTurn, boneRank);
|
|
1895
|
-
const
|
|
2084
|
+
const startRow = firstBlockingRow(measureAgainstTargets(run, startCanon.mesh, 'start'), input.targets.artFit);
|
|
2085
|
+
// Issue #1295: the refined source — the refinement's outcome — held to the residual before any removal; with no
|
|
2086
|
+
// region it is the source, which reads 0 against itself.
|
|
2087
|
+
const startBlock = startRow !== null ? rowName(startRow) : run.skinning === null ? null : skinningStart(run, () => startCanon.mesh);
|
|
1896
2088
|
let termination: Termination;
|
|
1897
2089
|
if (startBlock !== null) {
|
|
1898
|
-
const constraint = refined.kind === 'stuck' ? refined.constraint :
|
|
2090
|
+
const constraint = refined.kind === 'stuck' ? refined.constraint : startBlock;
|
|
1899
2091
|
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
2092
|
} else {
|
|
1901
2093
|
const reduced = removeVertices(run);
|
|
@@ -1912,6 +2104,87 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1912
2104
|
}
|
|
1913
2105
|
}
|
|
1914
2106
|
|
|
2107
|
+
/**
|
|
2108
|
+
* Issue #1295: the residual's admission — what the source itself reads under the declared envelope, before any step.
|
|
2109
|
+
* Null when no step reads a residual (the field left out, `null`, or a bound declared absent). A source the
|
|
2110
|
+
* measurement refuses — a deform key that moves vertices outside skinning, a bone the envelope does not declare, an
|
|
2111
|
+
* envelope bone the source does not bind, shares that do not close, a vertex off its UV, one mesh weighted and not the
|
|
2112
|
+
* other — is refused here, `invalid-input` with the measurement's code and words; what it cannot measure (both meshes
|
|
2113
|
+
* unweighted, fewer art samples than the floor) is kept as the reading every step is then blocked by.
|
|
2114
|
+
*/
|
|
2115
|
+
function admitSkinning(input: MeshReductionInput, rasters: ArtRasters): { input: SkinningResidualInput; args: Omit<SkinningMeasureArgs, 'candidate'>; unmeasured: CarryReading | null } | null {
|
|
2116
|
+
const declared = input.targets.skinning;
|
|
2117
|
+
if (declared === undefined || declared === null || declared.maxResidual === null) return null;
|
|
2118
|
+
const skinning = skinningInputOf(input);
|
|
2119
|
+
const args: Omit<SkinningMeasureArgs, 'candidate'> = {
|
|
2120
|
+
attachment: input.attachment,
|
|
2121
|
+
frame: input.art.frame,
|
|
2122
|
+
artBits: rasters.artBits(),
|
|
2123
|
+
maskWidth: input.art.mask.width,
|
|
2124
|
+
maskHeight: input.art.mask.height,
|
|
2125
|
+
minArtSamples: input.minArtSamples,
|
|
2126
|
+
threshold: input.art.threshold,
|
|
2127
|
+
skinning,
|
|
2128
|
+
};
|
|
2129
|
+
const contract = skinningContract({ ...args, candidate: input.source });
|
|
2130
|
+
if (!('state' in contract) || contract.state === 'measured') return { input: skinning, args, unmeasured: null };
|
|
2131
|
+
if (contract.state === 'refused') {
|
|
2132
|
+
const code = contract.reason.slice(0, contract.reason.indexOf(':'));
|
|
2133
|
+
throw new Stop('invalid-input', code, contract.reason.slice(code.length + 2));
|
|
2134
|
+
}
|
|
2135
|
+
return { input: skinning, args, unmeasured: { state: 'not-measurable', reason: contract.reason } };
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/** Issue #1295: the run's residual — the carried state over the working mesh as the reduction starts, or what stands in for it. */
|
|
2139
|
+
function skinningRunOf(run: Run, admitted: ReturnType<typeof admitSkinning>): SkinningRun | null {
|
|
2140
|
+
if (admitted === null) return null;
|
|
2141
|
+
const bound = admitted.input.maxResidual!;
|
|
2142
|
+
const base = { bound, input: admitted.input, args: admitted.args };
|
|
2143
|
+
if (admitted.unmeasured !== null) return { ...base, carry: null, unmeasured: admitted.unmeasured };
|
|
2144
|
+
const plant = run.hooks?.plant;
|
|
2145
|
+
if (plant === 'full-recompute') {
|
|
2146
|
+
const reading = skinningResidual({ ...admitted.args, candidate: run.input.source });
|
|
2147
|
+
if (reading.state === 'refused') {
|
|
2148
|
+
const code = reading.reason.slice(0, reading.reason.indexOf(':'));
|
|
2149
|
+
throw new Stop('invalid-input', code, reading.reason.slice(code.length + 2));
|
|
2150
|
+
}
|
|
2151
|
+
return { ...base, carry: null, unmeasured: null };
|
|
2152
|
+
}
|
|
2153
|
+
const carryPlant: CarryPlant | null = plant === 'stale-carrier' || plant === 'lost-maximum' || plant === 'no-rollback' || plant === 'previous-candidate' ? plant : null;
|
|
2154
|
+
try {
|
|
2155
|
+
const carry = new SkinningCarry(
|
|
2156
|
+
run.who,
|
|
2157
|
+
{ pos: run.work.pos, uv: run.work.uv, weights: run.work.weights! },
|
|
2158
|
+
run.work.nSource,
|
|
2159
|
+
run.work.triangles,
|
|
2160
|
+
run.input.source,
|
|
2161
|
+
admitted.input.envelope,
|
|
2162
|
+
admitted.args.artBits,
|
|
2163
|
+
admitted.args.maskWidth,
|
|
2164
|
+
admitted.args.maskHeight,
|
|
2165
|
+
run.input.art.frame,
|
|
2166
|
+
carryPlant,
|
|
2167
|
+
);
|
|
2168
|
+
return { ...base, carry, unmeasured: null };
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
if (err instanceof MeshReductionError && err.code === 'COMPARE_UV_CARRIER_NOT_UNIQUE') throw new Stop('invalid-input', 'SKINNING_UV_CARRIER_NOT_UNIQUE', err.message);
|
|
2171
|
+
throw err;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** Issue #1295: the residual's verdict on the mesh the removals start from — the refinement's outcome — as a refusal name, or null. */
|
|
2176
|
+
function skinningStart(run: Run, canonical: () => SourceMesh): string | null {
|
|
2177
|
+
const sk = run.skinning!;
|
|
2178
|
+
let reading: CarryReading;
|
|
2179
|
+
if (sk.unmeasured !== null) reading = sk.unmeasured;
|
|
2180
|
+
else if (sk.carry !== null) reading = sk.carry.reading();
|
|
2181
|
+
else {
|
|
2182
|
+
const r = skinningResidual({ ...sk.args, candidate: canonical() });
|
|
2183
|
+
reading = r.state === 'measured' ? { state: 'measured', value: r.value } : { state: r.state, reason: r.reason };
|
|
2184
|
+
}
|
|
2185
|
+
return skinningVeto(sk, reading);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1915
2188
|
/** §4: full coverage asked of a source that touches no pixel of some art island is refused, never met by deleting art. */
|
|
1916
2189
|
function checkIslands(input: MeshReductionInput, who: string, rasters: ArtRasters): void {
|
|
1917
2190
|
if (input.targets.artFit.minCoverage !== 1) return;
|
|
@@ -2015,13 +2288,13 @@ function reportOf(effective: EffectiveSettings, sourceCounts: MeshCounts | null,
|
|
|
2015
2288
|
}
|
|
2016
2289
|
|
|
2017
2290
|
/** 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 {
|
|
2291
|
+
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]>, plant: ReductionPlant | null, hooks: SkinningHooks | null = null): EffectiveSettings {
|
|
2019
2292
|
const amplitude = plant === 'echo-when-unset' && input.motionAmplitude === undefined ? null : input.motionAmplitude;
|
|
2020
2293
|
const fit = (f: ArtFitBounds): ArtFitBounds => ({ minCoverage: f.minCoverage, maxOvershoot: f.maxOvershoot, maxUndercut: f.maxUndercut });
|
|
2021
2294
|
return {
|
|
2022
2295
|
...measured,
|
|
2023
2296
|
sourceBounds: fit(input.sourceBounds),
|
|
2024
|
-
targets: input.targets,
|
|
2297
|
+
targets: hooks?.plant === 'echo-when-unset' && input.targets.skinning === undefined ? { ...input.targets, skinning: null } : input.targets,
|
|
2025
2298
|
referenceHull: sourceHull,
|
|
2026
2299
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
2027
2300
|
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|