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.
- package/package.json +1 -1
- package/src/meshallocation.ts +693 -0
- package/src/meshquality.ts +322 -4
- package/src/meshreduce.ts +273 -4
package/src/meshquality.ts
CHANGED
|
@@ -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;
|