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.
@@ -71,6 +71,7 @@ import {
71
71
  } from './mesh.ts';
72
72
  import { areaBand, triangleAreas } from './areaband.ts';
73
73
  import { allocationContrast, boundaryNecessityOnce, deformLoad, gradeMax, minAngleP10, type AllocationArt, type AllocationPlant } from './meshallocation.ts';
74
+ import { skinningEchoOf, skinningResidual, validateSkinning, type SkinningDetail, type SkinningEcho, type SkinningPlant, type SkinningResidualInput } from './meshskinning.ts';
74
75
  import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type RegionEdgeReading, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
75
76
  import { cropToSpineY } from './transform.ts';
76
77
 
@@ -369,6 +370,16 @@ export interface MeshMeasureInput {
369
370
  * `null` art bound is declared absent, #1257). It declares no bound: every allocation row stays `undeclared`.
370
371
  */
371
372
  motionAmplitude?: MotionAmplitude | null;
373
+ /**
374
+ * Issue #1294 (§7, mechanism 1): the skinning-envelope residual of this mesh — the candidate — against the original
375
+ * source it was made from, under a declared envelope (`SkinningResidualInput`, `src/meshskinning.ts`). Left out, it
376
+ * is not asked for: no `MQ_SKINNING_RESIDUAL` row and no echo, so the report is the one written without the field,
377
+ * byte for byte. `null` declares it absent: the row is `not-measurable` saying so, and the echo is `null`. Set, the
378
+ * row is measured over §3's samples at `art.threshold` and is required when `maxResidual` is a number; a source or an
379
+ * envelope the measurement cannot read refuses the row by name. Anything that is not a `SkinningResidualInput` in
380
+ * full is refused `REDUCE_INPUT_MISSING` naming its path, before any work.
381
+ */
382
+ skinning?: SkinningResidualInput | null;
372
383
  }
373
384
 
374
385
  /**
@@ -379,10 +390,20 @@ export interface MotionAmplitude {
379
390
  /** One entry per track the caller declares (an animation, a parameter axis — its name is echoed, never read). */
380
391
  tracks: TrackAmplitude[];
381
392
  /**
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.
393
+ * G, px per px — an author's number, read by `MQ_ALLOCATION_CONTRAST` only (issue #1291). Δ asks of each vertex
394
+ * whether the mesh still meets every declared need without it; a need — a silhouette edge of the B\* outline, a
395
+ * deforming triangle sized by θ and ε, a region — allows edges of its own size h at its source and h + G × d at
396
+ * 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
397
+ * coarsen away from what needs density: a larger G lets the need relax sooner, so more dense vertices read as
398
+ * removable and Δ and economy E rise (graded layers read as slack); a smaller G keeps more of them needed, and at 0
399
+ * every point needs the smallest size any source declares, so nothing reads as removable. The value an author
400
+ * declares on a region's `grade` for the same mesh is the same quantity; §8's fixtures used 0.75 because it is the
401
+ * one `grade` any of them declares. Nothing a rig or a reduction declares fixes G — docs/MESH_REDUCTION.md §8
402
+ * measured every candidate derivation, and Δ moves with G on every input there — so none is assumed: left out, it is
403
+ * not declared, and `null` declares it absent; either leaves `MQ_ALLOCATION_CONTRAST` `not-measurable` naming this
404
+ * field, and `MQ_DEFORM_LOAD`, which does not read G, measured. Set, finite and 0 or more.
384
405
  */
385
- gradation: number;
406
+ gradation?: number | null;
386
407
  }
387
408
 
388
409
  /** One track's amplitude: the pairs of bones a share moves between in it, each with θ, and the track's ε. */
@@ -432,6 +453,8 @@ export interface MeasureRow {
432
453
  motion?: MotionRowDetail;
433
454
  /** The five allocation rows only (issue #1280): what the value is a reading of, and what it was read from. */
434
455
  allocation?: AllocationDetail;
456
+ /** `MQ_SKINNING_RESIDUAL` when measured (issue #1294): the source, the samples, the worst sample's terms and the contract's own figures. */
457
+ skinning?: SkinningDetail;
435
458
  }
