rig-c 2.25.0 → 2.27.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.
@@ -70,6 +70,7 @@ import {
70
70
  type MeshOutline,
71
71
  } from './mesh.ts';
72
72
  import { areaBand, triangleAreas } from './areaband.ts';
73
+ import { allocationContrast, boundaryNecessityOnce, deformLoad, gradeMax, minAngleP10, type AllocationArt, type AllocationPlant } from './meshallocation.ts';
73
74
  import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type RegionEdgeReading, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
74
75
  import { cropToSpineY } from './transform.ts';
75
76
 
@@ -251,6 +252,46 @@ export interface MeshReductionInput {
251
252
  * 2 or more, and has no default; `null` is refused like any value that is not that object.
252
253
  */
253
254
  boundaryRuns?: BoundaryRuns;
255
+ /**
256
+ * The triangulation post-pass (issue #1283, §8 Q3/Q9) — opt-in. Left out = the returned mesh carries the triangles
257
+ * the removals left, and the call is the one it was before the field existed. `'delaunay'`: once the reduction has
258
+ * ended (or a replay has stopped), the kept vertices are re-triangulated by Delaunay edge flips — no vertex added,
259
+ * moved or removed; no outline edge, protected edge or edge a region holds is flipped, and no flip makes an edge a
260
+ * region holds or one over `protect.weightJump` — and the pass is taken whole only when every required row still
261
+ * passes on its result, else the mesh is returned as the removals left it and the report names the row
262
+ * (`ReductionChanges.retriangulation`). It is not a step: it tries no candidate and is not counted in the budget.
263
+ * Any other value, `null` included, is refused.
264
+ */
265
+ retriangulate?: 'delaunay';
266
+ /**
267
+ * The order single removals are attempted in (issue #1283, §8 Q11) — opt-in. Left out = ascending source index.
268
+ * `'deformation-load'`: at the start of each pass's single removals, every candidate is ranked by the deformation
269
+ * load its removal would add — the largest L · Δshare over the edges the re-triangulated hole adds, L the edge's
270
+ * length in px and Δshare half the L1 difference of its ends' weight vectors — and attempted in ascending load, ties
271
+ * by source index; a removal that cannot be made ranks last. Boundary runs keep their own order. Reads weights only:
272
+ * on an unweighted source every load is 0 and the order is the default's. Any other value, `null` included, is
273
+ * refused.
274
+ */
275
+ removalOrder?: 'deformation-load';
276
+ }
277
+
278
+ /**
279
+ * Issue #1283: which triangulation a reduced mesh carries, written in `ReductionChanges` only when the input set
280
+ * `retriangulate`.
281
+ */
282
+ export interface Retriangulation {
283
+ /** The input's `retriangulate`. */
284
+ method: 'delaunay';
285
+ /** Whether the returned mesh carries the pass's triangles (true) or the removals' (false, `refusedBy` says why). */
286
+ taken: boolean;
287
+ /** Edge flips the pass made — on the returned mesh when taken, on the one it refused otherwise. */
288
+ flips: number;
289
+ /** Sweeps over the interior edges, the last of them flipping none. */
290
+ sweeps: number;
291
+ /** Null when taken; else the first required row that failed on the pass's result, or the bound the pass reached. */
292
+ refusedBy: string | null;
293
+ /** What the operation does not promise about the accepted steps under the pass — the same sentence on every report. */
294
+ monotonicity: string;
254
295
  }
255
296
 
256
297
  /** Issue #1279: the opt-in to boundary runs — the most consecutive outline vertices one chord may replace. */
