spine-rigc 2.24.1 → 2.26.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 +306 -14
- package/src/meshreduce.ts +227 -24
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
|
|
|
@@ -236,13 +237,42 @@ export interface MeshReductionInput {
|
|
|
236
237
|
/** Every linked mesh of the source (they inherit the new topology); empty when none. */
|
|
237
238
|
linkedMeshes: AttachmentRef[];
|
|
238
239
|
/**
|
|
239
|
-
* Replay (issue #1268, §7 mechanism 2): stop after the n-th accepted
|
|
240
|
-
* taken, counted as `ReductionChanges.acceptedAt` counts them — and return
|
|
241
|
-
* had
|
|
242
|
-
* number, 0 or more: 0 takes no step. A number the run never reaches leaves
|
|
243
|
-
* that termination says so (`stopAfterAccepted` on it).
|
|
240
|
+
* Replay (issue #1268, §7 mechanism 2): stop after the n-th accepted operation — a refinement insertion, a removal
|
|
241
|
+
* or a boundary run taken, one each, counted as `ReductionChanges.acceptedAt` counts them (issue #1279) — and return
|
|
242
|
+
* the mesh the same call without this field had after it, byte for byte, terminated `replayed-to-accepted-step`.
|
|
243
|
+
* Left out = today's behaviour. A whole number, 0 or more: 0 takes no step. A number the run never reaches leaves
|
|
244
|
+
* the run to end as it would have, and that termination says so (`stopAfterAccepted` on it).
|
|
244
245
|
*/
|
|
245
246
|
stopAfterAccepted?: number;
|
|
247
|
+
/**
|
|
248
|
+
* Boundary runs taken as steps (issue #1279, §8 option (iv)) — opt-in. Left out = no run is tried, and the call is
|
|
249
|
+
* the one it was before the field existed. Set, each removal pass first tries to replace a run of 2 to
|
|
250
|
+
* `maxVertices` consecutive surviving source-hull vertices of the outline with one chord, as one step held to every
|
|
251
|
+
* required row exactly as a single removal is, before that pass's single removals. `maxVertices` is a whole number,
|
|
252
|
+
* 2 or more, and has no default; `null` is refused like any value that is not that object.
|
|
253
|
+
*/
|
|
254
|
+
boundaryRuns?: BoundaryRuns;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Issue #1279: the opt-in to boundary runs — the most consecutive outline vertices one chord may replace. */
|
|
258
|
+
export interface BoundaryRuns {
|
|
259
|
+
/** A whole number, 2 or more: the longest run tried. */
|
|
260
|
+
maxVertices: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Issue #1279: one accepted operation of a reduction, in the order taken — the entry `ReductionChanges.acceptedAt`
|
|
265
|
+
* carries for it and the unit `stopAfterAccepted` counts.
|
|
266
|
+
*/
|
|
267
|
+
export interface AcceptedOperation {
|
|
268
|
+
/** The attempt number — `candidatesTried` as it stood when the operation was taken, 1-based. */
|
|
269
|
+
step: number;
|
|
270
|
+
/** A refinement insertion, one source vertex removed, or a boundary run replaced by one chord. */
|
|
271
|
+
kind: 'insertion' | 'removal' | 'boundary-run';
|
|
272
|
+
/** Vertices the operation inserted (an insertion: 1) or removed (a removal: 1; a boundary run: 2 or more). */
|
|
273
|
+
count: number;
|
|
274
|
+
/** The source indices it removed, in outline order for a run — each `null` in `indexMap`; empty for an insertion. */
|
|
275
|
+
sourceVertices: number[];
|
|
246
276
|
}
|
|
247
277
|
|
|
248
278
|
/**
|
|
@@ -282,6 +312,41 @@ export interface MeshMeasureInput {
|
|
|
282
312
|
influences: InfluenceLimits | null;
|
|
283
313
|
boneOrder: string[] | null;
|
|
284
314
|
preset: { name: string; version: string } | null;
|
|
315
|
+
/**
|
|
316
|
+
* Issue #1280 (§8, [agreed, rig-parts#126] Q2): the motion amplitude the two weight-aware allocation rows read —
|
|
317
|
+
* `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD`. Left out, it is not declared and those two rows are
|
|
318
|
+
* `not-measurable`, naming this field; `null` declares it absent, with the same result and a reason saying so (as a
|
|
319
|
+
* `null` art bound is declared absent, #1257). It declares no bound: every allocation row stays `undeclared`.
|
|
320
|
+
*/
|
|
321
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Issue #1280: the amplitude a caller's motion moves a mesh's weight field by, per track, as the allocation rows read
|
|
326
|
+
* it. Nothing in it has a default; nothing in it gates anything.
|
|
327
|
+
*/
|
|
328
|
+
export interface MotionAmplitude {
|
|
329
|
+
/** One entry per track the caller declares (an animation, a parameter axis — its name is echoed, never read). */
|
|
330
|
+
tracks: TrackAmplitude[];
|
|
331
|
+
/**
|
|
332
|
+
* G, px per px: how fast a declared need relaxes away from its source — the size a need allows at distance d is its
|
|
333
|
+
* own plus G × d (the form of a region's `grade`). Read by `MQ_ALLOCATION_CONTRAST` only. Finite, 0 or more.
|
|
334
|
+
*/
|
|
335
|
+
gradation: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** One track's amplitude: the pairs of bones a share moves between in it, each with θ, and the track's ε. */
|
|
339
|
+
export interface TrackAmplitude {
|
|
340
|
+
/** The caller's name for the track; echoed, never read. */
|
|
341
|
+
track: string;
|
|
342
|
+
/**
|
|
343
|
+
* Each pair of bones whose relative transform the track moves, and θ: the largest ‖M − I‖ of the relative linear
|
|
344
|
+
* part M over the track — 2 sin(α / 2) for a rotation by α (≈ α in radians for a small one), |s − 1| for a uniform
|
|
345
|
+
* scale by s. Dimensionless, finite, 0 or more. A pair is unordered; declared twice, the larger θ is read.
|
|
346
|
+
*/
|
|
347
|
+
pairs: Array<{ bones: [string, string]; theta: number }>;
|
|
348
|
+
/** ε, drawing px: the chord error the track tolerates — what a deformation need is sized against. Finite, above 0. */
|
|
349
|
+
epsilon: number;
|
|
285
350
|
}
|
|
286
351
|
|
|
287
352
|
// ---------------------------------------------------------------------------
|
|
@@ -315,6 +380,18 @@ export interface MeasureRow {
|
|
|
315
380
|
sampling?: { domain: string; count: number };
|
|
316
381
|
/** Motion rows only (stage C, `src/meshcompare.ts`): how the row's value was taken over the schedule. */
|
|
317
382
|
motion?: MotionRowDetail;
|
|
383
|
+
/** The five allocation rows only (issue #1280): what the value is a reading of, and what it was read from. */
|
|
384
|
+
allocation?: AllocationDetail;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Issue #1280: an allocation row's own fields. Written on those five rows, measured or not, and on no other. */
|
|
388
|
+
export interface AllocationDetail {
|
|
389
|
+
/** One sentence: what the value is a reading of — and, for `MQ_DEFORM_LOAD`, that it is not predicted motion. */
|
|
390
|
+
reading: string;
|
|
391
|
+
/** `MQ_ALLOCATION_CONTRAST` when measured: economy E, and the counts Δ and E are taken from. */
|
|
392
|
+
contrast?: { economy: number; dense: { vertices: number; removableAlone: number }; rest: { vertices: number; removableAlone: number } };
|
|
393
|
+
/** `MQ_BOUNDARY_NECESSARY` when measured: the shortcuts the search tested, and the bound n × (n − 2) it is held to. */
|
|
394
|
+
search?: { shortcutsTested: number; bound: number };
|
|
318
395
|
}
|
|
319
396
|
|
|
320
397
|
/** Correction 2: the spatial grid and the value's own granularity are two fields. */
|
|
@@ -411,11 +488,13 @@ export interface ReductionChanges {
|
|
|
411
488
|
/** Every linked mesh of the source: each inherits the new topology. */
|
|
412
489
|
linkedMeshes: AttachmentRef[];
|
|
413
490
|
/**
|
|
414
|
-
* Issue #1268
|
|
415
|
-
*
|
|
416
|
-
*
|
|
491
|
+
* Issue #1268, one entry per accepted operation since issue #1279: each refinement insertion, each removal and each
|
|
492
|
+
* boundary run taken, in order, with the attempt it was taken at (`step`, strictly ascending), its kind, how many
|
|
493
|
+
* vertices it inserted or removed and which source vertices it removed. The counts sum to
|
|
494
|
+
* `insertedVertices + removedVertices`; with no boundary run every count is 1 and the length is that sum.
|
|
495
|
+
* `stopAfterAccepted: k` replays to the operation at `acceptedAt[k - 1]`.
|
|
417
496
|
*/
|
|
418
|
-
acceptedAt:
|
|
497
|
+
acceptedAt: AcceptedOperation[];
|
|
419
498
|
}
|
|
420
499
|
|
|
421
500
|
export interface MeshQualityReport {
|
|
@@ -461,6 +540,10 @@ export interface EffectiveSettings {
|
|
|
461
540
|
budget: { maxCandidates: number } | null;
|
|
462
541
|
/** A reduction's `stopAfterAccepted`, echoed when the input set it (issue #1268); absent otherwise. */
|
|
463
542
|
stopAfterAccepted?: number;
|
|
543
|
+
/** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
|
|
544
|
+
boundaryRuns?: BoundaryRuns;
|
|
545
|
+
/** A measurement's `motionAmplitude`, echoed when the input set it — `null` included (issue #1280); absent otherwise. */
|
|
546
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
464
547
|
}
|
|
465
548
|
|
|
466
549
|
export interface MeshCounts {
|
|
@@ -713,6 +796,33 @@ function validateInput(input: MeshMeasureInput): void {
|
|
|
713
796
|
if (input.preset !== null && (!isObject(input.preset) || typeof input.preset.name !== 'string' || typeof input.preset.version !== 'string')) {
|
|
714
797
|
refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
|
|
715
798
|
}
|
|
799
|
+
validateAmplitude(who, input.motionAmplitude);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in full. */
|
|
803
|
+
function validateAmplitude(who: string, amplitude: unknown): void {
|
|
804
|
+
if (amplitude === undefined || amplitude === null) return;
|
|
805
|
+
const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
|
|
806
|
+
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`);
|
|
807
|
+
if (!isFiniteNumber(amplitude.gradation) || amplitude.gradation < 0) {
|
|
808
|
+
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`);
|
|
809
|
+
}
|
|
810
|
+
amplitude.tracks.forEach((track: unknown, i: number) => {
|
|
811
|
+
const at = `motionAmplitude.tracks[${i}]`;
|
|
812
|
+
if (!isObject(track) || typeof track.track !== 'string' || track.track === '' || !Array.isArray(track.pairs)) {
|
|
813
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: ${at} is ${JSON.stringify(track)}; required { track: a non-empty name, pairs: a list, epsilon }`);
|
|
814
|
+
}
|
|
815
|
+
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`);
|
|
816
|
+
track.pairs.forEach((pair: unknown, j: number) => {
|
|
817
|
+
const here = `${at}.pairs[${j}]`;
|
|
818
|
+
const bones = isObject(pair) ? pair.bones : undefined;
|
|
819
|
+
if (!Array.isArray(bones) || bones.length !== 2 || bones.some((b) => typeof b !== 'string' || b === '') || bones[0] === bones[1]) {
|
|
820
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: ${here}.bones is ${JSON.stringify(bones)}; required two different bone names`);
|
|
821
|
+
}
|
|
822
|
+
const theta = (pair as Record<string, unknown>).theta;
|
|
823
|
+
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)`);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
716
826
|
}
|
|
717
827
|
|
|
718
828
|
// ---------------------------------------------------------------------------
|
|
@@ -980,6 +1090,24 @@ export function edgeIsHeldByRegion(a: readonly [number, number], b: readonly [nu
|
|
|
980
1090
|
return (last - first) * length > BAND_CONTACT_TOLERANCE;
|
|
981
1091
|
}
|
|
982
1092
|
|
|
1093
|
+
/**
|
|
1094
|
+
* The density bound the regions hold the segment `a`–`b` to, as `regionRows` reads it for a mesh edge — the
|
|
1095
|
+
* smallest over every region that holds it (`edgeIsHeldByRegion`): `L0` where it meets the closed polygon, else
|
|
1096
|
+
* `L0 + grade·d` on the `r6` grid — or null when no region holds it. Read by `MQ_ALLOCATION_CONTRAST` (issue #1280)
|
|
1097
|
+
* for the edges a collapse would create. The regions are taken as given: the caller has refused any that
|
|
1098
|
+
* `regionRefusal` refuses. Internal, as `measureMeshQualityWith` is: on `rig-c/mesh` only through `export *`.
|
|
1099
|
+
*/
|
|
1100
|
+
export function regionEdgeBound(a: readonly [number, number], b: readonly [number, number], regions: readonly RefinementRegion[]): number | null {
|
|
1101
|
+
let bound: number | null = null;
|
|
1102
|
+
for (const region of regions) {
|
|
1103
|
+
if (!edgeIsHeldByRegion(a, b, region)) continue;
|
|
1104
|
+
const d = segmentToPolygon(a, b, region.polygon);
|
|
1105
|
+
const here = d === 0 ? region.maxEdgeLength : r6(region.maxEdgeLength + region.grade * d);
|
|
1106
|
+
if (bound === null || here < bound) bound = here;
|
|
1107
|
+
}
|
|
1108
|
+
return bound;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
983
1111
|
/** Distance from a point to a closed polyline (the polygon's boundary), and the edge that realises it. */
|
|
984
1112
|
function pointToBoundary(p: Pt, poly: readonly Pt[]): { d: number; edge: number } {
|
|
985
1113
|
let d = Infinity;
|
|
@@ -1288,7 +1416,17 @@ function compareRows(a: MeasureRow, b: MeasureRow): number {
|
|
|
1288
1416
|
*/
|
|
1289
1417
|
export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
|
|
1290
1418
|
validateInput(input);
|
|
1291
|
-
return measureValidated(input, artRastersOf(input.art), null);
|
|
1419
|
+
return measureValidated(input, artRastersOf(input.art), null, null);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* `measureMeshQuality` with a fault planted in its allocation rows (issue #1280, `AllocationPlant` in
|
|
1424
|
+
* `src/meshallocation.ts`) — the `mesh-quality` suite's negative controls, and with `omit-rows` the report without
|
|
1425
|
+
* those rows that the opt-out identity is held to. Internal, as `measureMeshQualityWith` is.
|
|
1426
|
+
*/
|
|
1427
|
+
export function measureMeshQualityPlanted(input: MeshMeasureInput, plant: AllocationPlant): MeshQualityReport {
|
|
1428
|
+
validateInput(input);
|
|
1429
|
+
return measureValidated(input, artRastersOf(input.art), null, plant);
|
|
1292
1430
|
}
|
|
1293
1431
|
|
|
1294
1432
|
/**
|
|
@@ -1307,7 +1445,7 @@ export function measureMeshQuality(input: MeshMeasureInput): MeshQualityReport {
|
|
|
1307
1445
|
export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRasters): MeshQualityReport {
|
|
1308
1446
|
validateInput(input);
|
|
1309
1447
|
checkArtRasters(input, rasters);
|
|
1310
|
-
return measureValidated(input, rasters, null);
|
|
1448
|
+
return measureValidated(input, rasters, null, null);
|
|
1311
1449
|
}
|
|
1312
1450
|
|
|
1313
1451
|
/**
|
|
@@ -1324,7 +1462,7 @@ export function measureMeshQualityWith(input: MeshMeasureInput, rasters: ArtRast
|
|
|
1324
1462
|
export function measureMeshQualityStep(input: MeshMeasureInput, steps: StepRasters): MeshQualityReport {
|
|
1325
1463
|
validateInput(input);
|
|
1326
1464
|
checkArtRasters(input, steps.rasters);
|
|
1327
|
-
return measureValidated(input, steps.rasters, steps);
|
|
1465
|
+
return measureValidated(input, steps.rasters, steps, null);
|
|
1328
1466
|
}
|
|
1329
1467
|
|
|
1330
1468
|
/**
|
|
@@ -1400,7 +1538,7 @@ function checkArtRasters(input: MeshMeasureInput, rasters: ArtRasters): void {
|
|
|
1400
1538
|
}
|
|
1401
1539
|
|
|
1402
1540
|
/** The measurement proper, over an input `validateInput` accepted and rasters taken from its art. */
|
|
1403
|
-
function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null): MeshQualityReport {
|
|
1541
|
+
function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null): MeshQualityReport {
|
|
1404
1542
|
rasters.tally.uses++;
|
|
1405
1543
|
const { attachment, art, source, targets } = input;
|
|
1406
1544
|
const { mask, threshold, frame } = art;
|
|
@@ -1439,6 +1577,9 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
|
|
|
1439
1577
|
const points: Pt[] = source.points;
|
|
1440
1578
|
const hullPolygon: Pt[] = outline.walk.map((v) => points[v]);
|
|
1441
1579
|
|
|
1580
|
+
// --- allocation rows (issue #1280): undeclared or not-measurable, never required ------------
|
|
1581
|
+
if (plant !== 'omit-rows') rows.push(...allocationRows(input, outline, rasters, steps, plant));
|
|
1582
|
+
|
|
1442
1583
|
// --- raster rows ---------------------------------------------------------
|
|
1443
1584
|
const w = mask.width;
|
|
1444
1585
|
const h = mask.height;
|
|
@@ -1590,6 +1731,131 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
|
|
|
1590
1731
|
return report(counts, { id: input.id, counts, geometry, motion: null, accepted }, null);
|
|
1591
1732
|
}
|
|
1592
1733
|
|
|
1734
|
+
/** What each allocation row is a reading of — `AllocationDetail.reading`, one fixed sentence per code. */
|
|
1735
|
+
const ALLOCATION_READINGS: Record<string, string> = {
|
|
1736
|
+
MQ_GRADE:
|
|
1737
|
+
'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',
|
|
1738
|
+
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',
|
|
1739
|
+
MQ_ALLOCATION_CONTRAST:
|
|
1740
|
+
'Δ: 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',
|
|
1741
|
+
MQ_DEFORM_LOAD:
|
|
1742
|
+
'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)',
|
|
1743
|
+
MQ_BOUNDARY_NECESSARY:
|
|
1744
|
+
'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',
|
|
1745
|
+
};
|
|
1746
|
+
|
|
1747
|
+
/**
|
|
1748
|
+
* The five allocation rows (issue #1280; docs/MESH_REDUCTION.md §8): each `undeclared` with its value and worst
|
|
1749
|
+
* sample, or `not-measurable` with the sentence naming what is missing — never required, so none can block a step,
|
|
1750
|
+
* refuse a source, or count towards acceptance. `MQ_BOUNDARY_NECESSARY` is read against the reference hull once per
|
|
1751
|
+
* art rasters object (`boundaryNecessityOnce`), so a reduction computes it once per call.
|
|
1752
|
+
*/
|
|
1753
|
+
function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null): Built[] {
|
|
1754
|
+
const { attachment, source, targets, art } = input;
|
|
1755
|
+
const who = `attachment ${nameOf(attachment)}`;
|
|
1756
|
+
if (plant === 'mutates-input') {
|
|
1757
|
+
const tris = source.triangles;
|
|
1758
|
+
for (let t = 0; t + 2 < tris.length; t += 3) [tris[t + 1], tris[t + 2]] = [tris[t + 2], tris[t + 1]];
|
|
1759
|
+
}
|
|
1760
|
+
const points: Pt[] = source.points;
|
|
1761
|
+
const out: Built[] = [];
|
|
1762
|
+
const row = (code: string, unit: MeasureRow['unit'], built: Built, detail: Omit<AllocationDetail, 'reading'> = {}): void => {
|
|
1763
|
+
built.row.allocation = { reading: ALLOCATION_READINGS[code], ...detail };
|
|
1764
|
+
if (plant === 'rows-required') built.required = true;
|
|
1765
|
+
out.push(built);
|
|
1766
|
+
};
|
|
1767
|
+
const spec = (code: string, unit: MeasureRow['unit']): RowSpec => ({ code, unit, region: null, attachment });
|
|
1768
|
+
const measured = (code: string, unit: MeasureRow['unit'], value: number, worst: WorstSample): Built => measuredRow(spec(code, unit), value, null, worst, false);
|
|
1769
|
+
const missing = (code: string, unit: MeasureRow['unit'], reason: string): Built => unmeasuredRow(spec(code, unit), 'not-measurable', reason, false);
|
|
1770
|
+
|
|
1771
|
+
// Geometry alone.
|
|
1772
|
+
const grade = gradeMax(points, source.triangles, plant);
|
|
1773
|
+
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}`));
|
|
1774
|
+
const p10 = minAngleP10(points, source.triangles, plant);
|
|
1775
|
+
row(
|
|
1776
|
+
'MQ_MIN_ANGLE_P10',
|
|
1777
|
+
'degrees',
|
|
1778
|
+
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}`),
|
|
1779
|
+
);
|
|
1780
|
+
|
|
1781
|
+
// What the weight-aware and art-reading rows need.
|
|
1782
|
+
const amplitude = input.motionAmplitude;
|
|
1783
|
+
const amplitudeMissing =
|
|
1784
|
+
amplitude === undefined
|
|
1785
|
+
? `${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)`
|
|
1786
|
+
: amplitude === null
|
|
1787
|
+
? `${who}: motionAmplitude is declared absent (null), so no motion amplitude sizes a deformation need; required { tracks, gradation } for this row — no amplitude is assumed`
|
|
1788
|
+
: null;
|
|
1789
|
+
const unweighted = source.weights === null ? `${who}: source.weights is null — an unweighted mesh has no weight field for a motion to move` : null;
|
|
1790
|
+
const delta = targets.maxBoundaryDeviation;
|
|
1791
|
+
const noDelta = delta === null ? `${who}: targets.maxBoundaryDeviation is null, so no skip of the outline has a deviation to be held to` : null;
|
|
1792
|
+
const artCount = rasters.artCount();
|
|
1793
|
+
const fewArt =
|
|
1794
|
+
artCount < input.minArtSamples
|
|
1795
|
+
? `${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`
|
|
1796
|
+
: null;
|
|
1797
|
+
const allocationArt: AllocationArt = { mask: art.mask, bits: rasters.artBits(), scale: art.frame.pageScale };
|
|
1798
|
+
|
|
1799
|
+
// MQ_DEFORM_LOAD.
|
|
1800
|
+
const loadWhy = amplitudeMissing ?? unweighted;
|
|
1801
|
+
if (loadWhy !== null || amplitude === undefined || amplitude === null || source.weights === null) {
|
|
1802
|
+
row('MQ_DEFORM_LOAD', 'px', missing('MQ_DEFORM_LOAD', 'px', loadWhy ?? ''));
|
|
1803
|
+
} else {
|
|
1804
|
+
const load = deformLoad(points, source.triangles, source.weights, amplitude, plant);
|
|
1805
|
+
row(
|
|
1806
|
+
'MQ_DEFORM_LOAD',
|
|
1807
|
+
'px',
|
|
1808
|
+
load.measured
|
|
1809
|
+
? measured('MQ_DEFORM_LOAD', 'px', load.value.value, load.value.edge === null ? NOTHING_WORSE : { at: { edge: load.value.edge } })
|
|
1810
|
+
: missing('MQ_DEFORM_LOAD', 'px', `${who}: ${load.reason}`),
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// MQ_ALLOCATION_CONTRAST.
|
|
1815
|
+
const planted = plant === 'contrast-without-amplitude' && (amplitude === undefined || amplitude === null) ? { tracks: [], gradation: 0 } : null;
|
|
1816
|
+
const declared = planted ?? amplitude;
|
|
1817
|
+
let regionWhy: string | null = null;
|
|
1818
|
+
for (const region of targets.regions) {
|
|
1819
|
+
const refusal = regionRefusal(region, art.frame.pageScale, who, steps);
|
|
1820
|
+
if (refusal !== null) {
|
|
1821
|
+
regionWhy = `${who}: region "${region.name}" is refused (${refusal.code}), so its density cannot be read as a declared need`;
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
const contrastWhy = (planted === null ? amplitudeMissing : null) ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
|
|
1826
|
+
if (contrastWhy !== null || declared === undefined || declared === null || source.weights === null || delta === null) {
|
|
1827
|
+
row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', contrastWhy ?? ''));
|
|
1828
|
+
} else {
|
|
1829
|
+
const ownHull = points.slice(0, outline.hull);
|
|
1830
|
+
const silhouette = boundaryNecessityOnce(rasters, ownHull, delta, allocationArt, null).path;
|
|
1831
|
+
const contrast = allocationContrast(points, source.triangles, outline.hull, source.weights, delta, allocationArt, targets.regions, declared, silhouette, plant);
|
|
1832
|
+
if (contrast.measured) {
|
|
1833
|
+
const c = contrast.value;
|
|
1834
|
+
row('MQ_ALLOCATION_CONTRAST', 'fraction', measured('MQ_ALLOCATION_CONTRAST', 'fraction', c.contrast, { at: { vertex: c.worstVertex } }), {
|
|
1835
|
+
contrast: { economy: c.economy, dense: c.dense, rest: c.rest },
|
|
1836
|
+
});
|
|
1837
|
+
} else {
|
|
1838
|
+
row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', `${who}: ${contrast.reason}`));
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// MQ_BOUNDARY_NECESSARY.
|
|
1843
|
+
const reference = input.referenceHull;
|
|
1844
|
+
const noReference = reference === null ? `${who}: no referenceHull was given, so there is no source hull to find the necessary outline of` : null;
|
|
1845
|
+
const necessaryWhy = noReference ?? noDelta ?? fewArt;
|
|
1846
|
+
if (necessaryWhy !== null || reference === null || delta === null) {
|
|
1847
|
+
row('MQ_BOUNDARY_NECESSARY', 'count', missing('MQ_BOUNDARY_NECESSARY', 'count', necessaryWhy ?? ''));
|
|
1848
|
+
} else {
|
|
1849
|
+
const found = boundaryNecessityOnce(rasters, reference, delta, allocationArt, plant);
|
|
1850
|
+
const kept = new Set(found.path.map((i) => pointKey(reference[i])));
|
|
1851
|
+
const beyond = outline.walk.find((v) => !kept.has(pointKey(points[v])));
|
|
1852
|
+
row('MQ_BOUNDARY_NECESSARY', 'count', measured('MQ_BOUNDARY_NECESSARY', 'count', found.count, beyond === undefined ? NOTHING_WORSE : { at: { vertex: beyond } }), {
|
|
1853
|
+
search: { shortcutsTested: found.tests, bound: found.bound },
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
return out;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1593
1859
|
/** What one source states about itself: hull and interior from the outline, bindings from the weights. */
|
|
1594
1860
|
function countsOf(source: SourceMesh, outline: MeshOutline): MeshCounts {
|
|
1595
1861
|
const bindings = source.weights === null ? 0 : source.weights.reduce((s, v) => s + v.length, 0);
|
|
@@ -1901,6 +2167,7 @@ function effectiveOf(input: MeshMeasureInput): EffectiveSettings {
|
|
|
1901
2167
|
boneOrder: input.boneOrder,
|
|
1902
2168
|
schedule: null,
|
|
1903
2169
|
budget: null,
|
|
2170
|
+
...(input.motionAmplitude === undefined ? {} : { motionAmplitude: input.motionAmplitude }),
|
|
1904
2171
|
};
|
|
1905
2172
|
}
|
|
1906
2173
|
|
|
@@ -1990,6 +2257,8 @@ function effectiveJson(e: EffectiveSettings): Json {
|
|
|
1990
2257
|
schedule: scheduleJson(e.schedule),
|
|
1991
2258
|
budget: e.budget === null ? null : { maxCandidates: e.budget.maxCandidates },
|
|
1992
2259
|
...(e.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: e.stopAfterAccepted }),
|
|
2260
|
+
...(e.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: e.boundaryRuns.maxVertices } }),
|
|
2261
|
+
...(e.motionAmplitude === undefined ? {} : { motionAmplitude: amplitudeJson(e.motionAmplitude) }),
|
|
1993
2262
|
};
|
|
1994
2263
|
}
|
|
1995
2264
|
|
|
@@ -2033,9 +2302,32 @@ function rowJson(r: MeasureRow): Json {
|
|
|
2033
2302
|
}
|
|
2034
2303
|
if (r.sampling !== undefined) out.sampling = { domain: r.sampling.domain, count: r.sampling.count };
|
|
2035
2304
|
if (r.motion !== undefined) out.motion = motionDetailJson(r.motion);
|
|
2305
|
+
if (r.allocation !== undefined) out.allocation = allocationDetailJson(r.allocation);
|
|
2036
2306
|
return out;
|
|
2037
2307
|
}
|
|
2038
2308
|
|
|
2309
|
+
function allocationDetailJson(a: AllocationDetail): Json {
|
|
2310
|
+
const out: { [key: string]: Json } = { reading: a.reading };
|
|
2311
|
+
if (a.contrast !== undefined) {
|
|
2312
|
+
const c = a.contrast;
|
|
2313
|
+
out.contrast = {
|
|
2314
|
+
economy: c.economy,
|
|
2315
|
+
dense: { vertices: c.dense.vertices, removableAlone: c.dense.removableAlone },
|
|
2316
|
+
rest: { vertices: c.rest.vertices, removableAlone: c.rest.removableAlone },
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
if (a.search !== undefined) out.search = { shortcutsTested: a.search.shortcutsTested, bound: a.search.bound };
|
|
2320
|
+
return out;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function amplitudeJson(a: MotionAmplitude | null): Json {
|
|
2324
|
+
if (a === null) return null;
|
|
2325
|
+
return {
|
|
2326
|
+
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 })),
|
|
2327
|
+
gradation: a.gradation,
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2039
2331
|
function sectionJson(s: EvidenceSection): { [key: string]: Json } {
|
|
2040
2332
|
const m = s.summary;
|
|
2041
2333
|
return {
|
|
@@ -2067,7 +2359,7 @@ function candidateJson(c: CandidateReport): Json {
|
|
|
2067
2359
|
deformRemapped: k.deformRemapped.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key, droppedVertices: [...d.droppedVertices] })),
|
|
2068
2360
|
deformReevaluated: k.deformReevaluated.map((d) => ({ animation: d.animation, attachment: attachmentJson(d.attachment), key: d.key })),
|
|
2069
2361
|
linkedMeshes: k.linkedMeshes.map(attachmentJson),
|
|
2070
|
-
acceptedAt: [...
|
|
2362
|
+
acceptedAt: k.acceptedAt.map((a) => ({ step: a.step, kind: a.kind, count: a.count, sourceVertices: [...a.sourceVertices] })),
|
|
2071
2363
|
};
|
|
2072
2364
|
}
|
|
2073
2365
|
return out;
|