rig-c 2.28.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-c",
3
- "version": "2.28.0",
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": {
@@ -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 }, () => []);
@@ -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
- validateInput(input);
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,
@@ -379,10 +379,20 @@ export interface MotionAmplitude {
379
379
  /** One entry per track the caller declares (an animation, a parameter axis — its name is echoed, never read). */
380
380
  tracks: TrackAmplitude[];
381
381
  /**
382
- * G, px per px: how fast a declared need relaxes away from its source — the size a need allows at distance d is its
383
- * own plus G × d (the form of a region's `grade`). Read by `MQ_ALLOCATION_CONTRAST` only. Finite, 0 or more.
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.
384
394
  */
385
- gradation: number;
395
+ gradation?: number | null;
386
396
  }
387
397
 
388
398
  /** One track's amplitude: the pairs of bones a share moves between in it, each with θ, and the track's ε. */
@@ -865,8 +875,11 @@ export function validateMotionAmplitude(who: string, amplitude: unknown): void {
865
875
  if (amplitude === undefined || amplitude === null) return;
866
876
  const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
867
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`);
868
- if (!isFiniteNumber(amplitude.gradation) || amplitude.gradation < 0) {
869
- 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`);
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`);
870
883
  }
871
884
  amplitude.tracks.forEach((track: unknown, i: number) => {
872
885
  const at = `motionAmplitude.tracks[${i}]`;
@@ -1858,7 +1871,7 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1858
1871
  const allocationArt: AllocationArt = { mask: art.mask, bits: rasters.artBits(), scale: art.frame.pageScale };
1859
1872
 
1860
1873
  // MQ_DEFORM_LOAD.
1861
- 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);
1862
1875
  if (loadWhy !== null || amplitude === undefined || amplitude === null || source.weights === null) {
1863
1876
  row('MQ_DEFORM_LOAD', 'px', missing('MQ_DEFORM_LOAD', 'px', loadWhy ?? ''));
1864
1877
  } else {
@@ -1875,6 +1888,15 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1875
1888
  // MQ_ALLOCATION_CONTRAST.
1876
1889
  const planted = plant === 'contrast-without-amplitude' && (amplitude === undefined || amplitude === null) ? { tracks: [], gradation: 0 } : null;
1877
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`;
1878
1900
  let regionWhy: string | null = null;
1879
1901
  for (const region of targets.regions) {
1880
1902
  const refusal = regionRefusal(region, art.frame.pageScale, who, steps);
@@ -1883,13 +1905,13 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1883
1905
  break;
1884
1906
  }
1885
1907
  }
1886
- const contrastWhy = (planted === null ? amplitudeMissing : null) ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
1887
- 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) {
1888
1910
  row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', contrastWhy ?? ''));
1889
1911
  } else {
1890
1912
  const ownHull = points.slice(0, outline.hull);
1891
1913
  const silhouette = boundaryNecessityOnce(rasters, ownHull, delta, allocationArt, null).path;
1892
- 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);
1893
1915
  if (contrast.measured) {
1894
1916
  const c = contrast.value;
1895
1917
  row('MQ_ALLOCATION_CONTRAST', 'fraction', measured('MQ_ALLOCATION_CONTRAST', 'fraction', c.contrast, { at: { vertex: c.worstVertex } }), {
@@ -2387,7 +2409,7 @@ function amplitudeJson(a: MotionAmplitude | null): Json {
2387
2409
  if (a === null) return null;
2388
2410
  return {
2389
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 })),
2390
- gradation: a.gradation,
2412
+ ...(a.gradation === undefined ? {} : { gradation: a.gradation }),
2391
2413
  };
2392
2414
  }
2393
2415