@@ -311,6 +352,41 @@ export interface MeshMeasureInput {
311
352
  influences: InfluenceLimits | null;
312
353
  boneOrder: string[] | null;
313
354
  preset: { name: string; version: string } | null;
355
+ /**
356
+ * Issue #1280 (§8, [agreed, rig-parts#126] Q2): the motion amplitude the two weight-aware allocation rows read —
357
+ * `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD`. Left out, it is not declared and those two rows are
358
+ * `not-measurable`, naming this field; `null` declares it absent, with the same result and a reason saying so (as a
359
+ * `null` art bound is declared absent, #1257). It declares no bound: every allocation row stays `undeclared`.
360
+ */
361
+ motionAmplitude?: MotionAmplitude | null;
362
+ }
363
+
364
+ /**
365
+ * Issue #1280: the amplitude a caller's motion moves a mesh's weight field by, per track, as the allocation rows read
366
+ * it. Nothing in it has a default; nothing in it gates anything.
367
+ */
368
+ export interface MotionAmplitude {
369
+ /** One entry per track the caller declares (an animation, a parameter axis — its name is echoed, never read). */
370
+ tracks: TrackAmplitude[];
371
+ /**
372
+ * G, px per px: how fast a declared need relaxes away from its source — the size a need allows at distance d is its
373
+ * own plus G × d (the form of a region's `grade`). Read by `MQ_ALLOCATION_CONTRAST` only. Finite, 0 or more.
374
+ */
375
+ gradation: number;
376
+ }
377
+
378
+ /** One track's amplitude: the pairs of bones a share moves between in it, each with θ, and the track's ε. */
379
+ export interface TrackAmplitude {
380
+ /** The caller's name for the track; echoed, never read. */
381
+ track: string;
382
+ /**
383
+ * Each pair of bones whose relative transform the track moves, and θ: the largest ‖M − I‖ of the relative linear
384
+ * part M over the track — 2 sin(α / 2) for a rotation by α (≈ α in radians for a small one), |s − 1| for a uniform
385
+ * scale by s. Dimensionless, finite, 0 or more. A pair is unordered; declared twice, the larger θ is read.
386
+ */
387
+ pairs: Array<{ bones: [string, string]; theta: number }>;
388
+ /** ε, drawing px: the chord error the track tolerates — what a deformation need is sized against. Finite, above 0. */
389
+ epsilon: number;
314
390
  }
315
391
 
316
392
  // ---------------------------------------------------------------------------
@@ -344,6 +420,18 @@ export interface MeasureRow {
344
420
  sampling?: { domain: string; count: number };
345
421
  /** Motion rows only (stage C, `src/meshcompare.ts`): how the row's value was taken over the schedule. */
346
422
  motion?: MotionRowDetail;
423
+ /** The five allocation rows only (issue #1280): what the value is a reading of, and what it was read from. */
424
+ allocation?: AllocationDetail;
425
+ }
426
+
427
+ /** Issue #1280: an allocation row's own fields. Written on those five rows, measured or not, and on no other. */
428
+ export interface AllocationDetail {
429
+ /** One sentence: what the value is a reading of — and, for `MQ_DEFORM_LOAD`, that it is not predicted motion. */
430
+ reading: string;
431
+ /** `MQ_ALLOCATION_CONTRAST` when measured: economy E, and the counts Δ and E are taken from. */
432
+ contrast?: { economy: number; dense: { vertices: number; removableAlone: number }; rest: { vertices: number; removableAlone: number } };
433
+ /** `MQ_BOUNDARY_NECESSARY` when measured: the shortcuts the search tested, and the bound n × (n − 2) it is held to. */
434
+ search?: { shortcutsTested: number; bound: number };
347
435
  }
348
436
 
349
437
  /** Correction 2: the spatial grid and the value's own granularity are two fields. */
@@ -447,6 +535,8 @@ export interface ReductionChanges {
447
535
  * `stopAfterAccepted: k` replays to the operation at `acceptedAt[k - 1]`.
448
536
  */
449
537
  acceptedAt: AcceptedOperation[];
538
+ /** Issue #1283: which triangulation the mesh carries — present only when the input set `retriangulate`, written last. */
539
+ retriangulation?: Retriangulation;
450
540
  }
451
541
 
