rig-c 2.29.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/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 lastBlock = { refusal: block, what: `removing source vertex ${v}` };
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
- if (vertices.length > 1 && run.plant === 'run-skips-rows') return null;
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') return out(true, null);
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 === null) return out(true, null);
1725
- run.work.triangles = was;
1726
- return out(false, rowName(blocking));
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(input: MeshReductionInput, rasters: ArtRasters, steps: StepRasters | null, plant: ReductionPlant | null = null, observe: AttemptObserver | null = null): MeshReductionResult {
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 startBlock = firstBlockingRow(measureAgainstTargets(run, startCanon.mesh, 'start'), input.targets.artFit);
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 : rowName(startBlock);
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 }),