436
459
 
437
460
  /** Issue #1280: an allocation row's own fields. Written on those five rows, measured or not, and on no other. */
@@ -600,6 +623,8 @@ export interface EffectiveSettings {
600
623
  retriangulate?: 'delaunay';
601
624
  /** A reduction's `removalOrder`, echoed when the input set it (issue #1283); absent otherwise — the order was ascending source index. */
602
625
  removalOrder?: 'deformation-load';
626
+ /** A measurement's `skinning`, echoed when the input set it — `null` included (issue #1294): the source by id and digest. Absent otherwise. */
627
+ skinning?: SkinningEcho | null;
603
628
  }
604
629
 
605
630
  export interface MeshCounts {
@@ -853,6 +878,7 @@ function validateInput(input: MeshMeasureInput): void {
853
878
  refuse('REDUCE_INPUT_MISSING', `${who}: preset is not { name, version }; required that, or null`);
854
879
  }
855
880
  validateMotionAmplitude(who, input.motionAmplitude);
881
+ validateSkinning(who, input.skinning);
856
882
  }
857
883
 
858
884
  /**
@@ -865,8 +891,11 @@ export function validateMotionAmplitude(who: string, amplitude: unknown): void {
865
891
  if (amplitude === undefined || amplitude === null) return;
866
892
  const shape = '{ tracks: [{ track, pairs: [{ bones: [a, b], theta }], epsilon }], gradation }';
867
893
  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`);
894
+ // Issue #1291: `gradation` left out or `null` is accepted — MQ_ALLOCATION_CONTRAST then reads not-measurable naming it.
895
+ if (amplitude.gradation !== undefined && amplitude.gradation !== null && (!isFiniteNumber(amplitude.gradation) || amplitude.gradation < 0)) {
896
+ // A non-finite number is named as itself: JSON would print NaN as null, which this field now accepts.
897
+ const found = typeof amplitude.gradation === 'number' ? String(amplitude.gradation) : JSON.stringify(amplitude.gradation);
898
+ 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
899
  }
871
900
  amplitude.tracks.forEach((track: unknown, i: number) => {
872
901
  const at = `motionAmplitude.tracks[${i}]`;
@@ -1490,6 +1519,15 @@ export function measureMeshQualityPlanted(input: MeshMeasureInput, plant: Alloca
1490
1519
  return measureValidated(input, artRastersOf(input.art), null, plant);
1491
1520
  }
1492
1521
 
1522
+ /**
1523
+ * `measureMeshQuality` with a fault planted in the skinning residual or its report (issue #1294, `SkinningPlant` in
1524
+ * `src/meshskinning.ts`) — the `mesh-compare` suite's negative controls. Internal, as `measureMeshQualityWith` is.
1525
+ */
1526
+ export function measureMeshQualitySkinningPlanted(input: MeshMeasureInput, plant: SkinningPlant): MeshQualityReport {
1527
+ validateInput(input);
1528
+ return measureValidated(input, artRastersOf(input.art), null, null, plant);
1529
+ }
1530
+
1493
1531
  /**
1494
1532
  * `measureMeshQuality` over art rasters the caller already holds
1495
1533
  * (`src/meshrasters.ts`) — what `reduceMesh` calls for every measurement of
@@ -1599,12 +1637,12 @@ function checkArtRasters(input: MeshMeasureInput, rasters: ArtRasters): void {
1599
1637
  }
1600
1638
 
1601
1639
  /** The measurement proper, over an input `validateInput` accepted and rasters taken from its art. */
