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 +1 -1
- package/src/assertions/footprints.ts +2 -1
- package/src/bonedist.ts +3 -2
- package/src/chainfit.ts +7 -6
- package/src/check.ts +2 -1
- package/src/compile.ts +2 -1
- package/src/deformgen.ts +2 -1
- package/src/deformsurvey.ts +2 -1
- package/src/framing.ts +2 -1
- package/src/inspect.ts +4 -3
- package/src/mesh.ts +8 -7
- package/src/meshallocation.ts +3 -2
- package/src/meshcarriers.ts +2 -1
- package/src/meshcompare.ts +3 -2
- package/src/meshquality.ts +7 -6
- package/src/meshskinning.ts +6 -5
- package/src/numeric.ts +23 -0
- package/src/pose.ts +4 -3
- package/src/slots.ts +8 -7
- package/tools/plate.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.45.
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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,
|
|
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,
|
|
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(
|
|
1794
|
-
offsetPx: roundTo(
|
|
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 ${
|
|
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 (
|
|
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 =
|
|
573
|
+
const m = hypot2(offsets[2 * v], offsets[2 * v + 1]);
|
|
573
574
|
if (m > maxOffset) {
|
|
574
575
|
maxOffset = m;
|
|
575
576
|
maxOffsetVertex = v;
|
package/src/deformsurvey.ts
CHANGED
|
@@ -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 =
|
|
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,
|
|
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 =
|
|
1557
|
-
const ux = (b.worldX - a.worldX) /
|
|
1558
|
-
const uy = (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 (
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1429
|
+
const d = hypot2(dx, dy);
|
|
1429
1430
|
if (d < best) best = d;
|
|
1430
1431
|
}
|
|
1431
1432
|
}
|
package/src/meshallocation.ts
CHANGED
|
@@ -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 =>
|
|
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 +=
|
|
534
|
+
tv += hypot2(gx, gy);
|
|
534
535
|
}
|
|
535
536
|
tv /= 2;
|
|
536
537
|
if (tv <= 0) continue;
|
package/src/meshcarriers.ts
CHANGED
|
@@ -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 /
|
|
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);
|
package/src/meshcompare.ts
CHANGED
|
@@ -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 (
|
|
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 =
|
|
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;
|
package/src/meshquality.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
|
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,
|
|
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 {
|
package/src/meshskinning.ts
CHANGED
|
@@ -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] *
|
|
411
|
-
if (plant !== 'drop-lever') lev[k] = dw * (eps[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,
|
|
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,
|
|
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) *
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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 (
|
|
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,
|
|
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 ${
|
|
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 =
|
|
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(
|
|
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 (
|
|
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 =
|
|
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
|
}
|