452
542
  export interface MeshQualityReport {
@@ -494,6 +584,12 @@ export interface EffectiveSettings {
494
584
  stopAfterAccepted?: number;
495
585
  /** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
496
586
  boundaryRuns?: BoundaryRuns;
587
+ /** A measurement's `motionAmplitude`, echoed when the input set it — `null` included (issue #1280); absent otherwise. */
588
+ motionAmplitude?: MotionAmplitude | null;
589
+ /** A reduction's `retriangulate`, echoed when the input set it (issue #1283); absent otherwise. */
590
+ retriangulate?: 'delaunay';
591
+ /** A reduction's `removalOrder`, echoed when the input set it (issue #1283); absent otherwise — the order was ascending source index. */
592
+ removalOrder?: 'deformation-load';
497
593
  }
498
594
 
499
595
  export interface MeshCounts {
@@ -746,6 +842,33 @@ function validateInput(input: MeshMeasureInput): void {
746
842
  if (input.preset !== null && (!isObject(input.preset) || typeof input.preset.name !== 'string' || typeof input.preset.version !== 'string')) {
747
843
  refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
748
844
  }
845
+ validateAmplitude(who, input.motionAmplitude);
846
+ }
847
+
848
+ /** Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in full. */
849
+ function validateAmplitude(who: string, amplitude: unknown): void {
850
+ if (amplitude === undefined || amplitude === null) return;
851
+ const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
852
+ if (!isObject(amplitude) || !Array.isArray(amplitude.tracks)) refuse('REDUCE_INPUT_MISSING', `${who}: motionAmplitude is ${JSON.stringify(amplitude)}; required ${shape}, null, or the field left out`);
853
+ if (!isFiniteNumber(amplitude.gradation) || amplitude.gradation < 0) {
854
+ refuse('REDUCE_INPUT_MISSING', `${who}: motionAmplitude.gradation is ${JSON.stringify(amplitude.gradation)}; required a finite number of px per px, 0 or more — no gradation is assumed`);
855
+ }
856
+ amplitude.tracks.forEach((track: unknown, i: number) => {
857
+ const at = `motionAmplitude.tracks[${i}]`;
858
+ if (!isObject(track) || typeof track.track !== 'string' || track.track === '' || !Array.isArray(track.pairs)) {
859
+ refuse('REDUCE_INPUT_MISSING', `${who}: ${at} is ${JSON.stringify(track)}; required { track: a non-empty name, pairs: a list, epsilon }`);
860
+ }
861
+ if (!isFiniteNumber(track.epsilon) || track.epsilon <= 0) refuse('REDUCE_INPUT_MISSING', `${who}: ${at}.epsilon is ${JSON.stringify(track.epsilon)}; required a finite number of px above 0`);
862
+ track.pairs.forEach((pair: unknown, j: number) => {
863
+ const here = `${at}.pairs[${j}]`;
864
+ const bones = isObject(pair) ? pair.bones : undefined;
865
+ if (!Array.isArray(bones) || bones.length !== 2 || bones.some((b) => typeof b !== 'string' || b === '') || bones[0] === bones[1]) {
866
+ refuse('REDUCE_INPUT_MISSING', `${who}: ${here}.bones is ${JSON.stringify(bones)}; required two different bone names`);
867
+ }
868
+ const theta = (pair as Record<string, unknown>).theta;
869
+ if (!isFiniteNumber(theta) || theta < 0) refuse('REDUCE_INPUT_MISSING', `${who}: ${here}.theta is ${JSON.stringify(theta)}; required a finite number, 0 or more (‖M − I‖ of the pair's relative linear part)`);
870
+ });
871
+ });
749
872
  }
750
873
 
751
874
  // ---------------------------------------------------------------------------
@@ -1013,6 +1136,24 @@ export function edgeIsHeldByRegion(a: readonly [number, number], b: readonly [nu
1013
1136
  return (last - first) * length > BAND_CONTACT_TOLERANCE;
1014
1137
  }
1015
1138
 
1139
+ /**
1140
+ * The density bound the regions hold the segment `a`–`b` to, as `regionRows` reads it for a mesh edge — the
1141
+ * smallest over every region that holds it (`edgeIsHeldByRegion`): `L0` where it meets the closed polygon, else
1142
+ * `L0 + grade·d` on the `r6` grid — or null when no region holds it. Read by `MQ_ALLOCATION_CONTRAST` (issue #1280)
1143
+ * for the edges a collapse would create. The regions are taken as given: the caller has refused any that
1144
+ * `regionRefusal` refuses. Internal, as `measureMeshQualityWith` is: on `rig-c/mesh` only through `export *`.
1145
+ */
1146
+ export function regionEdgeBound(a: readonly [number, number], b: readonly [number, number], regions: readonly RefinementRegion[]): number | null {
1147
+ let bound: number | null = null;
1148
+ for (const region of regions) {
1149
+ if (!edgeIsHeldByRegion(a, b, region)) continue;
1150
+ const d = segmentToPolygon(a, b, region.polygon);
1151
+ const here = d === 0 ? region.maxEdgeLength : r6(region.maxEdgeLength + region.grade * d);
1152
+ if (bound === null || here < bound) bound = here;
1153
+ }
1154
+ return bound;
1155
+ }
1156
+
1016
1157
  /** Distance from a point to a closed polyline (the polygon's boundary), and the edge that realises it. */
1017
1158
  function pointToBoundary(p: Pt, poly: readonly Pt[]): { d: number; edge: number } {
1018
1159
  let d = Infinity;
@@ -1321,7 +1462,17 @@ function compareRows(a: MeasureRow, b: MeasureRow): number {
1321
1462
  */
1322
1463
  export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1323
1464
  validateInput(input);
1324
- return measureValidated(input, artRastersOf(input.art), null);
1465
+ return measureValidated(input, artRastersOf(input.art), null, null);
1466
+ }
1467
+
1468
+ /**
1469
+ * `measureMeshQuality` with a fault planted in its allocation rows (issue #1280, `AllocationPlant` in
1470
+ * `src/meshallocation.ts`) — the `mesh-quality` suite's negative controls, and with `omit-rows` the report without
1471
+ * those rows that the opt-out identity is held to. Internal, as `measureMeshQualityWith` is.
1472
+ */
1473
+ export function measureMeshQualityPlanted(input: MeshMeasureInput, plant: AllocationPlant): MeshQualityReport {
1474
+ validateInput(input);
1475
+ return measureValidated(input, artRastersOf(input.art), null, plant);
1325
1476
  }
1326
1477
 
1327
1478
  /**
@@ -1340,7 +1491,7 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
1340
1491
  export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
1341
1492
  validateInput(input);
1342
1493
  checkArtRasters(input, rasters);
1343
- return measureValidated(input, rasters, null);
1494
+ return measureValidated(input, rasters, null, null);
1344
1495
  }
1345
1496
 
1346
1497
  /**
@@ -1357,7 +1508,7 @@ export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRast
1357
1508
  export function measureMeshQualityStep(input: MeshMeasureInput, steps: StepRasters): MeshQualityReport {
1358
1509
  validateInput(input);
1359
1510
  checkArtRasters(input, steps.rasters);
1360
- return measureValidated(input, steps.rasters, steps);
1511
+ return measureValidated(input, steps.rasters, steps, null);
1361
1512
  }
1362
1513
 
1363
1514
  /**
@@ -1433,7 +1584,7 @@ function checkArtRasters(input: MeshMeasureInput, rasters: ArtRasters): void {
1433
1584
  }
1434
1585
 
1435
1586
  /** The measurement proper, over an input `validateInput` accepted and rasters taken from its art. */
1436
- function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null): MeshQualityReport {
1587
+ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null): MeshQualityReport {
1437
1588
  rasters.tally.uses++;
1438
1589
  const { attachment, art, source, targets } = input;
1439
1590
  const { mask, threshold, frame } = art;
@@ -1472,6 +1623,9 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
1472
1623
  const points: Pt[] = source.points;
1473
1624
  const hullPolygon: Pt[] = outline.walk.map((v) => points[v]);
1474
1625
 
1626
+ // --- allocation rows (issue #1280): undeclared or not-measurable, never required ------------
1627
+ if (plant !== 'omit-rows') rows.push(...allocationRows(input, outline, rasters, steps, plant));
1628
+
1475
1629
  // --- raster rows ---------------------------------------------------------
1476
1630
  const w = mask.width;
1477
1631
  const h = mask.height;
@@ -1623,6 +1777,131 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
1623
1777
  return report(counts, { id: input.id, counts, geometry, motion: null, accepted }, null);
1624
1778
  }
1625
1779
 
1780
+ /** What each allocation row is a reading of — `AllocationDetail.reading`, one fixed sentence per code. */
1781
+ const ALLOCATION_READINGS: Record<string, string> = {
1782
+ MQ_GRADE:
1783
+ 'the largest |h_u − h_v| / L over the edges, h a vertex\'s mean incident edge length: how abruptly the local size changes, whether or not the density is justified',
1784
+ MQ_MIN_ANGLE_P10: 'the tenth percentile, nearest rank, of the triangles\' smallest angles: sliver fans, where MQ_MIN_ANGLE reads the single worst triangle',
1785
+ MQ_ALLOCATION_CONTRAST:
1786
+ 'Δ: the share of the dense vertices removable alone against the declared needs (silhouette, deformation under motionAmplitude, regions) minus that share among the rest; economy E is the share of all',
1787
+ MQ_DEFORM_LOAD:
1788
+ 'the largest L · Δshare · θ / 4 over the edges, Δshare the share of weight that moves across the edge: a bound on the chord error of the mesh\'s own weight field under motionAmplitude, read to locate the edge; it is not predicted motion and no difference to any reference (docs/MESH_REDUCTION.md §8)',
1789
+ MQ_BOUNDARY_NECESSARY:
1790
+ 'B*: the fewest vertices of the reference hull a closed outline can keep with every skipped run within maxBoundaryDeviation of its chord both ways and no art pixel centre cut off (overshoot is not tested); worst is the first hull vertex of this mesh not on that outline',
1791
+ };
1792
+
1793
+ /**
1794
+ * The five allocation rows (issue #1280; docs/MESH_REDUCTION.md §8): each `undeclared` with its value and worst
1795
+ * sample, or `not-measurable` with the sentence naming what is missing — never required, so none can block a step,
1796
+ * refuse a source, or count towards acceptance. `MQ_BOUNDARY_NECESSARY` is read against the reference hull once per
1797
+ * art rasters object (`boundaryNecessityOnce`), so a reduction computes it once per call.
1798
+ */
1799
+ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null): Built[] {
1800
+ const { attachment, source, targets, art } = input;
1801
+ const who = `attachment ${nameOf(attachment)}`;
1802
+ if (plant === 'mutates-input') {
1803
+ const tris = source.triangles;
1804
+ for (let t = 0; t + 2 < tris.length; t += 3) [tris[t + 1], tris[t + 2]] = [tris[t + 2], tris[t + 1]];
1805
+ }
1806
+ const points: Pt[] = source.points;
1807
+ const out: Built[] = [];
1808
+ const row = (code: string, unit: MeasureRow['unit'], built: Built, detail: Omit<AllocationDetail, 'reading'> = {}): void => {
1809
+ built.row.allocation = { reading: ALLOCATION_READINGS[code], ...detail };
1810
+ if (plant === 'rows-required') built.required = true;
1811
+ out.push(built);
1812
+ };
1813
+ const spec = (code: string, unit: MeasureRow['unit']): RowSpec => ({ code, unit, region: null, attachment });
1814
+ const measured = (code: string, unit: MeasureRow['unit'], value: number, worst: WorstSample): Built => measuredRow(spec(code, unit), value, null, worst, false);
1815
+ const missing = (code: string, unit: MeasureRow['unit'], reason: string): Built => unmeasuredRow(spec(code, unit), 'not-measurable', reason, false);
1816
+
1817
+ // Geometry alone.
1818
+ const grade = gradeMax(points, source.triangles, plant);
1819
+ row('MQ_GRADE', 'ratio', grade.measured ? measured('MQ_GRADE', 'ratio', grade.value.value, { at: { edge: grade.value.edge } }) : missing('MQ_GRADE', 'ratio', `${who}: ${grade.reason}`));
1820
+ const p10 = minAngleP10(points, source.triangles, plant);
1821
+ row(
1822
+ 'MQ_MIN_ANGLE_P10',
1823
+ 'degrees',
1824
+ p10.measured ? measured('MQ_MIN_ANGLE_P10', 'degrees', p10.value.value, { at: { triangle: p10.value.triangle } }) : missing('MQ_MIN_ANGLE_P10', 'degrees', `${who}: ${p10.reason}`),
1825
+ );
1826
+
1827
+ // What the weight-aware and art-reading rows need.
1828
+ const amplitude = input.motionAmplitude;
1829
+ const amplitudeMissing =
1830
+ amplitude === undefined
1831
+ ? `${who}: motionAmplitude is not declared (the field is left out), so no motion amplitude sizes a deformation need; required { tracks, gradation } — no amplitude is assumed, and without one no reading of justification separates anything (§8)`
1832
+ : amplitude === null
1833
+ ? `${who}: motionAmplitude is declared absent (null), so no motion amplitude sizes a deformation need; required { tracks, gradation } for this row — no amplitude is assumed`
1834
+ : null;
1835
+ const unweighted = source.weights === null ? `${who}: source.weights is null — an unweighted mesh has no weight field for a motion to move` : null;
1836
+ const delta = targets.maxBoundaryDeviation;
1837
+ const noDelta = delta === null ? `${who}: targets.maxBoundaryDeviation is null, so no skip of the outline has a deviation to be held to` : null;
1838
+ const artCount = rasters.artCount();
1839
+ const fewArt =
1840
+ artCount < input.minArtSamples
1841
+ ? `${who} has ${artCount} art sample(s) at alpha >= ${art.threshold}; required at least ${input.minArtSamples} (minArtSamples, P9) — a row over fewer is not a measurement`
1842
+ : null;
1843
+ const allocationArt: AllocationArt = { mask: art.mask, bits: rasters.artBits(), scale: art.frame.pageScale };
1844
+
1845
+ // MQ_DEFORM_LOAD.
1846
+ const loadWhy = amplitudeMissing ?? unweighted;
1847
+ if (loadWhy !== null || amplitude === undefined || amplitude === null || source.weights === null) {
1848
+ row('MQ_DEFORM_LOAD', 'px', missing('MQ_DEFORM_LOAD', 'px', loadWhy ?? ''));
1849
+ } else {
1850
+ const load = deformLoad(points, source.triangles, source.weights, amplitude, plant);
1851
+ row(
1852
+ 'MQ_DEFORM_LOAD',
1853
+ 'px',
1854
+ load.measured
1855
+ ? measured('MQ_DEFORM_LOAD', 'px', load.value.value, load.value.edge === null ? NOTHING_WORSE : { at: { edge: load.value.edge } })
1856
+ : missing('MQ_DEFORM_LOAD', 'px', `${who}: ${load.reason}`),
1857
+ );
1858
+ }
1859
+
1860
+ // MQ_ALLOCATION_CONTRAST.
1861
+ const planted = plant === 'contrast-without-amplitude' && (amplitude === undefined || amplitude === null) ? { tracks: [], gradation: 0 } : null;
1862
+ const declared = planted ?? amplitude;
1863
+ let regionWhy: string | null = null;
1864
+ for (const region of targets.regions) {
1865
+ const refusal = regionRefusal(region, art.frame.pageScale, who, steps);
1866
+ if (refusal !== null) {
1867
+ regionWhy = `${who}: region "${region.name}" is refused (${refusal.code}), so its density cannot be read as a declared need`;
1868
+ break;
1869
+ }
1870
+ }
1871
+ const contrastWhy = (planted === null ? amplitudeMissing : null) ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
1872
+ if (contrastWhy !== null || declared === undefined || declared === null || source.weights === null || delta === null) {
1873
+ row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', contrastWhy ?? ''));
1874
+ } else {
1875
+ const ownHull = points.slice(0, outline.hull);
1876
+ const silhouette = boundaryNecessityOnce(rasters, ownHull, delta, allocationArt, null).path;
1877
+ const contrast = allocationContrast(points, source.triangles, outline.hull, source.weights, delta, allocationArt, targets.regions, declared, silhouette, plant);
1878
+ if (contrast.measured) {
1879
+ const c = contrast.value;
1880
+ row('MQ_ALLOCATION_CONTRAST', 'fraction', measured('MQ_ALLOCATION_CONTRAST', 'fraction', c.contrast, { at: { vertex: c.worstVertex } }), {
1881
+ contrast: { economy: c.economy, dense: c.dense, rest: c.rest },
1882
+ });
1883
+ } else {
1884
+ row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', `${who}: ${contrast.reason}`));
1885
+ }
1886
+ }
1887
+
1888
+ // MQ_BOUNDARY_NECESSARY.
1889
+ const reference = input.referenceHull;
1890
+ const noReference = reference === null ? `${who}: no referenceHull was given, so there is no source hull to find the necessary outline of` : null;
1891
+ const necessaryWhy = noReference ?? noDelta ?? fewArt;
1892
+ if (necessaryWhy !== null || reference === null || delta === null) {
1893
+ row('MQ_BOUNDARY_NECESSARY', 'count', missing('MQ_BOUNDARY_NECESSARY', 'count', necessaryWhy ?? ''));
1894
+ } else {
1895
+ const found = boundaryNecessityOnce(rasters, reference, delta, allocationArt, plant);
1896
+ const kept = new Set(found.path.map((i) => pointKey(reference[i])));
1897
+ const beyond = outline.walk.find((v) => !kept.has(pointKey(points[v])));
1898
+ row('MQ_BOUNDARY_NECESSARY', 'count', measured('MQ_BOUNDARY_NECESSARY', 'count', found.count, beyond === undefined ? NOTHING_WORSE : { at: { vertex: beyond } }), {
1899
+ search: { shortcutsTested: found.tests, bound: found.bound },
1900
+ });
1901
+ }
1902
+ return out;
1903
+ }
1904
+
1626
1905
  /** What one source states about itself: hull and interior from the outline, bindings from the weights. */
1627
1906
  function countsOf(source: SourceMesh, outline: MeshOutline): MeshCounts {
1628
1907
  const bindings = source.weights === null ? 0 : source.weights.reduce((s, v) => s + v.length, 0);
@@ -1934,6 +2213,7 @@ function effectiveOf(input: MeshMeasureInput): EffectiveSettings {
1934
2213
  boneOrder: input.boneOrder,
1935
2214
  schedule: null,
1936
2215
  budget: null,
2216
+ ...(input.motionAmplitude === undefined ? {} : { motionAmplitude: input.motionAmplitude }),
1937
2217
  };
1938
2218
  }
1939
2219
 
@@ -2024,6 +2304,9 @@ function effectiveJson(e: EffectiveSettings): Json {
2024
2304
  budget: e.budget === null ? null : { maxCandidates: e.budget.maxCandidates },
2025
2305
  ...(e.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: e.stopAfterAccepted }),
2026
2306
  ...(e.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: e.boundaryRuns.maxVertices } }),
