spine-rigc 2.45.0 → 2.45.1

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": "spine-rigc",
3
- "version": "2.45.0",
3
+ "version": "2.45.1",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,6 +59,7 @@ import { FOOTPRINT_SLACK, pageFootprint } from '../atlas.ts';
59
59
  import type { AtlasRegionEntry, AtlasRegionFacts } from './facts/atlas_regions.ts';
60
60
  import type { MeshEntry, MeshFacts } from './facts/mesh_attachments.ts';
61
61
  import type { RegionJoinFacts } from './facts/region_joins.ts';
62
+ import { hypot2 } from '../numeric.ts';
62
63
 
63
64
  /** A point in page texels, x right and y down from the page's top-left. */
64
65
  export type PagePoint = readonly [number, number];
@@ -266,7 +267,7 @@ export function piecesOverlap(a: readonly PagePoint[], b: readonly PagePoint[]):
266
267
  const q = poly[(i + 1) % poly.length];
267
268
  const nx = q[1] - p[1];
268
269
  const ny = p[0] - q[0];
269
- const len = Math.hypot(nx, ny);
270
+ const len = hypot2(nx, ny);
270
271
  if (len === 0) continue;
271
272
  let minA = Infinity;
272
273
  let maxA = -Infinity;
package/src/bonedist.ts CHANGED
@@ -85,6 +85,7 @@ import {
85
85
  type Posable,
86
86
  } from './render.ts';
87
87
  import { BONEDIST_SPEC, IDENTITY_CORRESPONDENCE } from './correspondence.ts';
88
+ import { hypot2 } from './numeric.ts';
88
89
 
89
90
  export class BoneDistError extends Error {}
90
91
 
