rig-c 2.28.0 → 2.30.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-c",
3
- "version": "2.28.0",
3
+ "version": "2.30.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/mesh.ts CHANGED
@@ -50,9 +50,11 @@ import type { ModelBinding, ModelVertices } from './model.ts';
50
50
  // two form an import cycle: safe only because neither reads the other's
51
51
  // bindings while it is being evaluated — every use is inside a function.
52
52
  // The reduction (`reduceMesh`, stage B2 of #1224) sits in the same cycle, by
53
- // the same rule.
53
+ // the same rule. So does the skinning residual and its envelope helper
54
+ // (`src/meshskinning.ts`, issue #1294), which the measurement reads.
54
55
  export * from './meshquality.ts';
55
56
  export * from './meshreduce.ts';
57
+ export * from './meshskinning.ts';
56
58
 
57
59
  export interface MeshSpecInput {
58
60
  /** Polygon in part-local pixels, y down, in manifest order. */
@@ -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
- /** h\*: the largest local size every primary need allows at a point, each relaxed at gradation G away from its source. */
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). For each vertex the
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 need = needField(points, triangles, weights, silhouette, regions, amplitudeRead ? pairs : new PairAmplitudes({ tracks: [], gradation: declared.gradation }, 'undeclared-pair-is-rigid'), declared.gradation);
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 }, () => []);
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Where a UV sample lies in a mesh's UV triangulation — §3 and correction 4 of
3
+ * docs/MESH_REDUCTION.md, the common domain two triangulations of one
4
+ * attachment are compared on.
5
+ *
6
+ * `uvCarriers` was written for the motion comparison (`src/meshcompare.ts`,
7
+ * issue #1230), which still re-exports it from `rig-c/meshcompare` where it is
8
+ * promised. It lives here, in a module that links no poser, because the
9
+ * skinning residual (`src/meshskinning.ts`, issue #1294) reads the same carriers
10
+ * from `rig-c/mesh`, which is geometry-only (§0, P1): one definition of "the
11
+ * triangle that carries this UV" for both, so the residual and the motion row
12
+ * it is compared with can never disagree about which triangle a sample is in.
13
+ *
14
+ * Pure: no clock, no randomness, no runtime.
15
+ */
16
+ import { MeshReductionError } from './mesh.ts';
17
+ import { areaBand, triangleAreas } from './areaband.ts';
18
+
19
+ /** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
20
+ const CONTAINS = 1e-9;
21
+
22
+ function refuse(code: string, message: string): never {
23
+ throw new MeshReductionError(code, message);
24
+ }
25
+
26
+ /** A sample's carrier: the UV triangle that contains it, its three vertex indices and the sample's barycentric coordinates there. */
27
+ export interface Carrier {
28
+ triangle: number;
29
+ corners: [number, number, number];
30
+ bary: [number, number, number];
31
+ }
32
+
33
+ /**
34
+ * Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
35
+ * Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
36
+ * other than across a vertex or edge they share.
37
+ */
38
+ export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
39
+ const areas = triangleAreas(uvs, triangles);
40
+ const band = areaBand(areas, uvs);
41
+ const live: number[] = [];
42
+ const boxes: number[] = [];
43
+ for (let t = 0; t < areas.length; t++) {
44
+ if (Math.abs(areas[t]) <= band) continue;
45
+ live.push(t);
46
+ const xs = [uvs[triangles[t * 3] * 2], uvs[triangles[t * 3 + 1] * 2], uvs[triangles[t * 3 + 2] * 2]];
47
+ const ys = [uvs[triangles[t * 3] * 2 + 1], uvs[triangles[t * 3 + 1] * 2 + 1], uvs[triangles[t * 3 + 2] * 2 + 1]];
48
+ boxes.push(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
49
+ }
50
+ return samples.map((s) => {
51
+ const [px, py] = s.uv;
52
+ const hits: Carrier[] = [];
53
+ live.forEach((t, k) => {
54
+ const slack = 1e-9;
55
+ if (px < boxes[k * 4] - slack || py < boxes[k * 4 + 1] - slack || px > boxes[k * 4 + 2] + slack || py > boxes[k * 4 + 3] + slack) return;
56
+ const i0 = triangles[t * 3];
57
+ const i1 = triangles[t * 3 + 1];
58
+ const i2 = triangles[t * 3 + 2];
59
+ const ax = uvs[i0 * 2];
60
+ const ay = uvs[i0 * 2 + 1];
61
+ const bx = uvs[i1 * 2] - ax;
62
+ const by = uvs[i1 * 2 + 1] - ay;
63
+ const cx = uvs[i2 * 2] - ax;
64
+ const cy = uvs[i2 * 2 + 1] - ay;
65
+ const qx = px - ax;
66
+ const qy = py - ay;
67
+ const det = bx * cy - cx * by;
68
+ const l1 = (qx * cy - cx * qy) / det;
69
+ const l2 = (bx * qy - qx * by) / det;
70
+ const l0 = 1 - l1 - l2;
71
+ if (l0 < -CONTAINS || l1 < -CONTAINS || l2 < -CONTAINS) return;
72
+ hits.push({ triangle: t, corners: [i0, i1, i2], bary: [l0, l1, l2] });
73
+ });
74
+ if (hits.length === 0) return null;
75
+ if (hits.length === 1) return hits[0];
76
+ // Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
77
+ // it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
78
+ const support = (c: Carrier): string =>
79
+ c.corners
80
+ .filter((_v, k) => c.bary[k] > CONTAINS)
81
+ .sort((a, b) => a - b)
82
+ .join(',');
83
+ const first = support(hits[0]);
84
+ const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
85
+ if (!shared) {
86
+ refuse(
87
+ 'COMPARE_UV_CARRIER_NOT_UNIQUE',
88
+ `${who}: the sample at uv (${px}, ${py}) lies in ${hits.length} UV triangles — ${hits.map((c) => `triangle ${c.triangle} (vertices ${c.corners.join(', ')})`).join(' and ')} — that do not meet there at a shared vertex or edge; required one carrier per sample (correction 4: overlapping or folded UV triangles would carry it by an arbitrary one)`,
89
+ );
90
+ }
91
+ return hits[0];
92
+ });
93
+ }
@@ -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,
@@ -98,6 +100,7 @@ import {
98
100
  type WorstSample,
99
101
  } from './meshquality.ts';
100
102
  import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
103
+ import { uvCarriers, type Carrier } from './meshcarriers.ts';
101
104
  import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
102
105
  import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
103
106
  import { sampleTime } from './core/animation.ts';
@@ -151,15 +154,31 @@ export interface MotionComparisonInput {
151
154
  bounds: MotionBounds;
152
155
  motionRequired: boolean;
153
156
  perFrame: boolean;
157
+ /**
158
+ * Issue #1291: the motion amplitude the setup art fit's measurement reads — `MeshMeasureInput.motionAmplitude`, the
159
+ * same shape and the same handling, handed to that measurement of every build and to nothing else (no frame, no
160
+ * motion row and no acceptance reads it). Left out, it is not declared and the setup sections' `MQ_DEFORM_LOAD`
161
+ * and `MQ_ALLOCATION_CONTRAST` are `not-measurable` naming it, as before the field existed; `null` declares it
162
+ * absent; set, `MQ_DEFORM_LOAD` is measured on each build's setup section. `MQ_ALLOCATION_CONTRAST` stays
163
+ * `not-measurable` there either way, naming `targets.maxBoundaryDeviation`: the comparison declares no deviation
164
+ * bound and no reference hull. Anything that is not a `MotionAmplitude` in full is refused `COMPARE_INPUT_MISSING`
165
+ * before any build is read, in the measurement's words. Echoed in `effective` when set, `null` included.
166
+ */
167
+ motionAmplitude?: MotionAmplitude | null;
154
168
  }
155
169
 
170
+ /**
171
+ * Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
172
+ * negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
173
+ * before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
174
+ * amplitude moves a verdict. `compareMeshesInMotion` plants none.
175
+ */
176
+ export type ComparePlant = 'amplitude-not-carried' | 'null-read-as-left-out' | 'amplitude-unvalidated' | 'echo-when-unset' | 'amplitude-gates-acceptance';
177
+
156
178
  // ---------------------------------------------------------------------------
157
179
  // fixed tolerances — the tree's own
158
180
  // ---------------------------------------------------------------------------
159
181
 
160
- /** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
161
- const CONTAINS = 1e-9;
162
-
163
182
  /** §3 *Independent evidence* (a): the rows a reference is refused on — its art fit against its own mask, and only that. */
164
183
  const REFERENCE_ART_FIT: readonly string[] = ['MQ_COVERAGE', 'MQ_OVERSHOOT', 'MQ_UNDERCUT'];
165
184
 
@@ -204,7 +223,7 @@ function validateFit(field: string, f: unknown): void {
204
223
  * art itself (mask, threshold, frame) is `measureMeshQuality`'s to refuse, by
205
224
  * its own codes, when the setup art fit is taken.
206
225
  */
207
- function validateInput(input: MotionComparisonInput): void {
226
+ function validateInput(input: MotionComparisonInput, plant: ComparePlant | null): void {
208
227
  if (!isObject(input)) missing('the input', input, 'a MotionComparisonInput object');
209
228
  const builds = [input.reference, ...(Array.isArray(input.candidates) ? input.candidates : [])];
210
229
  if (!Array.isArray(input.candidates) || input.candidates.length === 0) missing('candidates', input.candidates, 'a non-empty list of builds');
@@ -257,6 +276,16 @@ function validateInput(input: MotionComparisonInput): void {
257
276
  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
277
  if (typeof input.motionRequired !== 'boolean') missing('motionRequired', input.motionRequired, 'true or false (P6)');
259
278
  if (typeof input.perFrame !== 'boolean') missing('perFrame', input.perFrame, 'true or false (P7)');
279
+ if (plant !== 'amplitude-unvalidated') {
280
+ // Issue #1291: the measurement's own validator, so a comparison and a measurement refuse one value in the same
281
+ // words — under the comparison's code, and here, before any build is read.
282
+ try {
283
+ validateMotionAmplitude('the comparison', input.motionAmplitude);
284
+ } catch (err) {
285
+ if (err instanceof MeshReductionError) refuse('COMPARE_INPUT_MISSING', err.message.slice(err.code.length + 2));
286
+ throw err;
287
+ }
288
+ }
260
289
  const s = input.schedule;
261
290
  if (s === null) return;
262
291
  if (!isObject(s)) missing('schedule', s, 'a MotionSchedule, or null for no motion');
@@ -554,6 +583,9 @@ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames
554
583
  // §3 — the samples and their carriers
555
584
  // ---------------------------------------------------------------------------
556
585
 
586
+ /** Promised on `rig-c/meshcompare` since issue #1230; defined in `src/meshcarriers.ts` since issue #1294, which the skinning residual shares. */
587
+ export { uvCarriers };
588
+
557
589
  interface Sample {
558
590
  uv: [number, number];
559
591
  /** Part-local drawing px, y down — where a region's polygon is read. */
@@ -563,12 +595,6 @@ interface Sample {
563
595
  vertex: number | null;
564
596
  }
565
597
 
566
- interface Carrier {
567
- triangle: number;
568
- corners: [number, number, number];
569
- bary: [number, number, number];
570
- }
571
-
572
598
  interface MeshOf {
573
599
  uvs: number[];
574
600
  triangles: number[];
@@ -576,68 +602,6 @@ interface MeshOf {
576
602
  weights: SourceMesh['weights'];
577
603
  }
578
604
 
579
- /**
580
- * Each sample's carrier in one mesh's UV triangulation — §3 and correction 4.
581
- * Throws `COMPARE_UV_CARRIER_NOT_UNIQUE` for a sample two triangles carry
582
- * other than across a vertex or edge they share.
583
- */
584
- export function uvCarriers(uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> {
585
- const areas = triangleAreas(uvs, triangles);
586
- const band = areaBand(areas, uvs);
587
- const live: number[] = [];
588
- const boxes: number[] = [];
589
- for (let t = 0; t < areas.length; t++) {
590
- if (Math.abs(areas[t]) <= band) continue;
591
- live.push(t);
592
- const xs = [uvs[triangles[t * 3] * 2], uvs[triangles[t * 3 + 1] * 2], uvs[triangles[t * 3 + 2] * 2]];
593
- const ys = [uvs[triangles[t * 3] * 2 + 1], uvs[triangles[t * 3 + 1] * 2 + 1], uvs[triangles[t * 3 + 2] * 2 + 1]];
594
- boxes.push(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys));
595
- }
596
- return samples.map((s) => {
597
- const [px, py] = s.uv;
598
- const hits: Carrier[] = [];
599
- live.forEach((t, k) => {
600
- const slack = 1e-9;
601
- if (px < boxes[k * 4] - slack || py < boxes[k * 4 + 1] - slack || px > boxes[k * 4 + 2] + slack || py > boxes[k * 4 + 3] + slack) return;
602
- const i0 = triangles[t * 3];
603
- const i1 = triangles[t * 3 + 1];
604
- const i2 = triangles[t * 3 + 2];
605
- const ax = uvs[i0 * 2];
606
- const ay = uvs[i0 * 2 + 1];
607
- const bx = uvs[i1 * 2] - ax;
608
- const by = uvs[i1 * 2 + 1] - ay;
609
- const cx = uvs[i2 * 2] - ax;
610
- const cy = uvs[i2 * 2 + 1] - ay;
611
- const qx = px - ax;
612
- const qy = py - ay;
613
- const det = bx * cy - cx * by;
614
- const l1 = (qx * cy - cx * qy) / det;
615
- const l2 = (bx * qy - qx * by) / det;
616
- const l0 = 1 - l1 - l2;
617
- if (l0 < -CONTAINS || l1 < -CONTAINS || l2 < -CONTAINS) return;
618
- hits.push({ triangle: t, corners: [i0, i1, i2], bary: [l0, l1, l2] });
619
- });
620
- if (hits.length === 0) return null;
621
- if (hits.length === 1) return hits[0];
622
- // Several carriers are one hit only across a vertex or an edge they all share: the sample's support — the corners
623
- // it does not sit at zero weight on — is then the same vertex indices in each, and the carried point is theirs.
624
- const support = (c: Carrier): string =>
625
- c.corners
626
- .filter((_v, k) => c.bary[k] > CONTAINS)
627
- .sort((a, b) => a - b)
628
- .join(',');
629
- const first = support(hits[0]);
630
- const shared = first.split(',').length <= 2 && hits.every((c) => support(c) === first);
631
- if (!shared) {
632
- refuse(
633
- 'COMPARE_UV_CARRIER_NOT_UNIQUE',
634
- `${who}: the sample at uv (${px}, ${py}) lies in ${hits.length} UV triangles — ${hits.map((c) => `triangle ${c.triangle} (vertices ${c.corners.join(', ')})`).join(' and ')} — that do not meet there at a shared vertex or edge; required one carrier per sample (correction 4: overlapping or folded UV triangles would carry it by an arbitrary one)`,
635
- );
636
- }
637
- return hits[0];
638
- });
639
- }
640
-
641
605
  /** The point a carrier puts the sample at over one pose's world vertices. */
642
606
  function carried(c: Carrier, world: readonly number[]): [number, number] {
643
607
  const [i0, i1, i2] = c.corners;
@@ -852,7 +816,24 @@ function foldingSlots(raw: unknown, who: string): Set<string> {
852
816
  * undercut at the setup pose fails `referenceArtFit`, and `measureMeshQuality`'s own codes for the art).
853
817
  */
854
818
  export function compareMeshesInMotion(input: MotionComparisonInput): MeshQualityReport {
855
- validateInput(input);
819
+ return compareMeshesInMotionWith(input, null);
820
+ }
821
+
822
+ /**
823
+ * `compareMeshesInMotion` with a fault planted in how it carries `motionAmplitude` (`ComparePlant`) — the
824
+ * `mesh-compare` suite's negative controls. Internal: merely exported, so not promised (RELEASING.md *The import
825
+ * surface*).
826
+ */
827
+ export function compareMeshesInMotionPlanted(input: MotionComparisonInput, plant: ComparePlant): MeshQualityReport {
828
+ return compareMeshesInMotionWith(input, plant);
829
+ }
830
+
831
+ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: ComparePlant | null): MeshQualityReport {
832
+ validateInput(input, plant);
833
+ const amplitude = input.motionAmplitude;
834
+ // What the setup measurement is handed: the field as the caller set it — left out as left out, `null` as `null`.
835
+ const handed: { motionAmplitude?: MotionAmplitude | null } =
836
+ plant === 'amplitude-not-carried' || amplitude === undefined || (plant === 'null-read-as-left-out' && amplitude === null) ? {} : { motionAmplitude: amplitude };
856
837
  const refs = input.attachments.map((a) => a.attachment);
857
838
  const reference = readBuild(input.reference, 'reference', refs);
858
839
  const candidates = input.candidates.map((c, i) => readBuild(c, `candidates[${i}]`, refs));
@@ -895,9 +876,14 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
895
876
  influences: null,
896
877
  boneOrder: null,
897
878
  preset: null,
879
+ ...handed,
898
880
  });
899
881
  const c = measured.candidates[0];
900
882
  if (c.geometry === null || c.counts === null) return { geometry: null, counts: null };
883
+ if (plant === 'amplitude-gates-acceptance') {
884
+ const load = c.geometry.rows.find((r) => r.code === 'MQ_DEFORM_LOAD');
885
+ if (load !== undefined && load.state === 'undeclared') c.geometry.verdict = 'fail';
886
+ }
901
887
  sections.push(c.geometry);
902
888
  counts.push(c.counts);
903
889
  }
@@ -1176,6 +1162,7 @@ export function compareMeshesInMotion(input: MotionComparisonInput): MeshQuality
1176
1162
  boneOrder: null,
1177
1163
  schedule,
1178
1164
  budget: null,
1165
+ ...(amplitude === undefined ? (plant === 'echo-when-unset' ? { motionAmplitude: null } : {}) : { motionAmplitude: amplitude }),
1179
1166
  };
1180
1167
  return {
1181
1168
  spec: MESH_QUALITY_REPORT_SPEC,