1602
- function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null): MeshQualityReport {
1640
+ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: StepRasters | null, plant: AllocationPlant | null, skinningPlant: SkinningPlant | null = null): MeshQualityReport {
1603
1641
  rasters.tally.uses++;
1604
1642
  const { attachment, art, source, targets } = input;
1605
1643
  const { mask, threshold, frame } = art;
1606
1644
  const scale = frame.pageScale;
1607
- const effective = effectiveOf(input);
1645
+ const effective = effectiveOf(input, skinningPlant);
1608
1646
  const report = (sourceCounts: MeshCounts | null, candidate: CandidateReport, termination: Termination | null): MeshQualityReport => ({
1609
1647
  spec: MESH_QUALITY_REPORT_SPEC,
1610
1648
  operation: 'measure',
@@ -1641,6 +1679,9 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
1641
1679
  // --- allocation rows (issue #1280): undeclared or not-measurable, never required ------------
1642
1680
  if (plant !== 'omit-rows') rows.push(...allocationRows(input, outline, rasters, steps, plant));
1643
1681
 
1682
+ // --- the skinning residual (issue #1294): only when the input asked for it ------------------
1683
+ if (input.skinning !== undefined || skinningPlant === 'row-when-unset') rows.push(skinningRow(input, rasters, skinningPlant));
1684
+
1644
1685
  // --- raster rows ---------------------------------------------------------
1645
1686
  const w = mask.width;
1646
1687
  const h = mask.height;
@@ -1792,6 +1833,53 @@ function measureValidated(input: MeshMeasureInput, rasters: ArtRasters, steps: S
1792
1833
  return report(counts, { id: input.id, counts, geometry, motion: null, accepted }, null);
1793
1834
  }
1794
1835
 
1836
+ /**
1837
+ * `MQ_SKINNING_RESIDUAL` (issue #1294, `src/meshskinning.ts`): a geometry row in drawing px, required when
1838
+ * `skinning.maxResidual` is a number. `null` for the field reads `not-measurable` saying it was declared absent;
1839
+ * a refusal of the source or the envelope is a `refused` row whose reason opens with its code. The samples are §3's:
1840
+ * art pixel centres at `art.threshold` and the source hull's UVs.
1841
+ */
1842
+ function skinningRow(input: MeshMeasureInput, rasters: ArtRasters, plant: SkinningPlant | null): Built {
1843
+ const { attachment, art } = input;
1844
+ const spec: RowSpec = { code: 'MQ_SKINNING_RESIDUAL', unit: 'px', region: null, attachment };
1845
+ const skinning = input.skinning;
1846
+ if (skinning === undefined || skinning === null) {
1847
+ const why =
1848
+ skinning === null
1849
+ ? `attachment ${nameOf(attachment)}: skinning is null — the residual is declared absent, so there is no source or envelope to measure against`
1850
+ : `attachment ${nameOf(attachment)}: skinning was not set`;
1851
+ return unmeasuredRow(spec, 'not-measurable', why, false);
1852
+ }
1853
+ const bound = skinning.maxResidual === null ? null : { op: '<=' as const, value: skinning.maxResidual };
1854
+ const reading = skinningResidual(
1855
+ {
1856
+ attachment,
1857
+ frame: art.frame,
1858
+ artBits: rasters.artBits(),
1859
+ maskWidth: art.mask.width,
1860
+ maskHeight: art.mask.height,
1861
+ minArtSamples: input.minArtSamples,
1862
+ threshold: art.threshold,
1863
+ candidate: input.source,
1864
+ skinning,
1865
+ },
1866
+ plant,
1867
+ );
1868
+ const sampling = { domain: `art pixel centres at alpha >= ${art.threshold} and the source's hull UVs, on the region UV square, carried by the source's and this mesh's UV triangles`, count: reading.samples };
1869
+ const artField = { threshold: art.threshold, connectivity: null, samples: reading.art };
1870
+ if (reading.state !== 'measured') {
1871
+ const built = unmeasuredRow(spec, reading.state, reading.reason, bound !== null);
1872
+ built.row.art = artField;
1873
+ built.row.sampling = sampling;
1874
+ return built;
1875
+ }
1876
+ const built = measuredRow(spec, reading.value, bound, reading.worst, true);
1877
+ built.row.art = artField;
1878
+ built.row.sampling = sampling;
1879
+ built.row.skinning = reading.detail;
1880
+ return built;
1881
+ }
1882
+
1795
1883
  /** What each allocation row is a reading of — `AllocationDetail.reading`, one fixed sentence per code. */
1796
1884
  const ALLOCATION_READINGS: Record<string, string> = {
1797
1885
  MQ_GRADE:
@@ -1858,7 +1946,7 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1858
1946
  const allocationArt: AllocationArt = { mask: art.mask, bits: rasters.artBits(), scale: art.frame.pageScale };
1859
1947
 
1860
1948
  // MQ_DEFORM_LOAD.
1861
- const loadWhy = amplitudeMissing ?? unweighted;
1949
+ 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
1950
  if (loadWhy !== null || amplitude === undefined || amplitude === null || source.weights === null) {
1863
1951
  row('MQ_DEFORM_LOAD', 'px', missing('MQ_DEFORM_LOAD', 'px', loadWhy ?? ''));
1864
1952
  } else {
@@ -1875,6 +1963,15 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1875
1963
  // MQ_ALLOCATION_CONTRAST.
1876
1964
  const planted = plant === 'contrast-without-amplitude' && (amplitude === undefined || amplitude === null) ? { tracks: [], gradation: 0 } : null;
1877
1965
  const declared = planted ?? amplitude;
1966
+ // Issue #1291: G is the author's; an amplitude without it measures the load and leaves Δ unread, naming the field.
1967
+ const declaredGradation = declared === undefined || declared === null ? undefined : declared.gradation;
1968
+ const gradation = plant === 'gradation-defaulted' && (declaredGradation === undefined || declaredGradation === null) ? 0.75 : declaredGradation;
1969
+ const gradationMissing =
1970
+ declared === undefined || declared === null || (gradation !== undefined && gradation !== null)
1971
+ ? null
1972
+ : gradation === undefined
1973
+ ? `${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)`
1974
+ : `${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
1975
  let regionWhy: string | null = null;
1879
1976
  for (const region of targets.regions) {
1880
1977
  const refusal = regionRefusal(region, art.frame.pageScale, who, steps);
@@ -1883,13 +1980,13 @@ function allocationRows(input: MeshMeasureInput, outline: MeshOutline, rasters:
1883
1980
  break;
1884
1981
  }
1885
1982
  }
1886
- const contrastWhy = (planted === null ? amplitudeMissing : null) ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
1887
- if (contrastWhy !== null || declared === undefined || declared === null || source.weights === null || delta === null) {
1983
+ const contrastWhy = (planted === null ? amplitudeMissing : null) ?? gradationMissing ?? unweighted ?? noDelta ?? fewArt ?? regionWhy;
1984
+ if (contrastWhy !== null || declared === undefined || declared === null || gradation === undefined || gradation === null || source.weights === null || delta === null) {
1888
1985
  row('MQ_ALLOCATION_CONTRAST', 'fraction', missing('MQ_ALLOCATION_CONTRAST', 'fraction', contrastWhy ?? ''));
1889
1986
  } else {
1890
1987
  const ownHull = points.slice(0, outline.hull);
1891
1988
  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);
1989
+ const contrast = allocationContrast(points, source.triangles, outline.hull, source.weights, delta, allocationArt, targets.regions, declared, gradation, silhouette, plant);
1893
1990
  if (contrast.measured) {
1894
1991
  const c = contrast.value;
1895
1992
  row('MQ_ALLOCATION_CONTRAST', 'fraction', measured('MQ_ALLOCATION_CONTRAST', 'fraction', c.contrast, { at: { vertex: c.worstVertex } }), {
@@ -2201,7 +2298,7 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
2201
2298
  // the echo, and the document's text
2202
2299
  // ---------------------------------------------------------------------------
2203
2300
 
2204
- function effectiveOf(input: MeshMeasureInput): EffectiveSettings {
2301
+ function effectiveOf(input: MeshMeasureInput, skinningPlant: SkinningPlant | null = null): EffectiveSettings {
2205
2302
  const { art } = input;
2206
2303
  return {
2207
2304
  preset: input.preset,
@@ -2229,6 +2326,7 @@ function effectiveOf(input: MeshMeasureInput): EffectiveSettings {
2229
2326
  schedule: null,
2230
2327
  budget: null,
2231
2328
  ...(input.motionAmplitude === undefined ? {} : { motionAmplitude: input.motionAmplitude }),
2329
+ ...(input.skinning === undefined ? (skinningPlant === 'echo-when-unset' ? { skinning: null } : {}) : { skinning: skinningEchoOf(input.skinning) }),
2232
2330
  };
2233
2331
  }
2234
2332
 
@@ -2322,6 +2420,7 @@ function effectiveJson(e: EffectiveSettings): Json {
2322
2420
  ...(e.motionAmplitude === undefined ? {} : { motionAmplitude: amplitudeJson(e.motionAmplitude) }),
2323
2421
  ...(e.retriangulate === undefined ? {} : { retriangulate: e.retriangulate }),
2324
2422
  ...(e.removalOrder === undefined ? {} : { removalOrder: e.removalOrder }),
2423
+ ...(e.skinning === undefined ? {} : { skinning: skinningEchoJson(e.skinning) }),
2325
2424
  };
2326
2425
  }
2327
2426
 
@@ -2366,9 +2465,40 @@ function rowJson(r: MeasureRow): Json {
2366
2465
  if (r.sampling !== undefined) out.sampling = { domain: r.sampling.domain, count: r.sampling.count };
2367
2466
  if (r.motion !== undefined) out.motion = motionDetailJson(r.motion);
2368
2467
  if (r.allocation !== undefined) out.allocation = allocationDetailJson(r.allocation);
2468
+ if (r.skinning !== undefined) out.skinning = skinningDetailJson(r.skinning);
2369
2469
  return out;
2370
2470
  }
2371
2471
 
2472
+ function deformJson(t: DeformTimelineInput): Json {
2473
+ return {
2474
+ animation: t.animation,
2475
+ attachment: attachmentJson(t.attachment),
2476
+ keys: t.keys.map((k): Json => (k.kind === 'vertices' ? { time: k.time, kind: k.kind, offset: k.offset, vertices: [...k.vertices] } : { time: k.time, kind: k.kind })),
2477
+ };
2478
+ }
2479
+
2480
+ function skinningEchoJson(e: SkinningEcho | null): Json {
2481
+ if (e === null) return null;
2482
+ return {
2483
+ source: { id: e.source.id, digest: e.source.digest },
2484
+ envelope: { reference: e.envelope.reference, bones: e.envelope.bones.map((b) => ({ bone: b.bone, linear: b.linear, pivot: pair(b.pivot), translation: b.translation })) },
2485
+ maxResidual: e.maxResidual,
2486
+ deform: e.deform.map(deformJson),
2487
+ };
2488
+ }
2489
+
2490
+ function skinningDetailJson(d: SkinningDetail): Json {
2491
+ return {
2492
+ reading: d.reading,
2493
+ source: { id: d.source.id, digest: d.source.digest },
2494
+ reference: d.reference,
2495
+ samples: { measured: d.samples.measured, uncarried: { source: d.samples.uncarried.source.map(pair), candidate: d.samples.uncarried.candidate.map(pair) } },
2496
+ worst: d.worst === null ? null : { covariance: d.worst.covariance, lever: d.worst.lever, bones: d.worst.bones.map((b) => ({ bone: b.bone, covariance: b.covariance, lever: b.lever })) },
2497
+ weightField: { l1: d.weightField.l1, lInf: d.weightField.lInf },
2498
+ setup: { vertexGap: d.setup.vertexGap, vertexSlack: pair(d.setup.vertexSlack), sampleGap: d.setup.sampleGap, weightSumGap: d.setup.weightSumGap },
2499
+ };
2500
+ }
2501
+
2372
2502
  function allocationDetailJson(a: AllocationDetail): Json {
2373
2503
  const out: { [key: string]: Json } = { reading: a.reading };
2374
2504
  if (a.contrast !== undefined) {
@@ -2387,7 +2517,7 @@ function amplitudeJson(a: MotionAmplitude | null): Json {
2387
2517
  if (a === null) return null;
2388
2518
  return {
2389
2519
  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,
2520
+ ...(a.gradation === undefined ? {} : { gradation: a.gradation }),
2391
2521
  };
2392
2522
  }
2393
2523