rig-c 2.27.0 → 2.28.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.27.0",
3
+ "version": "2.28.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": {
@@ -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
  /**
@@ -584,7 +594,7 @@ export interface EffectiveSettings {
584
594
  stopAfterAccepted?: number;
585
595
  /** A reduction's `boundaryRuns`, echoed when the input set it (issue #1279); absent otherwise. */
586
596
  boundaryRuns?: BoundaryRuns;
587
- /** A measurement's `motionAmplitude`, echoed when the input set it — `null` included (issue #1280); absent otherwise. */
597
+ /** A measurement's or a reduction's `motionAmplitude`, echoed when the input set it — `null` included (issues #1280, #1287); absent otherwise. */
588
598
  motionAmplitude?: MotionAmplitude | null;
589
599
  /** A reduction's `retriangulate`, echoed when the input set it (issue #1283); absent otherwise. */
590
600
  retriangulate?: 'delaunay';
@@ -842,11 +852,16 @@ function validateInput(input: MeshMeasureInput): void {
842
852
  if (input.preset !== null && (!isObject(input.preset) || typeof input.preset.name !== 'string' || typeof input.preset.version !== 'string')) {
843
853
  refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
844
854
  }
845
- validateAmplitude(who, input.motionAmplitude);
855
+ validateMotionAmplitude(who, input.motionAmplitude);
846
856
  }
847
857
 
848
- /** Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in full. */
849
- function validateAmplitude(who: string, amplitude: unknown): void {
858
+ /**
859
+ * Issue #1280: `motionAmplitude` left out or `null` is accepted as it stands; anything else is a `MotionAmplitude` in
860
+ * full, else refused `REDUCE_INPUT_MISSING` naming the path. `reduceMesh` refuses its own field with it before any work
861
+ * (issue #1287), so a reduction and a measurement refuse one value in the same words. Internal, as
862
+ * `measureMeshQualityWith` is: on `rig-c/mesh` only through `export *`.
863
+ */
864
+ export function validateMotionAmplitude(who: string, amplitude: unknown): void {
850
865
  if (amplitude === undefined || amplitude === null) return;
851
866
  const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
852
867
  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`);
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
- /** A measurement of a canonical mesh against the result's full contract. */
795
- function measureAgainstTargets(run: Run, mesh: SourceMesh, id: string): MeshQualityReport {
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 blocking = firstBlockingRow(measureAgainstTargets(run, canon.mesh, 'candidate'), run.input.targets.artFit);
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]>): EffectiveSettings {
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
  };