@@ -193,7 +194,7 @@ function skeletonSize(posable: Pick<Posable, 'data'>): SkeletonSize {
193
194
  let size = 0;
194
195
  let farthest: string | null = null;
195
196
  for (const bone of bones) {
196
- const distance = Math.hypot(bone.worldX - root.worldX, bone.worldY - root.worldY);
197
+ const distance = hypot2(bone.worldX - root.worldX, bone.worldY - root.worldY);
197
198
  if (distance > size) {
198
199
  size = distance;
199
200
  farthest = bone.name;
@@ -244,7 +245,7 @@ function deltaOf(
244
245
  const rx = (reference.worldX - referenceRoot.worldX) / rScale;
245
246
  const ry = (reference.worldY - referenceRoot.worldY) / rScale;
246
247
  return {
247
- position: Math.hypot(cx - rx, cy - ry),
248
+ position: hypot2(cx - rx, cy - ry),
248
249
  rotation: Math.abs(wrapDegrees(candidate.rotationX - reference.rotationX)),
249
250
  scale: Math.max(Math.abs(candidate.scaleX - reference.scaleX), Math.abs(candidate.scaleY - reference.scaleY)),
250
251
  linear: Math.max(
package/src/chainfit.ts CHANGED
@@ -106,6 +106,7 @@ import {
106
106
  type Samples,
107
107
  } from './pose.ts';
108
108
  import type { SpineBone, SpineRegionAttachment, SpineSkeletonJson } from './types.ts';
109
+ import { hypot2 } from './numeric.ts';
109
110
 
110
111
  export class ChainFitError extends Error {}
111
112
 
@@ -844,7 +845,7 @@ function solveFromDescendants(points: InwardPoint[]): BonePlace | null {
844
845
  if (!(spread > 1e-12)) return null;
845
846
  const p = dotp / spread;
846
847
  const q = dotq / spread;
847
- const unit = Math.hypot(p, q);
848
+ const unit = hypot2(p, q);
848
849
  if (!(unit > 1e-9)) return null;
849
850
  return {
850
851
  x: mX - p * ma + q * mb,
@@ -991,7 +992,7 @@ function stampCover(into: Uint8Array, width: number, height: number, part: Plate
991
992
  const sin = Math.sin(pl.rotDeg * DEG);
992
993
  const halfW = part.width / 2;
993
994
  const halfH = part.height / 2;
994
- const reach = Math.hypot(halfW, halfH) * pl.scale + 1;
995
+ const reach = hypot2(halfW, halfH) * pl.scale + 1;
995
996
  const x0 = Math.max(0, Math.floor(pl.cx - reach));
996
997
  const x1 = Math.min(width - 1, Math.ceil(pl.cx + reach));
997
998
  const y0 = Math.max(0, Math.floor(pl.cy - reach));
@@ -1665,7 +1666,7 @@ export function estimateChainFit(options: ChainFitOptions): ChainFitReport {
1665
1666
  anchoredTo.set(name, name);
1666
1667
  carried.set(name, []);
1667
1668
  if (chainPlace !== null) {
1668
- pivotDisagreement.set(name, Math.hypot(chainPlace.x - place.x, chainPlace.y - place.y));
1669
+ pivotDisagreement.set(name, hypot2(chainPlace.x - place.x, chainPlace.y - place.y));
1669
1670
  }
1670
1671
  continue;
1671
1672
  }
@@ -1757,7 +1758,7 @@ export function estimateChainFit(options: ChainFitOptions): ChainFitReport {
1757
1758
  let lever = 0;
1758
1759
  for (let i = 0; i < usable.length; i++) {
1759
1760
  for (let j = i + 1; j < usable.length; j++) {
1760
- lever = Math.max(lever, Math.hypot(usable[i].place.x - usable[j].place.x, usable[i].place.y - usable[j].place.y));
1761
+ lever = Math.max(lever, hypot2(usable[i].place.x - usable[j].place.x, usable[i].place.y - usable[j].place.y));
1761
1762
  }
1762
1763
  }
1763
1764
  if (solved === null) {
@@ -1790,8 +1791,8 @@ export function estimateChainFit(options: ChainFitOptions): ChainFitReport {
1790
1791
  return {
1791
1792
  bone: u.bone,
1792
1793
  part: u.part,
1793
- leverPx: roundTo(Math.hypot(u.place.x - solved.x, u.place.y - solved.y), 3),
1794
- offsetPx: roundTo(Math.hypot(px - u.place.x, py - u.place.y), 4),
1794
+ leverPx: roundTo(hypot2(u.place.x - solved.x, u.place.y - solved.y), 3),
1795
+ offsetPx: roundTo(hypot2(px - u.place.x, py - u.place.y), 4),
1795
1796
  carried: u.carried,
1796
1797
  };
1797
1798
  });
package/src/check.ts CHANGED
@@ -98,6 +98,7 @@ import {
98
98
  type TextureSubstitution,
99
99
  type Viewport,
100
100
  } from './render_shared.ts';
101
+ import { hypot2 } from './numeric.ts';
101
102
  // The one name here that is the runtime's: a candidate's pages, read off the shape spine-core's loader returns.
102
103
  import type { Posable } from './render.ts';
103
104
  import {
@@ -3699,7 +3700,7 @@ export function checkLines(report: CheckReport, opts?: { allFrames?: boolean }):
3699
3700
  lines.push(' `⤷ bounded by` line under a drift is what that figure could have reached: for a');
3700
3701
  lines.push(` template match, its whole-pixel winner plus ${SUBPIXEL_CLAMP} px on each axis — so a winner at`);
3701
3702
  lines.push(
3702
- ` the offset you drew bounds the whole figure at ${Math.hypot(SUBPIXEL_CLAMP, SUBPIXEL_CLAMP).toFixed(2)} px ` +
3703
+ ` the offset you drew bounds the whole figure at ${hypot2(SUBPIXEL_CLAMP, SUBPIXEL_CLAMP).toFixed(2)} px ` +
3703
3704
  'and says the drift is the',
3704
3705
  );
3705
3706
  lines.push(' instrument, not the rig. Read every drift against its own line and not against a page.');
package/src/compile.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  } from './depth.ts';
36
36
  import { CompileError, NotImplementedError } from './errors.ts';
37
37
  import { parseJsonWithPosition } from './json-position.ts';
38
+ import { hypot2 } from './numeric.ts';
38
39
  // The "did you mean" list on a missing atlas region, from the one implementation
39
40
  // of it — the same search serves `refuseUnknownKeys`, and a second copy here with
40
41
  // a threshold edited is how such a pair drifts apart.
@@ -7581,7 +7582,7 @@ function checkAxisSelfConsistency(manifest: FaceManifest): void {
7581
7582
  }
7582
7583
  const ex = Math.cos((deg * Math.PI) / 180);
7583
7584
  const ey = Math.sin((deg * Math.PI) / 180);
7584
- if (Math.hypot(unit[0] - ex, unit[1] - ey) > 1e-3) {
7585
+ if (hypot2(unit[0] - ex, unit[1] - ey) > 1e-3) {
7585
7586
  throw new CompileError(
7586
7587
  `manifest axis.unit [${unit[0]}, ${unit[1]}] does not match axis.deg ${deg} (expected [${f32(ex)}, ${f32(ey)}])`,
7587
7588
  );
package/src/deformgen.ts CHANGED
@@ -56,6 +56,7 @@
56
56
  */
57
57
 
58
58
  import { CompileError } from './errors.ts';
59
+ import { hypot2 } from './numeric.ts';
59
60
 
60
61
  /** Which coordinate a bend or a wave reads, and which one it displaces. */
61
62
  export type DeformAxis = 'x' | 'y';
@@ -569,7 +570,7 @@ export function evaluateDeformTransform(
569
570
  let maxOffset = 0;
570
571
  let maxOffsetVertex = 0;
571
572
  for (let v = 0; v < count; v++) {
572
- const m = Math.hypot(offsets[2 * v], offsets[2 * v + 1]);
573
+ const m = hypot2(offsets[2 * v], offsets[2 * v + 1]);
573
574
  if (m > maxOffset) {
574
575
  maxOffset = m;
575
576
  maxOffsetVertex = v;
@@ -26,6 +26,7 @@ import { CoreInputError, underSkin, type CompiledDocument } from './core/index.t
26
26
  import { float32Rows, meshWorld, poseDial as corePoseDial, poseJump, recordIdentity, sliderRecordOf, slotDraw, type CoreSurveyPose } from './core/hooks.ts';
27
27
  import { modelStructure, type SurveyAnimation, type SurveyDeformTimeline, type SurveyMesh, type SurveySkin, type SurveySlider, type SurveyStructure } from './deformstructure.ts';
28
28
  import { areaBand, DEFORM_AREA_EPSILON, float32AreaNoise, stretchSingularValues, triangleAreas } from './areaband.ts';
29
+ import { hypot2 } from './numeric.ts';
29
30
 
30
31
  // The area band lives in `src/areaband.ts` (moved unchanged, issue #1224) so the
31
32
  // geometry entry can read it without reaching the compiler, and
@@ -1433,7 +1434,7 @@ function measurePosed(
1433
1434
  const dy = deformed[v * 2 + 1] - plain[v * 2 + 1];
1434
1435
  if (dx === 0 && dy === 0) continue;
1435
1436
  moved++;
1436
- const distance = Math.hypot(dx, dy);
1437
+ const distance = hypot2(dx, dy);
1437
1438
  if (distance > maxDisplacement) {
1438
1439
  maxDisplacement = distance;
1439
1440
  maxDisplacementVertex = v;
package/src/framing.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  type Frame,
57
57
  type Viewport,
58
58
  } from './render_shared.ts';
59
+ import { hypot2 } from './numeric.ts';
59
60
 
60
61
  /** How far a channel must move for a pixel to count as "not background". */
61
62
  export const BACKGROUND_TOLERANCE = 8;
@@ -511,7 +512,7 @@ function cornerSpread(box: ContentBox, displace: (x: number, y: number) => [numb
511
512
  [right, bottom],
512
513
  ]) {
513
514
  const [dx, dy] = displace(x, y);
514
- worst = Math.max(worst, Math.hypot(dx, dy));
515
+ worst = Math.max(worst, hypot2(dx, dy));
515
516
  }
516
517
  return worst;
517
518
  }
package/src/inspect.ts CHANGED
@@ -70,6 +70,7 @@ import {
70
70
  type Viewport,
71
71
  viewportFor,
72
72
  } from './render_shared.ts';
73
+ import { hypot2 } from './numeric.ts';
73
74
 
74
75
  /** The legend's format tag. */
75
76
  export const INSPECT_SPEC = 'rigc-inspect/1';
@@ -1553,9 +1554,9 @@ function drawTile(
1553
1554
  entry.from = point(a.worldX, a.worldY);
1554
1555
  entry.to = point(b.worldX, b.worldY);
1555
1556
  // Across the whole tile: extend the line past both ends by the tile's diagonal, in world units.
1556
- const reach = Math.hypot(viewport.maxX - viewport.minX, viewport.maxY - viewport.minY);
1557
- const ux = (b.worldX - a.worldX) / Math.hypot(b.worldX - a.worldX, b.worldY - a.worldY);
1558
- const uy = (b.worldY - a.worldY) / Math.hypot(b.worldX - a.worldX, b.worldY - a.worldY);
1557
+ const reach = hypot2(viewport.maxX - viewport.minX, viewport.maxY - viewport.minY);
1558
+ const ux = (b.worldX - a.worldX) / hypot2(b.worldX - a.worldX, b.worldY - a.worldY);
1559
+ const uy = (b.worldY - a.worldY) / hypot2(b.worldX - a.worldX, b.worldY - a.worldY);
1559
1560
  const [p0x, p0y] = toPixel(a.worldX - ux * reach, a.worldY - uy * reach);
1560
1561
  const [p1x, p1y] = toPixel(a.worldX + ux * reach, a.worldY + uy * reach);
1561
1562
  line(tile, tx(p0x) + shiftX, ty(p0y) + shiftY, tx(p1x) + shiftX, ty(p1y) + shiftY, colour('declared'));
package/src/mesh.ts CHANGED
@@ -43,6 +43,7 @@
43
43
  * (assertion A18 recompiles and compares bytes).
44
44
  */
45
45
  import type { ModelBinding, ModelVertices } from './model.ts';
46
+ import { hypot2 } from './numeric.ts';
46
47
 
47
48
  // The mesh-quality measurement and its report (issue #1224) live in their own
48
49
  // module and reach a dependant through this file, which is what
@@ -468,7 +469,7 @@ export function ringControlAngles(
468
469
  const [x, y] = positionOf(name);
469
470
  const dx = x - center[0];
470
471
  const dy = y - center[1];
471
- if (Math.hypot(dx, dy) < 1e-6) {
472
+ if (hypot2(dx, dy) < 1e-6) {
472
473
  throw new MeshError(`control bone "${name}" sits on the aperture centre, so it has no radial direction`);
473
474
  }
474
475
  return (Math.atan2(dy, dx) * 180) / Math.PI;
@@ -755,10 +756,10 @@ export function distanceToSegment(
755
756
  const dx = b[0] - a[0];
756
757
  const dy = b[1] - a[1];
757
758
  const len = dx * dx + dy * dy;
758
- if (len === 0) return Math.hypot(p[0] - a[0], p[1] - a[1]);
759
+ if (len === 0) return hypot2(p[0] - a[0], p[1] - a[1]);
759
760
  let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len;
760
761
  t = Math.max(0, Math.min(1, t));
761
- return Math.hypot(p[0] - (a[0] + dx * t), p[1] - (a[1] + dy * t));
762
+ return hypot2(p[0] - (a[0] + dx * t), p[1] - (a[1] + dy * t));
762
763
  }
763
764
 
764
765
  /**
@@ -809,7 +810,7 @@ export function simplifyClosedPolygon(poly: Array<[number, number]>, tolerance:
809
810
  let far = 1;
810
811
  let farD = -1;
811
812
  for (let i = 1; i < n; i++) {
812
- const d = Math.hypot(poly[i][0] - poly[0][0], poly[i][1] - poly[0][1]);
813
+ const d = hypot2(poly[i][0] - poly[0][0], poly[i][1] - poly[0][1]);
813
814
  if (d > farD) {
814
815
  farD = d;
815
816
  far = i;
@@ -852,7 +853,7 @@ export function offsetPolygon(poly: Array<[number, number]>, margin: number): Ar
852
853
  const n2: [number, number] = [e2[1], -e2[0]];
853
854
  let bx = n1[0] + n2[0];
854
855
  let by = n1[1] + n2[1];
855
- let len = Math.hypot(bx, by);
856
+ let len = hypot2(bx, by);
856
857
  if (len < 1e-9) {
857
858
  // A 180-degree turn: the two edges double back, so there is no bisector.
858
859
  // The incoming edge's own normal is the only direction that is outward for
@@ -870,7 +871,7 @@ export function offsetPolygon(poly: Array<[number, number]>, margin: number): Ar
870
871
  }
871
872
 
872
873
  function unit(dx: number, dy: number): [number, number] {
873
- const len = Math.hypot(dx, dy);
874
+ const len = hypot2(dx, dy);
874
875
  return len < 1e-12 ? [0, 0] : [dx / len, dy / len];
875
876
  }
876
877
 
@@ -1425,7 +1426,7 @@ export function measureContourFit(
1425
1426
  const nx = x + dx;
1426
1427
  if (nx < 0 || nx >= w) continue;
1427
1428
  if (!filled[ny * w + nx]) continue;
1428
- const d = Math.hypot(dx, dy);
1429
+ const d = hypot2(dx, dy);
1429
1430
  if (d < best) best = d;
1430
1431
  }
1431
1432
  }
@@ -35,6 +35,7 @@
35
35
  */
36
36
  import { distanceToSegment, r6, type AlphaMask } from './mesh.ts';
37
37
  import { closedPolygonCentres, inClosedPolygon, regionEdgeBound, type MotionAmplitude, type RefinementRegion } from './meshquality.ts';
38
+ import { hypot2 } from './numeric.ts';
38
39
 
39
40
  type Pt = readonly [number, number];
40
41
  type Weights = ReadonlyArray<ReadonlyArray<{ bone: string; weight: number }>>;
@@ -105,7 +106,7 @@ function uniqueEdges(triangles: readonly number[]): Array<[number, number]> {
105
106
  return out;
106
107
  }
107
108
 
108
- const lengthOf = (a: Pt, b: Pt): number => Math.hypot(b[0] - a[0], b[1] - a[1]);
109
+ const lengthOf = (a: Pt, b: Pt): number => hypot2(b[0] - a[0], b[1] - a[1]);
109
110
 
110
111
  /**
111
112
  * Nearest rank at index round((n − 1) · p) of the values sorted ascending, ties by index — §8's percentile rule —
@@ -530,7 +531,7 @@ function needField(points: readonly Pt[], triangles: readonly number[], weights:
530
531
  const [wa, wb, wc] = [wt(i), wt(j), wt(k)];
531
532
  const gx = ((wb - wa) * (C[1] - A[1]) - (wc - wa) * (B[1] - A[1])) / det;
532
533
  const gy = ((wc - wa) * (B[0] - A[0]) - (wb - wa) * (C[0] - A[0])) / det;
533
- tv += Math.hypot(gx, gy);
534
+ tv += hypot2(gx, gy);
534
535
  }
535
536
  tv /= 2;
536
537
  if (tv <= 0) continue;
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { MeshReductionError } from './mesh.ts';
17
17
  import { areaBand, triangleAreas } from './areaband.ts';
18
+ import { hypot2 } from './numeric.ts';
18
19
 
19
20
  /** `pointInTriangle`'s epsilon in `src/mesh.ts`, applied to each barycentric coordinate. */
20
21
  export const CONTAINS = 1e-9;
@@ -222,7 +223,7 @@ function slackSupport(hits: readonly Carrier[], uvOf: (v: number) => readonly [n
222
223
  const h = [0, 1, 2].map((k) => {
223
224
  const a = p[(k + 1) % 3];
224
225
  const b = p[(k + 2) % 3];
225
- return twice / Math.hypot(b[0] - a[0], b[1] - a[1]);
226
+ return twice / hypot2(b[0] - a[0], b[1] - a[1]);
226
227
  }) as [number, number, number];
227
228
  heights.set(c, h);
228
229
  widest = Math.max(widest, ...h);
@@ -110,6 +110,7 @@ import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts'
110
110
  import { sampleTime } from './core/animation.ts';
111
111
  import { CORE_DEFAULT_SKIN } from './core/skins.ts';
112
112
  import { readVersion } from './package_meta.ts';
113
+ import { hypot2 } from './numeric.ts';
113
114
 
114
115
  // ---------------------------------------------------------------------------
115
116
  // the input — §3, with what the contract left undefined defined here
@@ -664,7 +665,7 @@ function inClosedPolygon(p: readonly [number, number], poly: ReadonlyArray<reado
664
665
  const dy = b[1] - a[1];
665
666
  const len = dx * dx + dy * dy;
666
667
  const t = len === 0 ? 0 : Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len));
667
- if (Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t) <= ON_BOUNDARY) return true;
668
+ if (hypot2(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t) <= ON_BOUNDARY) return true;
668
669
  }
669
670
  let inside = false;
670
671
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
@@ -1082,7 +1083,7 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
1082
1083
  if (!both[j]) continue;
1083
1084
  const p = carried(refCarrier[j]!, rw);
1084
1085
  const q = carried(candCarrier[j]!, cw);
1085
- const d = Math.hypot(q[0] - p[0], q[1] - p[1]);
1086
+ const d = hypot2(q[0] - p[0], q[1] - p[1]);
1086
1087
  if (d > worst) {
1087
1088
  worst = d;
1088
1089
  worstAt = j;
@@ -74,6 +74,7 @@ import { allocationContrast, boundaryNecessityOnce, deformLoad, gradeMax, minAng
74
74
  import { skinningEchoOf, skinningResidual, sourceMeshDigest, validateSkinning, type ReductionSkinning, type SkinningDetail, type SkinningEcho, type SkinningPlant, type SkinningResidualInput } from './meshskinning.ts';
75
75
  import { artRastersOf, type ArtRasters, type CoverageReading, type OutlineMemo, type RegionEdgeReading, type SilhouetteReading, type StepRasters } from './meshrasters.ts';
76
76
  import { cropToSpineY } from './transform.ts';
77
+ import { hypot2 } from './numeric.ts';
77
78
 
78
79
  // ---------------------------------------------------------------------------
79
80
  // §1 — inputs
@@ -1465,12 +1466,12 @@ function nearestParameter(a: Pt, b: Pt, c: Pt, d: Pt): number {
1465
1466
  * to the polygon within the tolerance of `transition`.
1466
1467
  */
1467
1468
  function runsAlongOuterBoundary(a: Pt, b: Pt, poly: readonly Pt[], transition: number): boolean {
1468
- const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
1469
+ const length = hypot2(b[0] - a[0], b[1] - a[1]);
1469
1470
  const n = poly.length;
1470
1471
  for (let i = 0; i < n; i++) {
1471
1472
  const c = poly[i];
1472
1473
  const e = poly[(i + 1) % n];
1473
- const side = Math.hypot(e[0] - c[0], e[1] - c[1]);
1474
+ const side = hypot2(e[0] - c[0], e[1] - c[1]);
1474
1475
  if (side === 0) continue;
1475
1476
  const ux = (e[0] - c[0]) / side;
1476
1477
  const uy = (e[1] - c[1]) / side;
@@ -1525,7 +1526,7 @@ export function edgeIsHeldByRegion(a: readonly [number, number], b: readonly [nu
1525
1526
  if (d < transition - BAND_CONTACT_TOLERANCE) return true;
1526
1527
  if (runsAlongOuterBoundary(a, b, poly, transition)) return true;
1527
1528
  // Where on the edge each polygon side is touched: one point, or several apart.
1528
- const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
1529
+ const length = hypot2(b[0] - a[0], b[1] - a[1]);
1529
1530
  let first = Infinity;
1530
1531
  let last = -Infinity;
1531
1532
  const n = poly.length;
@@ -2548,7 +2549,7 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
2548
2549
  for (let i = 0; i < encoded.length; i += 2) {
2549
2550
  const a = encoded[i] / 2;
2550
2551
  const b = encoded[i + 1] / 2;
2551
- edges.push({ a: Math.min(a, b), b: Math.max(a, b), length: Math.hypot(points[b][0] - points[a][0], points[b][1] - points[a][1]) });
2552
+ edges.push({ a: Math.min(a, b), b: Math.max(a, b), length: hypot2(points[b][0] - points[a][0], points[b][1] - points[a][1]) });
2552
2553
  }
2553
2554
  edges.sort((p, q) => p.a - q.a || p.b - q.b);
2554
2555
 
@@ -2688,14 +2689,14 @@ function regionRows(input: MeshMeasureInput, outline: MeshOutline, hullPolygon:
2688
2689
  };
2689
2690
  const distance = (i: number, p: Pt): number => {
2690
2691
  const centre = centreOf(i);
2691
- return Math.hypot(p[0] - centre[0], p[1] - centre[1]);
2692
+ return hypot2(p[0] - centre[0], p[1] - centre[1]);
2692
2693
  };
2693
2694
  if (steps === null) {
2694
2695
  for (const i of regionPixels) {
2695
2696
  const centre = centreOf(i);
2696
2697
  if (!inClosedPolygon(centre, hullPolygon)) continue;
2697
2698
  let nearest = Infinity;
2698
- for (const p of points) nearest = Math.min(nearest, Math.hypot(p[0] - centre[0], p[1] - centre[1]));
2699
+ for (const p of points) nearest = Math.min(nearest, hypot2(p[0] - centre[0], p[1] - centre[1]));
2699
2700
  visit(i, nearest);
2700
2701
  }
2701
2702
  } else {
@@ -58,6 +58,7 @@ import { MeshReductionError, r6 } from './mesh.ts';
58
58
  import { areaBand, triangleAreas } from './areaband.ts';
59
59
  import { carrierIn, resolveCarriers, uvCarriers, type Carrier } from './meshcarriers.ts';
60
60
  import type { AttachmentRef, DeformTimelineInput, SourceFrame, SourceMesh, WorstSample } from './meshquality.ts';
61
+ import { hypot2 } from './numeric.ts';
61
62
 
62
63
  type Pt = readonly [number, number];
63
64
 
@@ -407,8 +408,8 @@ export function sampleResidual(
407
408
  const dw = Math.abs(c.wbar[k] - r.wbar[k]);
408
409
  l1 += dw;
409
410
  lInf = Math.max(lInf, dw);
410
- if (plant !== 'drop-covariance') cov[k] = eps[k] * Math.hypot(c.moment[k][0] - r.moment[k][0], c.moment[k][1] - r.moment[k][1]);
411
- if (plant !== 'drop-lever') lev[k] = dw * (eps[k] * Math.hypot(r.p[0] - pivot[k][0], r.p[1] - pivot[k][1]) + tau[k]);
411
+ if (plant !== 'drop-covariance') cov[k] = eps[k] * hypot2(c.moment[k][0] - r.moment[k][0], c.moment[k][1] - r.moment[k][1]);
412
+ if (plant !== 'drop-lever') lev[k] = dw * (eps[k] * hypot2(r.p[0] - pivot[k][0], r.p[1] - pivot[k][1]) + tau[k]);
412
413
  total += cov[k] + lev[k];
413
414
  }
414
415
  return { total, cov, lev, l1, lInf };
@@ -539,7 +540,7 @@ export function skinningContract(args: SkinningMeasureArgs, plant: SkinningPlant
539
540
  for (let v = 0; v < mesh.points.length; v++) {
540
541
  const dx = Math.abs(mesh.points[v][0] - mesh.uvs[v * 2] * frame.width);
541
542
  const dy = Math.abs(mesh.points[v][1] - mesh.uvs[v * 2 + 1] * frame.height);
542
- vertexGap = Math.max(vertexGap, Math.hypot(dx, dy));
543
+ vertexGap = Math.max(vertexGap, hypot2(dx, dy));
543
544
  if ((dx > slack[0] || dy > slack[1]) && plant !== 'setup-unchecked') {
544
545
  return out(
545
546
  'refused',
@@ -601,7 +602,7 @@ export function skinningResidual(args: SkinningMeasureArgs, plant: SkinningPlant
601
602
  measured++;
602
603
  const r = termsAt(source, cs, index);
603
604
  const c = termsAt(candidate, cc, index);
604
- sampleGap = Math.max(sampleGap, Math.hypot(c.p[0] - r.p[0], c.p[1] - r.p[1]));
605
+ sampleGap = Math.max(sampleGap, hypot2(c.p[0] - r.p[0], c.p[1] - r.p[1]));
605
606
  const { total, cov, lev, l1, lInf } = sampleResidual(r, c, eps, tau, pivot, plant);
606
607
  l1Max = Math.max(l1Max, l1);
607
608
  lInfMax = Math.max(lInfMax, lInf);
@@ -767,7 +768,7 @@ function envelopeBoneWith(ranges: EnvelopeBoneRanges, plant: EnvelopePlant | nul
767
768
  for (let j = below.length - 2; j >= 0; j--) {
768
769
  if (plant === 'translation-ignored') break;
769
770
  const x = below[j];
770
- delta = eOf(x) * Math.hypot(c[0] - x.pivot[0], c[1] - x.pivot[1]) + (1 + eOf(x)) * delta + x.translate;
771
+ delta = eOf(x) * hypot2(c[0] - x.pivot[0], c[1] - x.pivot[1]) + (1 + eOf(x)) * delta + x.translate;
771
772
  }
772
773
  return { bone: (ranges.chain[ranges.chain.length - 1] as BoneMotionRange).bone, linear: S * composed, pivot: [c[0], c[1]], translation: S * delta };
773
774
  }
package/src/numeric.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The length of a 2-D vector, spelled so that every platform returns the same bits
3
+ * (issue #1377).
4
+ *
5
+ * `Math.sqrt(dx * dx + dy * dy)` and not `Math.hypot(dx, dy)`. IEEE 754 requires
6
+ * `*`, `+` and `sqrt` to be correctly rounded, so this expression has one answer
7
+ * for one input wherever it runs; `Math.hypot` carries no such requirement, and
8
+ * Bun 1.4.2's was measured returning 34.27192399143071 on macOS and
9
+ * 34.27192399143072 on Linux for the same bit-identical input (rig-c/parts#276).
10
+ * One ulp is enough to turn a strict win into a tie, move a value across a
11
+ * rounding grid, or reorder a sort — each of which this tree writes to disk.
12
+ *
13
+ * What it gives up is `hypot`'s guard against intermediate overflow and
14
+ * underflow: the squares overflow above about 1.3e154 and lose precision below
15
+ * about 1.5e-154. Every length measured here is in pixels, world units or UV
16
+ * units, many orders of magnitude inside both.
17
+ *
18
+ * 🔒 `CUR128` in `selftest.ts` holds `src/` and every module `src/` imports from
19
+ * outside itself free of `Math.hypot`, naming the file and symbol of any call.
20
+ */
21
+ export function hypot2(dx: number, dy: number): number {
22
+ return Math.sqrt(dx * dx + dy * dy);
23
+ }
package/src/pose.ts CHANGED
@@ -54,6 +54,7 @@
54
54
  import { existsSync, readdirSync, statSync } from 'node:fs';
55
55
  import { basename, join, resolve } from 'node:path';
56
56
  import { Plate, readPlate } from '../tools/plate.ts';
57
+ import { hypot2 } from './numeric.ts';
57
58
  // The rasteriser's sampler rather than a second one written here: how a pixel is
58
59
  // read between two pixel centres is exactly the kind of thing two
59
60
  // implementations drift on.
@@ -1249,7 +1250,7 @@ function localMinima(field: CoarseField, radius: number, keep: number): Candidat
1249
1250
  const accepted: Candidate[] = [];
1250
1251
  for (const cand of found) {
1251
1252
  if (accepted.length >= keep) break;
1252
- if (accepted.some((a) => Math.hypot(a.cx - cand.cx, a.cy - cand.cy) <= radius * field.stride)) continue;
1253
+ if (accepted.some((a) => hypot2(a.cx - cand.cx, a.cy - cand.cy) <= radius * field.stride)) continue;
1253
1254
  accepted.push(cand);
1254
1255
  }
1255
1256
  return accepted;
@@ -1383,7 +1384,7 @@ function dedupe(sorted: Candidate[], within: number, degrees: number, scaleRatio
1383
1384
  for (const cand of sorted) {
1384
1385
  const same = out.some(
1385
1386
  (o) =>
1386
- Math.hypot(o.cx - cand.cx, o.cy - cand.cy) <= within &&
1387
+ hypot2(o.cx - cand.cx, o.cy - cand.cy) <= within &&
1387
1388
  Math.abs(normaliseDegrees(o.rotDeg - cand.rotDeg)) <= degrees &&
1388
1389
  Math.abs(Math.log(o.scale / cand.scale)) <= Math.log(scaleRatio),
1389
1390
  );
@@ -2126,7 +2127,7 @@ function placePart(
2126
2127
  for (const m of measured) {
2127
2128
  const same = distinct.some(
2128
2129
  (d) =>
2129
- Math.hypot(d.placement.x - m.placement.x, d.placement.y - m.placement.y) <= Math.max(1.5, 0.03 * span * m.placement.scale) &&
2130
+ hypot2(d.placement.x - m.placement.x, d.placement.y - m.placement.y) <= Math.max(1.5, 0.03 * span * m.placement.scale) &&
2130
2131
  Math.abs(normaliseDegrees(d.placement.rotationDeg - m.placement.rotationDeg)) <= 5 &&
2131
2132
  Math.abs(Math.log(d.placement.scale / m.placement.scale)) <= Math.log(1.05),
2132
2133
  );
package/src/slots.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  type Footprint,
56
56
  type Viewport,
57
57
  } from './render_shared.ts';
58
+ import { hypot2 } from './numeric.ts';
58
59
 
59
60
  /** Components smaller than this are antialiasing crumbs, not parts. */
60
61
  const MIN_COMPONENT_PIXELS = 4;
@@ -293,7 +294,7 @@ export interface SlotTrack {
293
294
  */
294
295
  export function driftBound(track: SlotTrack): number | null {
295
296
  if (track.method !== 'template' || track.wholePixel === null) return null;
296
- return Math.hypot(Math.abs(track.wholePixel.dx) + SUBPIXEL_CLAMP, Math.abs(track.wholePixel.dy) + SUBPIXEL_CLAMP);
297
+ return hypot2(Math.abs(track.wholePixel.dx) + SUBPIXEL_CLAMP, Math.abs(track.wholePixel.dy) + SUBPIXEL_CLAMP);
297
298
  }
298
299
 
299
300
  /**
@@ -411,7 +412,7 @@ export function matchSlots(
411
412
  // a guess, so it is bounded by what this slot could plausibly have moved, and
412
413
  // a rival about as near makes it a guess between two things.
413
414
  const ranked = components
414
- .map((c) => ({ c, d: Math.hypot(c.cx - foot.cx, c.cy - foot.cy) }))
415
+ .map((c) => ({ c, d: hypot2(c.cx - foot.cx, c.cy - foot.cy) }))
415
416
  .sort((a, b) => a.d - b.d);
416
417
  const nearest = ranked[0];
417
418
  if (nearest.d > radius) {
@@ -519,7 +520,7 @@ function fillComponentMatch(track: SlotTrack, foot: Footprint, component: Compon
519
520
  };
520
521
  track.driftX = component.cx - foot.cx;
521
522
  track.driftY = component.cy - foot.cy;
522
- track.drift = Math.hypot(track.driftX, track.driftY);
523
+ track.drift = hypot2(track.driftX, track.driftY);
523
524
  track.widthDrift = foot.maxX - foot.minX - track.reference.width;
524
525
  track.heightDrift = foot.maxY - foot.minY - track.reference.height;
525
526
  }
@@ -836,12 +837,12 @@ function applyTemplateMatch(track: SlotTrack, foot: Footprint, source: SlotSourc
836
837
  const gap = Math.max(RIVAL_GAP, coarse);
837
838
  let rival = Infinity;
838
839
  for (const { dx, dy, s } of sweep) {
839
- if (Math.hypot(dx - bestX, dy - bestY) < gap) continue;
840
+ if (hypot2(dx - bestX, dy - bestY) < gap) continue;
840
841
  if (s < rival) rival = s;
841
842
  }
842
843
  const confidence = Number.isFinite(rival) && rival > 0 ? Math.max(0, Math.min(1, 1 - best / rival)) : 0;
843
844
 
844
- const reach = Math.min(1, Math.hypot(bestX, bestY) / radius);
845
+ const reach = Math.min(1, hypot2(bestX, bestY) / radius);
845
846
  const required = MIN_CONFIDENCE + reach * CONFIDENCE_SLOPE;
846
847
  if (best > template.contrast * MAX_RESIDUAL_FRACTION || confidence < required) {
847
848
  track.method = 'none';
@@ -851,7 +852,7 @@ function applyTemplateMatch(track: SlotTrack, foot: Footprint, source: SlotSourc
851
852
  `${radius} px either (best residual ${best.toFixed(1)} against its own ${template.contrast.toFixed(1)} of ` +
852
853
  `contrast over the ${template.ink - template.covered} of its ${template.ink} px that reach the picture; ` +
853
854
  `confidence ${(Number.isFinite(confidence) ? confidence : 0).toFixed(2)} where ` +
854
- `${required.toFixed(2)} is needed ${Math.hypot(bestX, bestY).toFixed(1)} px out)`;
855
+ `${required.toFixed(2)} is needed ${hypot2(bestX, bestY).toFixed(1)} px out)`;
855
856
  return;
856
857
  }
857
858
 
@@ -861,7 +862,7 @@ function applyTemplateMatch(track: SlotTrack, foot: Footprint, source: SlotSourc
861
862
  track.reference = null;
862
863
  track.driftX = dx;
863
864
  track.driftY = dy;
864
- track.drift = Math.hypot(dx, dy);
865
+ track.drift = hypot2(dx, dy);
865
866
  track.widthDrift = null;
866
867
  track.heightDrift = null;
867
868
  track.confidence = confidence;
package/tools/plate.ts CHANGED
@@ -16,6 +16,7 @@ import { deflateSync, inflateSync } from 'node:zlib';
16
16
  import { readFileSync, writeFileSync } from 'node:fs';
17
17
  import { drawText, textWidth } from './font5x7.ts';
18
18
  import { assertPng, NotAPngError } from '../src/png.ts';
19
+ import { hypot2 } from '../src/numeric.ts';
19
20
 
20
21
  export type RGBA = [number, number, number, number];
21
22
 
@@ -88,7 +89,7 @@ export class Plate {
88
89
  }
89
90
 
90
91
  line(x0: number, y0: number, x1: number, y1: number, t: number, colour: RGBA): void {
91
- const steps = Math.ceil(Math.hypot(x1 - x0, y1 - y0)) * 2 + 1;
92
+ const steps = Math.ceil(hypot2(x1 - x0, y1 - y0)) * 2 + 1;
92
93
  for (let s = 0; s <= steps; s++) {
93
94
  const u = s / steps;
94
95
  const cx = x0 + (x1 - x0) * u;
@@ -100,7 +101,7 @@ export class Plate {
100
101
  disc(cx: number, cy: number, r: number, colour: RGBA): void {
101
102
  for (let y = Math.floor(cy - r); y <= Math.ceil(cy + r); y++) {
102
103
  for (let x = Math.floor(cx - r); x <= Math.ceil(cx + r); x++) {
103
- if (Math.hypot(x - cx, y - cy) <= r) this.blend(x, y, colour);
104
+ if (hypot2(x - cx, y - cy) <= r) this.blend(x, y, colour);
104
105
  }
105
106
  }
106
107
  }
@@ -108,7 +109,7 @@ export class Plate {
108
109
  ring(cx: number, cy: number, r: number, t: number, colour: RGBA): void {
109
110
  for (let y = Math.floor(cy - r - t); y <= Math.ceil(cy + r + t); y++) {
110
111
  for (let x = Math.floor(cx - r - t); x <= Math.ceil(cx + r + t); x++) {
111
- const d = Math.hypot(x - cx, y - cy);
112
+ const d = hypot2(x - cx, y - cy);
112
113
  if (Math.abs(d - r) <= t) this.blend(x, y, colour);
113
114
  }
114
115
  }