rig-c 2.27.0 → 2.29.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 +18 -4
- package/src/meshcompare.ts +56 -2
- package/src/meshquality.ts +51 -14
- package/src/meshreduce.ts +74 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-c",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/meshallocation.ts
CHANGED
|
@@ -54,6 +54,10 @@ type Weights = ReadonlyArray<ReadonlyArray<{ bone: string; weight: number }>>;
|
|
|
54
54
|
* - `bstar-ignores-art`: B\* without the art test.
|
|
55
55
|
* - `rows-required`: the rows counted as required.
|
|
56
56
|
* - `mutates-input`: the measurement sorts the caller's triangle list in place.
|
|
57
|
+
* - `gradation-defaulted`: an amplitude whose `gradation` is left out or `null` read at 0.75, §8's fixtures' value —
|
|
58
|
+
* the default issue #1291 measured and refused to choose.
|
|
59
|
+
* - `load-needs-gradation`: `MQ_DEFORM_LOAD` left unread for want of a `gradation` it does not read.
|
|
60
|
+
* - `gradation-ignored`: the need relaxed at 0.75 whatever `gradation` declares, so Δ no longer reads the field.
|
|
57
61
|
*/
|
|
58
62
|
export type AllocationPlant =
|
|
59
63
|
| 'omit-rows'
|
|
@@ -64,7 +68,10 @@ export type AllocationPlant =
|
|
|
64
68
|
| 'undeclared-pair-is-rigid'
|
|
65
69
|
| 'bstar-ignores-art'
|
|
66
70
|
| 'rows-required'
|
|
67
|
-
| 'mutates-input'
|
|
71
|
+
| 'mutates-input'
|
|
72
|
+
| 'gradation-defaulted'
|
|
73
|
+
| 'load-needs-gradation'
|
|
74
|
+
| 'gradation-ignored';
|
|
68
75
|
|
|
69
76
|
/** The art as the allocation rows read it: art pixels on the mask's grid, and the scale that grid is at. */
|
|
70
77
|
export interface AllocationArt {
|
|
@@ -493,7 +500,11 @@ export interface AllocationContrast {
|
|
|
493
500
|
worstVertex: number;
|
|
494
501
|
}
|
|
495
502
|
|
|
496
|
-
/**
|
|
503
|
+
/**
|
|
504
|
+
* h\*: the largest local size every primary need allows at a point, each relaxed at gradation G away from its source —
|
|
505
|
+
* G the caller's `motionAmplitude.gradation`, never a constant of this module: §8 measured that it derives from nothing
|
|
506
|
+
* a rig declares and that Δ moves with it (issue #1291). A region relaxes at its own declared `grade` over its band.
|
|
507
|
+
*/
|
|
497
508
|
interface Need {
|
|
498
509
|
at(p: Pt): number;
|
|
499
510
|
}
|
|
@@ -559,7 +570,8 @@ function needField(points: readonly Pt[], triangles: readonly number[], weights:
|
|
|
559
570
|
const twiceSigned = (a: Pt, b: Pt, c: Pt): number => (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
560
571
|
|
|
561
572
|
/**
|
|
562
|
-
* Δ and E (§8; the stage-A record for #1271, M7)
|
|
573
|
+
* Δ and E (§8; the stage-A record for #1271, M7), with the need relaxing at `gradation` — the caller's declared G,
|
|
574
|
+
* which `measureMeshQuality` hands over only when it is set. For each vertex the
|
|
563
575
|
* cheapest half-edge collapse into a neighbour — a hull vertex only along the
|
|
564
576
|
* hull — that turns no triangle over; ν is the largest, over what that
|
|
565
577
|
* collapse changes, of: the silhouette (the moved outline's chord distance
|
|
@@ -580,13 +592,15 @@ export function allocationContrast(
|
|
|
580
592
|
art: AllocationArt,
|
|
581
593
|
regions: readonly RefinementRegion[],
|
|
582
594
|
declared: MotionAmplitude,
|
|
595
|
+
gradation: number,
|
|
583
596
|
silhouette: number[],
|
|
584
597
|
plant: AllocationPlant | null,
|
|
585
598
|
): Reading<AllocationContrast> {
|
|
586
599
|
const n = points.length;
|
|
587
600
|
const pairs = new PairAmplitudes(declared, plant);
|
|
588
601
|
const amplitudeRead = plant !== 'contrast-without-amplitude';
|
|
589
|
-
const
|
|
602
|
+
const G = plant === 'gradation-ignored' ? 0.75 : gradation;
|
|
603
|
+
const need = needField(points, triangles, weights, silhouette, regions, amplitudeRead ? pairs : new PairAmplitudes({ tracks: [] }, 'undeclared-pair-is-rigid'), G);
|
|
590
604
|
if (need === null) return unmeasured(undeclaredPairReason(pairs.missing!));
|
|
591
605
|
const nb: Array<Set<number>> = Array.from({ length: n }, () => new Set<number>());
|
|
592
606
|
const triOf: number[][] = Array.from({ length: n }, () => []);
|
package/src/meshcompare.ts
CHANGED
|
@@ -79,6 +79,7 @@ import { artOf, MeshReductionError, r6 } from './mesh.ts';
|
|
|
79
79
|
import {
|
|
80
80
|
measureMeshQuality,
|
|
81
81
|
MESH_QUALITY_REPORT_SPEC,
|
|
82
|
+
validateMotionAmplitude,
|
|
82
83
|
type ArtFitBounds,
|
|
83
84
|
type ArtInput,
|
|
84
85
|
type AttachmentRef,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
type MeasureRow,
|
|
90
91
|
type MeshCounts,
|
|
91
92
|
type MeshQualityReport,
|
|
93
|
+
type MotionAmplitude,
|
|
92
94
|
type MotionBounds,
|
|
93
95
|
type MotionReading,
|
|
94
96
|
type MotionRowDetail,
|
|
@@ -151,8 +153,27 @@ export interface MotionComparisonInput {
|
|
|
151
153
|
bounds: MotionBounds;
|
|
152
154
|
motionRequired: boolean;
|
|
153
155
|
perFrame: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Issue #1291: the motion amplitude the setup art fit's measurement reads — `MeshMeasureInput.motionAmplitude`, the
|
|
158
|
+
* same shape and the same handling, handed to that measurement of every build and to nothing else (no frame, no
|
|
159
|
+
* motion row and no acceptance reads it). Left out, it is not declared and the setup sections' `MQ_DEFORM_LOAD`
|
|
160
|
+
* and `MQ_ALLOCATION_CONTRAST` are `not-measurable` naming it, as before the field existed; `null` declares it
|
|
161
|
+
* absent; set, `MQ_DEFORM_LOAD` is measured on each build's setup section. `MQ_ALLOCATION_CONTRAST` stays
|
|
162
|
+
* `not-measurable` there either way, naming `targets.maxBoundaryDeviation`: the comparison declares no deviation
|
|
163
|
+
* bound and no reference hull. Anything that is not a `MotionAmplitude` in full is refused `COMPARE_INPUT_MISSING`
|
|
164
|
+
* before any build is read, in the measurement's words. Echoed in `effective` when set, `null` included.
|
|
165
|
+
*/
|
|
166
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
171
|
+
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
172
|
+
* before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
|
|
173
|
+
* amplitude moves a verdict. `compareMeshesInMotion` plants none.
|
|
174
|
+
*/
|
|
175
|
+
export type ComparePlant = 'amplitude-not-carried' | 'null-read-as-left-out' | 'amplitude-unvalidated' | 'echo-when-unset' | 'amplitude-gates-acceptance';
|
|
176
|
+
|
|
156
177
|
// ---------------------------------------------------------------------------
|
|
157
178
|
// fixed tolerances — the tree's own
|
|
158
179
|
// ---------------------------------------------------------------------------
|
|
@@ -204,7 +225,7 @@ function validateFit(field: string, f: unknown): void {
|
|
|
204
225
|
* art itself (mask, threshold, frame) is `measureMeshQuality`'s to refuse, by
|
|
205
226
|
* its own codes, when the setup art fit is taken.
|
|
206
227
|
*/
|
|
207
|
-
function validateInput(input: MotionComparisonInput): void {
|
|
228
|
+
function validateInput(input: MotionComparisonInput, plant: ComparePlant | null): void {
|
|
208
229
|
if (!isObject(input)) missing('the input', input, 'a MotionComparisonInput object');
|
|
209
230
|
const builds = [input.reference, ...(Array.isArray(input.candidates) ? input.candidates : [])];
|
|
210
231
|
if (!Array.isArray(input.candidates) || input.candidates.length === 0) missing('candidates', input.candidates, 'a non-empty list of builds');
|
|
@@ -257,6 +278,16 @@ function validateInput(input: MotionComparisonInput): void {
|
|
|
257
278
|
if (b.minStretch !== undefined && (!isFiniteNumber(b.minStretch) || b.minStretch < 0)) missing('bounds.minStretch', b.minStretch, 'a finite ratio >= 0, or the field left out');
|
|
258
279
|
if (typeof input.motionRequired !== 'boolean') missing('motionRequired', input.motionRequired, 'true or false (P6)');
|
|
259
280
|
if (typeof input.perFrame !== 'boolean') missing('perFrame', input.perFrame, 'true or false (P7)');
|
|
281
|
+
if (plant !== 'amplitude-unvalidated') {
|
|
282
|
+
// Issue #1291: the measurement's own validator, so a comparison and a measurement refuse one value in the same
|
|
283
|
+
// words — under the comparison's code, and here, before any build is read.
|
|
284
|
+
try {
|
|
285
|
+
validateMotionAmplitude('the comparison', input.motionAmplitude);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (err instanceof MeshReductionError) refuse('COMPARE_INPUT_MISSING', err.message.slice(err.code.length + 2));
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
260
291
|
const s = input.schedule;
|
|
261
292
|
if (s === null) return;
|
|
262
293
|
if (!isObject(s)) missing('schedule', s, 'a MotionSchedule, or null for no motion');
|
|
@@ -852,7 +883,24 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
|
|
|
852
883
|
* undercut at the setup pose fails `referenceArtFit`, and `measureMeshQuality`'s own codes for the art).
|
|
853
884
|
*/
|
|
854
885
|
export function compareMeshesInMotion(input: MotionComparisonInput): MeshQualityReport {
|
|
855
|
-
|
|
886
|
+
return compareMeshesInMotionWith(input, null);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* `compareMeshesInMotion` with a fault planted in how it carries `motionAmplitude` (`ComparePlant`) — the
|
|
891
|
+
* `mesh-compare` suite's negative controls. Internal: merely exported, so not promised (RELEASING.md *The import
|
|
892
|
+
* surface*).
|
|
893
|
+
*/
|
|
894
|
+
export function compareMeshesInMotionPlanted(input: MotionComparisonInput, plant: ComparePlant): MeshQualityReport {
|
|
895
|
+
return compareMeshesInMotionWith(input, plant);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null): MeshQualityReport {
|
|
899
|
+
validateInput(input, plant);
|
|
900
|
+
const amplitude = input.motionAmplitude;
|
|
901
|
+
// What the setup measurement is handed: the field as the caller set it — left out as left out, `null` as `null`.
|
|
902
|
+
const handed: { motionAmplitude?: MotionAmplitude | null } =
|
|
903
|
+
plant === 'amplitude-not-carried' || amplitude === undefined || (plant === 'null-read-as-left-out' && amplitude === null) ? {} : { motionAmplitude: amplitude };
|
|
856
904
|
const refs = input.attachments.map((a) => a.attachment);
|
|
857
905
|
const reference = readBuild(input.reference, 'reference', refs);
|
|
858
906
|
const candidates = input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs));
|
|
@@ -895,9 +943,14 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
895
943
|
influences: null,
|
|
896
944
|
boneOrder: null,
|
|
897
945
|
preset: null,
|
|
946
|
+
...handed,
|
|
898
947
|
});
|
|
899
948
|
const c = measured.candidates[0];
|
|
900
949
|
if (c.geometry === null || c.counts === null) return { geometry: null, counts: null };
|
|
950
|
+
if (plant === 'amplitude-gates-acceptance') {
|
|
951
|
+
const load = c.geometry.rows.find((r) => r.code === 'MQ_DEFORM_LOAD');
|
|
952
|
+
if (load !== undefined && load.state === 'undeclared') c.geometry.verdict = 'fail';
|
|
953
|
+
}
|
|
901
954
|
sections.push(c.geometry);
|
|
902
955
|
counts.push(c.counts);
|
|
903
956
|
}
|
|
@@ -1176,6 +1229,7 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
|
|
|
1176
1229
|
boneOrder: null,
|
|
1177
1230
|
schedule,
|
|
1178
1231
|
budget: null,
|
|
1232
|
+
...(amplitude === undefined ? (plant === 'echo-when-unset' ? { motionAmplitude: null } : {}) : { motionAmplitude: amplitude }),
|
|
1179
1233
|
};
|
|
1180
1234
|
return {
|
|
1181
1235
|
spec: MESH_QUALITY_REPORT_SPEC,
|
package/src/meshquality.ts
CHANGED
|
@@ -273,6 +273,16 @@ export interface MeshReductionInput {
|
|
|
273
273
|
* refused.
|
|
274
274
|
*/
|
|
275
275
|
removalOrder?: 'deformation-load';
|
|
276
|
+
/**
|
|
277
|
+
* The motion amplitude the two weight-aware allocation rows read (issue #1287) — `MeshMeasureInput.motionAmplitude`,
|
|
278
|
+
* the same shape and the same handling: left out, it is not declared; `null` declares it absent; anything else that
|
|
279
|
+
* is not a `MotionAmplitude` in full is refused `REDUCE_INPUT_MISSING` naming its path, before any work. Set, the
|
|
280
|
+
* result's measurement reads it, so `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD` are measured on the report rather
|
|
281
|
+
* than `not-measurable`; it is echoed in `effective` (`null` included). It declares no bound — both rows stay
|
|
282
|
+
* `undeclared` — and no step reads it: the admission, every step and the post-pass are measured without it, so the
|
|
283
|
+
* mesh, `acceptedAt`, the termination and every other row are the call's without it (docs/MESH_REDUCTION.md §8).
|
|
284
|
+
*/
|
|
285
|
+
motionAmplitude?: MotionAmplitude | null;
|
|
276
286
|
}
|
|
277
287
|
|
|
278
288
|
/**
|
|
@@ -369,10 +379,20 @@ export interface MotionAmplitude {
|
|
|
369
379
|
/** One entry per track the caller declares (an animation, a parameter axis — its name is echoed, never read). */
|
|
370
380
|
tracks: TrackAmplitude[];
|
|
371
381
|
/**
|
|
372
|
-
* G, px per px
|
|
373
|
-
*
|
|
382
|
+
* G, px per px — an author's number, read by `MQ_ALLOCATION_CONTRAST` only (issue #1291). Δ asks of each vertex
|
|
383
|
+
* whether the mesh still meets every declared need without it; a need — a silhouette edge of the B\* outline, a
|
|
384
|
+
* deforming triangle sized by θ and ε, a region — allows edges of its own size h at its source and h + G × d at
|
|
385
|
+
* distance d from it, the form of a region's `grade`. So G is the fastest rate at which the author allows the mesh to
|
|
386
|
+
* coarsen away from what needs density: a larger G lets the need relax sooner, so more dense vertices read as
|
|
387
|
+
* removable and Δ and economy E rise (graded layers read as slack); a smaller G keeps more of them needed, and at 0
|
|
388
|
+
* every point needs the smallest size any source declares, so nothing reads as removable. The value an author
|
|
389
|
+
* declares on a region's `grade` for the same mesh is the same quantity; §8's fixtures used 0.75 because it is the
|
|
390
|
+
* one `grade` any of them declares. Nothing a rig or a reduction declares fixes G — docs/MESH_REDUCTION.md §8
|
|
391
|
+
* measured every candidate derivation, and Δ moves with G on every input there — so none is assumed: left out, it is
|
|
392
|
+
* not declared, and `null` declares it absent; either leaves `MQ_ALLOCATION_CONTRAST` `not-measurable` naming this
|
|
393
|
+
* field, and `MQ_DEFORM_LOAD`, which does not read G, measured. Set, finite and 0 or more.
|
|
374
394
|
*/
|
|
375
|
-
gradation
|
|
395
|
+
gradation?: number | null;
|
|
376
396
|
}
|
|
377
397
|
|
|
378
398
|
/** One track's amplitude: the pairs of bones a share moves between in it, each with θ, and the track's ε. */
|
|
@@ -584,7 +604,7 @@ export interface EffectiveSettings {
|
|
|
584
604
|
stopAfterAccepted?: number;
|
|
585
605
|
/** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
|
|
586
606
|
boundaryRuns?: BoundaryRuns;
|
|
587
|
-
/** A measurement's `motionAmplitude`, echoed when the input set it — `null` included (
|
|
607
|
+
/** A measurement's or a reduction's `motionAmplitude`, echoed when the input set it — `null` included (issues #1280, #1287); absent otherwise. */
|
|
588
608
|
motionAmplitude?: MotionAmplitude | null;
|
|
589
609
|
/** A reduction's `retriangulate`, echoed when the input set it (issue #1283); absent otherwise. */
|
|
590
610
|
retriangulate?: 'delaunay';
|
|
@@ -842,16 +862,24 @@ function validateInput(input: MeshMeasureInput): void {
|
|
|
842
862
|
if (input.preset !== null && (!isObject(input.preset) || typeof input.preset.name !== 'string' || typeof input.preset.version !== 'string')) {
|
|
843
863
|
refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
|
|
844
864
|
}
|
|
845
|
-
|
|
865
|
+
validateMotionAmplitude(who, input.motionAmplitude);
|
|
846
866
|
}
|
|
847
867
|
|
|
848
|
-
/**
|
|
849
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in
|
|
870
|
+
* full, else refused `REDUCE_INPUT_MISSING` naming the path. `reduceMesh` refuses its own field with it before any work
|
|
871
|
+
* (issue #1287), so a reduction and a measurement refuse one value in the same words. Internal, as
|
|
872
|
+
* `measureMeshQualityWith` is: on `rig-c/mesh` only through `export *`.
|
|
873
|
+
*/
|
|
874
|
+
export function validateMotionAmplitude(who: string, amplitude: unknown): void {
|
|
850
875
|
if (amplitude === undefined || amplitude === null) return;
|
|
851
876
|
const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
|
|
852
877
|
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
|
-
|
|
854
|
-
|
|
878
|
+
// Issue #1291: `gradation` left out or `null` is accepted — MQ_ALLOCATION_CONTRAST then reads not-measurable naming it.
|
|
879
|
+
if (amplitude.gradation !== undefined && amplitude.gradation !== null && (!isFiniteNumber(amplitude.gradation) || amplitude.gradation < 0)) {
|
|
880
|
+
// A non-finite number is named as itself: JSON would print NaN as null, which this field now accepts.
|
|
881
|
+
const found = typeof amplitude.gradation === 'number' ? String(amplitude.gradation) : JSON.stringify(amplitude.gradation);
|
|
882
|
+
refuse('REDUCE_INPUT_MISSING', `${who}: motionAmplitude.gradation is ${found}; required a finite number of px per px, 0 or more, null, or the field left out — no gradation is assumed`);
|
|
855
883
|
}
|
|
856
884
|
amplitude.tracks.forEach((track: unknown, i: number) => {
|
|
857
885
|
const at = `motionAmplitude.tracks[${i}]`;
|
|
@@ -1843,7 +1871,7 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
|
|
|
1843
1871
|
const allocationArt: AllocationArt = { mask: art.mask, bits: rasters.artBits(), scale: art.frame.pageScale };
|
|
1844
1872
|
|
|
1845
1873
|
// MQ_DEFORM_LOAD.
|
|
1846
|
-
const loadWhy = amplitudeMissing ?? unweighted;
|
|
1874
|
+
const loadWhy = amplitudeMissing ?? unweighted ?? (plant === 'load-needs-gradation' && (amplitude?.gradation === undefined || amplitude.gradation === null) ? `${who}: planted — the load refused for want of a gradation it does not read` : null);
|
|
1847
1875
|
if (loadWhy !== null || amplitude === undefined || amplitude === null || source.weights === null) {
|
|
1848
1876
|
row('MQ_DEFORM_LOAD', 'px', missing('MQ_DEFORM_LOAD', 'px', loadWhy ?? ''));
|
|
1849
1877
|
} else {
|
|
@@ -1860,6 +1888,15 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
|
|
|
1860
1888
|
// MQ_ALLOCATION_CONTRAST.
|
|
1861
1889
|
const planted = plant === 'contrast-without-amplitude' && (amplitude === undefined || amplitude === null) ? { tracks: [], gradation: 0 } : null;
|
|
1862
1890
|
const declared = planted ?? amplitude;
|
|
1891
|
+
// Issue #1291: G is the author's; an amplitude without it measures the load and leaves Δ unread, naming the field.
|
|
1892
|
+
const declaredGradation = declared === undefined || declared === null ? undefined : declared.gradation;
|
|
1893
|
+
const gradation = plant === 'gradation-defaulted' && (declaredGradation === undefined || declaredGradation === null) ? 0.75 : declaredGradation;
|
|
1894
|
+
const gradationMissing =
|
|
1895
|
+
declared === undefined || declared === null || (gradation !== undefined && gradation !== null)
|
|
1896
|
+
? null
|
|
1897
|
+
: gradation === undefined
|
|
1898
|
+
? `${who}: motionAmplitude.gradation is not declared (the field is left out), so no rate says how fast a declared need relaxes away from its source; required G, px per px, 0 or more — none is assumed, because Δ moves with G and nothing a rig declares fixes it (docs/MESH_REDUCTION.md §8)`
|
|
1899
|
+
: `${who}: motionAmplitude.gradation is declared absent (null), so no rate says how fast a declared need relaxes away from its source; required G, px per px, 0 or more, for this row — none is assumed`;
|
|
1863
1900
|
let regionWhy: string | null = null;
|
|
1864
1901
|
for (const region of targets.regions) {
|
|
1865
1902
|
const refusal = regionRefusal(region, art.frame.pageScale, who, steps);
|
|
@@ -1868,13 +1905,13 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
|
|
|
1868
1905
|
break;
|
|
1869
1906
|
}
|
|
1870
1907
|
}
|
|
1871
|
-
const contrastWhy = (planted === null ? amplitudeMissing : null) ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
|
|
1872
|
-
if (contrastWhy !== null || declared === undefined || declared === null || source.weights === null || delta === null) {
|
|
1908
|
+
const contrastWhy = (planted === null ? amplitudeMissing : null) ?? gradationMissing ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
|
|
1909
|
+
if (contrastWhy !== null || declared === undefined || declared === null || gradation === undefined || gradation === null || source.weights === null || delta === null) {
|
|
1873
1910
|
row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', contrastWhy ?? ''));
|
|
1874
1911
|
} else {
|
|
1875
1912
|
const ownHull = points.slice(0, outline.hull);
|
|
1876
1913
|
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);
|
|
1914
|
+
const contrast = allocationContrast(points, source.triangles, outline.hull, source.weights, delta, allocationArt, targets.regions, declared, gradation, silhouette, plant);
|
|
1878
1915
|
if (contrast.measured) {
|
|
1879
1916
|
const c = contrast.value;
|
|
1880
1917
|
row('MQ_ALLOCATION_CONTRAST', 'fraction', measured('MQ_ALLOCATION_CONTRAST', 'fraction', c.contrast, { at: { vertex: c.worstVertex } }), {
|
|
@@ -2372,7 +2409,7 @@ function amplitudeJson(a: MotionAmplitude | null): Json {
|
|
|
2372
2409
|
if (a === null) return null;
|
|
2373
2410
|
return {
|
|
2374
2411
|
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,
|
|
2412
|
+
...(a.gradation === undefined ? {} : { gradation: a.gradation }),
|
|
2376
2413
|
};
|
|
2377
2414
|
}
|
|
2378
2415
|
|
package/src/meshreduce.ts
CHANGED
|
@@ -70,6 +70,15 @@
|
|
|
70
70
|
* is not a step: no candidate, no `acceptedAt` entry, so a replay and the
|
|
71
71
|
* budget cut at the same step end on the same state and the same pass.
|
|
72
72
|
*
|
|
73
|
+
* ## The amplitude (issue #1287, opt-in)
|
|
74
|
+
*
|
|
75
|
+
* With `motionAmplitude`, the result's own measurement reads it, so the
|
|
76
|
+
* report's `MQ_ALLOCATION_CONTRAST` and `MQ_DEFORM_LOAD` are measured rather
|
|
77
|
+
* than `not-measurable`. No other measurement of the call is handed it — no
|
|
78
|
+
* step is decided by either row, and no report carries the other
|
|
79
|
+
* measurements' rows (`amplitudeOf`) — so the mesh and every step are the
|
|
80
|
+
* call's without it.
|
|
81
|
+
*
|
|
73
82
|
* A removal whose deviation floor — the furthest removed source-hull vertex
|
|
74
83
|
* from the candidate's outline, which `MQ_BOUNDARY_DEVIATION` can only exceed —
|
|
75
84
|
* is over the bound is refused without the measurement; its name, when read,
|
|
@@ -119,6 +128,7 @@ import {
|
|
|
119
128
|
edgeIsHeldByRegion,
|
|
120
129
|
measureMeshQualityStep,
|
|
121
130
|
measureMeshQualityWith,
|
|
131
|
+
validateMotionAmplitude,
|
|
122
132
|
type AcceptedOperation,
|
|
123
133
|
type AttachmentRef,
|
|
124
134
|
type ArtFitBounds,
|
|
@@ -130,6 +140,7 @@ import {
|
|
|
130
140
|
type MeshMeasureInput,
|
|
131
141
|
type MeshQualityReport,
|
|
132
142
|
type MeshReductionInput,
|
|
143
|
+
type MotionAmplitude,
|
|
133
144
|
type RefinementRegion,
|
|
134
145
|
type ReductionChanges,
|
|
135
146
|
type Retriangulation,
|
|
@@ -274,7 +285,7 @@ function artBoundAbsent(fit: ArtFitBounds, row: MeasureRow): boolean {
|
|
|
274
285
|
* by `measureMeshQuality`'s own validation when the source is first measured,
|
|
275
286
|
* with the same codes.
|
|
276
287
|
*/
|
|
277
|
-
function validateReduction(input: MeshReductionInput): void {
|
|
288
|
+
function validateReduction(input: MeshReductionInput, plant: ReductionPlant | null = null): void {
|
|
278
289
|
if (!isObject(input)) refuse('REDUCE_INPUT_MISSING', `the input is ${JSON.stringify(input)}; required a MeshReductionInput object`);
|
|
279
290
|
if (!isRef(input.attachment)) refuse('REDUCE_INPUT_MISSING', `attachment is ${JSON.stringify(input.attachment)}; required { skin: string | null, slot: string, attachment: string }`);
|
|
280
291
|
const who = `attachment ${nameOf(input.attachment)}`;
|
|
@@ -305,6 +316,9 @@ function validateReduction(input: MeshReductionInput): void {
|
|
|
305
316
|
if ('removalOrder' in input && input.removalOrder !== undefined && input.removalOrder !== 'deformation-load') {
|
|
306
317
|
refuse('REDUCE_INPUT_MISSING', `${who}: removalOrder is ${JSON.stringify(input.removalOrder)}; required "deformation-load" — single removals in ascending predicted deformation load, ties by source index — or the field left out (issue #1283: ascending source index)`);
|
|
307
318
|
}
|
|
319
|
+
// Issue #1287: refused here, before the admission measurement and any step, in the measurement's own words — the
|
|
320
|
+
// admission does not carry the field, and a call whose source is refused never reaches the one measurement that does.
|
|
321
|
+
if (plant !== 'amplitude-unvalidated') validateMotionAmplitude(who, input.motionAmplitude);
|
|
308
322
|
const src = input.source;
|
|
309
323
|
if (!isObject(src) || !Array.isArray(src.points)) refuse('REDUCE_INPUT_MISSING', `${who}: source is not { points, uvs, triangles, hull, weights }`);
|
|
310
324
|
const n = src.points.length;
|
|
@@ -791,8 +805,11 @@ interface Run {
|
|
|
791
805
|
stepRasters: StepRasters | null;
|
|
792
806
|
}
|
|
793
807
|
|
|
794
|
-
/**
|
|
795
|
-
|
|
808
|
+
/**
|
|
809
|
+
* A measurement of a canonical mesh against the result's full contract. `final` marks the result's own measurement —
|
|
810
|
+
* the one whose rows the report carries, and the only one handed the input's `motionAmplitude` (`amplitudeOf`).
|
|
811
|
+
*/
|
|
812
|
+
function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string, final = false): MeshQualityReport {
|
|
796
813
|
const { input } = run;
|
|
797
814
|
const targets = input.targets;
|
|
798
815
|
const measureInput: MeshMeasureInput = {
|
|
@@ -808,10 +825,33 @@ function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string): MeshQual
|
|
|
808
825
|
influences: input.influences,
|
|
809
826
|
boneOrder: input.boneOrder,
|
|
810
827
|
preset: input.preset,
|
|
828
|
+
...amplitudeOf(run.input, run.plant, final),
|
|
811
829
|
};
|
|
812
830
|
return run.stepRasters === null ? measureMeshQualityWith(measureInput, run.rasters) : measureMeshQualityStep(measureInput, run.stepRasters);
|
|
813
831
|
}
|
|
814
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Issue #1287: what one measurement of a reduction is handed as `motionAmplitude` — the input's, on the result's own
|
|
835
|
+
* measurement (`final`) only. The admission, every refinement and removal step and the post-pass are measured without
|
|
836
|
+
* it: no row it adds is read by a step (both are `undeclared`, so `firstBlockingRow` never names one) and no report
|
|
837
|
+
* carries those measurements' rows, so carrying it there would buy nothing and cost Δ's own silhouette search at every
|
|
838
|
+
* step (docs/MESH_REDUCTION.md §8, *Stage B — the amplitude on a reduction*, measured). A field left out is passed as
|
|
839
|
+
* left out and `null` as `null`, so the rows' reasons say which.
|
|
840
|
+
*/
|
|
841
|
+
function amplitudeOf(input: MeshReductionInput, plant: ReductionPlant | null, final: boolean): { motionAmplitude?: MotionAmplitude | null } {
|
|
842
|
+
const amplitude = input.motionAmplitude;
|
|
843
|
+
if (!final && plant !== 'amplitude-every-measurement' && plant !== 'amplitude-gates-steps') return {};
|
|
844
|
+
if (plant === 'amplitude-not-carried') return {};
|
|
845
|
+
if (plant === 'amplitude-invented' && (amplitude === undefined || amplitude === null)) {
|
|
846
|
+
const bones = [...new Set((input.source.weights ?? []).flatMap((v) => v.map((b) => b.bone)))].sort();
|
|
847
|
+
const pairs: Array<{ bones: [string, string]; theta: number }> = [];
|
|
848
|
+
for (let i = 0; i < bones.length; i++) for (let j = i + 1; j < bones.length; j++) pairs.push({ bones: [bones[i], bones[j]], theta: 1 });
|
|
849
|
+
return { motionAmplitude: { tracks: [{ track: 'invented', pairs, epsilon: 1 }], gradation: 0 } };
|
|
850
|
+
}
|
|
851
|
+
if (plant === 'null-read-as-left-out' && amplitude === null) return {};
|
|
852
|
+
return amplitude === undefined ? {} : { motionAmplitude: amplitude };
|
|
853
|
+
}
|
|
854
|
+
|
|
815
855
|
/** The order constraints are named in when several fail on one step: structure, then shape, then art, then density. */
|
|
816
856
|
const BLOCKING_ORDER = ['MQ_ORIENTATION', 'MQ_DEGENERATE', 'MQ_BOUNDARY_DEVIATION', 'MQ_COVERAGE', 'MQ_OVERSHOOT', 'MQ_UNDERCUT', 'MQ_MIN_ANGLE', 'MQ_MAX_EDGE', 'MQ_TRANSITION'];
|
|
817
857
|
|
|
@@ -1051,7 +1091,8 @@ export type ReductionPlant =
|
|
|
1051
1091
|
| 'measure-every-candidate'
|
|
1052
1092
|
| 'floor-half-a-pixel-short'
|
|
1053
1093
|
| RetriangulationPlant
|
|
1054
|
-
| OrderPlant
|
|
1094
|
+
| OrderPlant
|
|
1095
|
+
| AmplitudePlant;
|
|
1055
1096
|
|
|
1056
1097
|
/**
|
|
1057
1098
|
* Issue #1283's faults in the triangulation post-pass: the pass run on a call that did not opt in; the pass skipped
|
|
@@ -1064,6 +1105,24 @@ export type RetriangulationPlant = 'flips-without-opt-in' | 'flips-not-on-replay
|
|
|
1064
1105
|
/** Issue #1283's faults in the removal order: the load order used by a call that did not opt in, and the loads ranked descending. */
|
|
1065
1106
|
export type OrderPlant = 'order-without-opt-in' | 'order-descending';
|
|
1066
1107
|
|
|
1108
|
+
/**
|
|
1109
|
+
* Issue #1287's faults in carrying `motionAmplitude`: the amplitude not handed to the result's measurement; an
|
|
1110
|
+
* amplitude invented (every pair of the source's bones at θ 1, ε 1, gradation 0) when the field is left out or
|
|
1111
|
+
* `null`; `null` passed to the measurement as the field left out; the field not validated by the reduction; a
|
|
1112
|
+
* measured allocation row read as blocking a step; and `effective` echoing `null` for a field left out. And one path
|
|
1113
|
+
* that is not a fault, held equal to the call: the amplitude handed to every measurement of the call — the admission,
|
|
1114
|
+
* each step, the post-pass — which is what the decision to carry it on the result's measurement only is measured
|
|
1115
|
+
* against.
|
|
1116
|
+
*/
|
|
1117
|
+
export type AmplitudePlant =
|
|
1118
|
+
| 'amplitude-not-carried'
|
|
1119
|
+
| 'amplitude-invented'
|
|
1120
|
+
| 'null-read-as-left-out'
|
|
1121
|
+
| 'amplitude-unvalidated'
|
|
1122
|
+
| 'amplitude-gates-steps'
|
|
1123
|
+
| 'echo-when-unset'
|
|
1124
|
+
| 'amplitude-every-measurement';
|
|
1125
|
+
|
|
1067
1126
|
/** One removal-phase attempt as a control reads it (issue #1279): what was tried, and what refused it or null when taken. */
|
|
1068
1127
|
export interface AttemptRecord {
|
|
1069
1128
|
step: number;
|
|
@@ -1484,7 +1543,10 @@ function tryOperation(run: Run, vertices: readonly number[], full: boolean): Ref
|
|
|
1484
1543
|
};
|
|
1485
1544
|
}
|
|
1486
1545
|
}
|
|
1487
|
-
const
|
|
1546
|
+
const measured = measureAgainstTargets(run, canon.mesh, 'candidate');
|
|
1547
|
+
const blocking =
|
|
1548
|
+
firstBlockingRow(measured, run.input.targets.artFit) ??
|
|
1549
|
+
(run.plant === 'amplitude-gates-steps' ? ((measured.candidates[0]?.geometry?.rows ?? []).find((r) => (r.code === 'MQ_DEFORM_LOAD' || r.code === 'MQ_ALLOCATION_CONTRAST') && r.state === 'undeclared') ?? null) : null);
|
|
1488
1550
|
if (blocking !== null) {
|
|
1489
1551
|
undo();
|
|
1490
1552
|
return rowName(blocking);
|
|
@@ -1722,7 +1784,7 @@ export function reduceMeshWith(
|
|
|
1722
1784
|
plant: ReductionPlant | null = null,
|
|
1723
1785
|
observe: AttemptObserver | null = null,
|
|
1724
1786
|
): MeshReductionResult {
|
|
1725
|
-
validateReduction(input);
|
|
1787
|
+
validateReduction(input, plant);
|
|
1726
1788
|
if (steps !== null && steps.rasters !== rasters) {
|
|
1727
1789
|
refuse('REDUCE_ART_RASTERS_MISMATCH', `attachment ${nameOf(input.attachment)}: the step rasters were made over another rasters object; required step rasters made over the rasters passed beside them (stepRastersOf(rasters))`);
|
|
1728
1790
|
}
|
|
@@ -1754,9 +1816,10 @@ function reduceValidated(input: MeshReductionInput, rasters: ArtRasters, steps:
|
|
|
1754
1816
|
influences: input.influences,
|
|
1755
1817
|
boneOrder: input.boneOrder,
|
|
1756
1818
|
preset: input.preset,
|
|
1819
|
+
...amplitudeOf(input, plant, false),
|
|
1757
1820
|
};
|
|
1758
1821
|
const admit = steps === null ? measureMeshQualityWith(admitInput, rasters) : measureMeshQualityStep(admitInput, steps);
|
|
1759
|
-
const effective = effectiveOf(input, admit.effective, sourceHull);
|
|
1822
|
+
const effective = effectiveOf(input, admit.effective, sourceHull, plant);
|
|
1760
1823
|
const sourceCounts = admit.sourceCounts;
|
|
1761
1824
|
const noMesh = (termination: Termination): MeshReductionResult => ({
|
|
1762
1825
|
mesh: null,
|
|
@@ -1911,7 +1974,7 @@ function finish(run: Run, effective: EffectiveSettings, sourceCounts: MeshCounts
|
|
|
1911
1974
|
}
|
|
1912
1975
|
throw err;
|
|
1913
1976
|
}
|
|
1914
|
-
const measured = measureAgainstTargets(run, canon.mesh, 'result');
|
|
1977
|
+
const measured = measureAgainstTargets(run, canon.mesh, 'result', true);
|
|
1915
1978
|
const candidate = measured.candidates[0];
|
|
1916
1979
|
const changes: ReductionChanges = {
|
|
1917
1980
|
removedVertices: indexMap.filter((r) => r === null).length,
|
|
@@ -1952,7 +2015,8 @@ function reportOf(effective: EffectiveSettings, sourceCounts: MeshCounts | null,
|
|
|
1952
2015
|
}
|
|
1953
2016
|
|
|
1954
2017
|
/** Correction 1: the reduction's inputs echoed with their structure — the measurement's echo, with what a reduction adds. */
|
|
1955
|
-
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]
|
|
2018
|
+
function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sourceHull: Array<[number, number]>, plant: ReductionPlant | null): EffectiveSettings {
|
|
2019
|
+
const amplitude = plant === 'echo-when-unset' && input.motionAmplitude === undefined ? null : input.motionAmplitude;
|
|
1956
2020
|
const fit = (f: ArtFitBounds): ArtFitBounds => ({ minCoverage: f.minCoverage, maxOvershoot: f.maxOvershoot, maxUndercut: f.maxUndercut });
|
|
1957
2021
|
return {
|
|
1958
2022
|
...measured,
|
|
@@ -1962,6 +2026,7 @@ function effectiveOf(input: MeshReductionInput, measured: EffectiveSettings, sou
|
|
|
1962
2026
|
budget: { maxCandidates: input.budget.maxCandidates },
|
|
1963
2027
|
...(input.stopAfterAccepted === undefined ? {} : { stopAfterAccepted: input.stopAfterAccepted }),
|
|
1964
2028
|
...(input.boundaryRuns === undefined ? {} : { boundaryRuns: { maxVertices: input.boundaryRuns.maxVertices } }),
|
|
2029
|
+
...(amplitude === undefined ? {} : { motionAmplitude: amplitude }),
|
|
1965
2030
|
...(input.retriangulate === undefined ? {} : { retriangulate: input.retriangulate }),
|
|
1966
2031
|
...(input.removalOrder === undefined ? {} : { removalOrder: input.removalOrder }),
|
|
1967
2032
|
};
|