2307
+ ...(e.motionAmplitude === undefined ? {} : { motionAmplitude: amplitudeJson(e.motionAmplitude) }),
2308
+ ...(e.retriangulate === undefined ? {} : { retriangulate: e.retriangulate }),
2309
+ ...(e.removalOrder === undefined ? {} : { removalOrder: e.removalOrder }),
2027
2310
  };
2028
2311
  }
2029
2312
 
@@ -2067,9 +2350,32 @@ function rowJson(r: MeasureRow): Json {
2067
2350
  }
2068
2351
  if (r.sampling !== undefined) out.sampling = { domain: r.sampling.domain, count: r.sampling.count };
2069
2352
  if (r.motion !== undefined) out.motion = motionDetailJson(r.motion);
2353
+ if (r.allocation !== undefined) out.allocation = allocationDetailJson(r.allocation);
2070
2354
  return out;
2071
2355
  }
2072
2356
 
2357
+ function allocationDetailJson(a: AllocationDetail): Json {
2358
+ const out: { [key: string]: Json } = { reading: a.reading };
2359
+ if (a.contrast !== undefined) {
2360
+ const c = a.contrast;
2361
+ out.contrast = {
2362
+ economy: c.economy,
2363
+ dense: { vertices: c.dense.vertices, removableAlone: c.dense.removableAlone },
2364
+ rest: { vertices: c.rest.vertices, removableAlone: c.rest.removableAlone },
2365
+ };
2366
+ }
2367
+ if (a.search !== undefined) out.search = { shortcutsTested: a.search.shortcutsTested, bound: a.search.bound };
2368
+ return out;
2369
+ }
2370
+
2371
+ function amplitudeJson(a: MotionAmplitude | null): Json {
2372
+ if (a === null) return null;
2373
+ return {
2374
+ tracks: a.tracks.map((t) => ({ track: t.track, pairs: t.pairs.map((p) => ({ bones: [p.bones[0], p.bones[1]], theta: p.theta })), epsilon: t.epsilon })),
2375
+ gradation: a.gradation,
2376
+ };
2377
+ }
2378
+
2073
2379
  function sectionJson(s: EvidenceSection): { [key: string]: Json } {
2074
2380
  const m = s.summary;
2075
2381
  return {
@@ -2102,6 +2408,18 @@ function candidateJson(c: CandidateReport): Json {
2102
2408
  deformReevaluated: k.deformReevaluated.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key })),
2103
2409
  linkedMeshes: k.linkedMeshes.map(attachmentJson),
2104
2410
  acceptedAt: k.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
2411
+ ...(k.retriangulation === undefined
2412
+ ? {}
2413
+ : {
2414
+ retriangulation: {
2415
+ method: k.retriangulation.method,
2416
+ taken: k.retriangulation.taken,
2417
+ flips: k.retriangulation.flips,
2418
+ sweeps: k.retriangulation.sweeps,
2419
+ refusedBy: k.retriangulation.refusedBy,
2420
+ monotonicity: k.retriangulation.monotonicity,
2421
+ },
2422
+ }),
2105
2423
  };
2106
2424
  }
2107
2425
  return out;