Distances between tracks, pads and zones, point-in-shape tests, areas and pad positions on a printed circuit board.
How the precision is kept
Plain 64-bit geometry in the board's own unit. Orientation tests compare a cross product with zero directly, without exact predicates, so shapes that touch or are collinear to within rounding can go either way. Pad positions are rotated with the cosine and sine of an angle given in degrees.
Open item. Shift a polygon to a local origin before the shoelace sum.
Calculations under this policy
10 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
geom.jsShared shell, every app64-bit9 calculationsReviewed
The distance from a point to a track center line or a polygon edge.
t = clamp(((p − a) · (b − a)) ÷ |b − a|², 0, 1); d = |p − (a + t (b − a))|
One projection and Math.hypot. A zero-length segment falls back to the point distance.
geom.js
lines 4 to 7
export function pointSegDist(p, a, b) {
const dx = b[0] - a[0], dy = b[1] - a[1], l2 = dx * dx + dy * dy; if (l2 === 0) return dist(p, a);
const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l2)); return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
}
Orientation of three points
64-bit
Which side of a line a point lies on.
o = (b − a) × (c − a)
An ordinary 64-bit cross product: two products of coordinate differences subtracted. For three nearly collinear points the sign can be wrong, because the subtraction cancels and no exact arithmetic backs it up.
cross when the orientation signs differ on both segments; touch when an orientation is 0 and the point lies in the other's box
The collinear and touching cases are found by comparing the orientation values with 0 exactly. Nearly collinear or just-touching segments are therefore decided by rounding; the distance functions built on this then return 0 or a tiny positive number accordingly.
The distance between two track center lines or edges.
0 when they cross; else the least of four point-to-segment distances
For segments that do not cross, the least of the four end-point distances is the distance between them, each computed in 64-bit.
geom.js
line 15
export function segSegDist(a, b, c, d) { if (segsIntersect(a, b, c, d)) return 0; return Math.min(pointSegDist(a, c, d), pointSegDist(b, c, d), pointSegDist(c, a, b), pointSegDist(d, a, b)); }
Point in polygon
64-bit
Whether a point lies inside a zone, a pad or the board outline.
even-odd rule along a ray in +x
The crossing abscissa is computed with one division per edge. A point on an edge or level with a vertex is assigned by the > and < comparisons, with no tolerance.
geom.js
lines 16 to 20
export function pointInPoly(p, poly) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { const a = poly[i], b = poly[j]; if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]) inside = !inside; }
return inside;
}
Polygon area
64-bit
The signed area of a closed polygon.
A = ½ Σ (xᵢ yᵢ₊₁ − xᵢ₊₁ yᵢ)
The shoelace sum on absolute coordinates. Each term has the size of the distance from the origin squared while the result has the size of the shape squared, so the relative error is about 1e-16 × (distance ÷ size)².
geom.js
line 21
export const polyArea = (poly) => { let s = 0; for (let i = 0; i < poly.length; i++) { const a = poly[i], b = poly[(i + 1) % poly.length]; s += a[0] * b[1] - b[0] * a[1]; } return s / 2; };
Polygon to polygon distance
64-bit
The distance between two closed shapes.
0 when one first vertex is inside the other; else the least edge-to-edge distance
Containment is tested with one vertex of each shape; outlines that cross give a zero edge distance in the loop that follows. Every pair of edges is compared.
geom.js
lines 24 to 28
/** Distance between two closed polygons (0 if they touch, cross or one contains the other). */
export function polyPolyDist(P, Q) {
if (pointInPoly(P[0], Q) || pointInPoly(Q[0], P)) return 0;
let m = Infinity; for (const [a, b] of polyEdges(P)) for (const [c, d] of polyEdges(Q)) { const v = segSegDist(a, b, c, d); if (v < m) m = v; if (m === 0) return 0; } return m;
}
Gap between copper shapes
64-bit
The edge-to-edge clearance between two tracks, pads or zones.
gap = max(0, center-line distance − r₁ − r₂)
Tracks and round pads are stadiums: the gap is the distance between center lines less the two half-widths, clamped at 0. For shapes that nearly touch, the gap is a small difference of larger numbers: its absolute error stays near 1e-16 times the distance, but few of its own digits are left.
geom.js
lines 32 to 38
/** A copper/solid primitive: {k:'cap', a, b, r} (a stadium; a = b is a circle) or {k:'poly', pts, r:0}. Gap between two primitives (edge to edge, negative never: 0 when touching). */
export function gap(s, t) {
if (s.k === 'cap' && t.k === 'cap') return Math.max(0, segSegDist(s.a, s.b, t.a, t.b) - s.r - t.r);
if (s.k === 'cap' && t.k === 'poly') return Math.max(0, segPolyDist(s.a, s.b, t.pts) - s.r);
if (s.k === 'poly' && t.k === 'cap') return Math.max(0, segPolyDist(t.a, t.b, s.pts) - t.r);
return polyPolyDist(s.pts, t.pts);
}
Rotation of a point
64-bit
A pad or outline point turned by an angle in degrees.
(x cos θ − y sin θ, x sin θ + y cos θ), θ = degrees × π ÷ 180
The angle is converted to radians and passed to Math.cos and Math.sin. For 90°, 180° and 270° the results are not 0 or ±1 to the last bit (cos 90° comes out as 6.1e-17), so right-angle rotations leave a residue of that size times the coordinate.
geom.js
line 42
export const rotPt = (p, deg) => { const c = Math.cos(deg * Math.PI / 180), s = Math.sin(deg * Math.PI / 180); return [p[0] * c - p[1] * s, p[0] * s + p[1] * c]; };
board.jsShared shell, every app64-bit1 calculationReviewed
Where a footprint pad sits on the board, with its shape for the clearance check.
world = part position + R(part rotation) × (±x, y)
A bottom-side part mirrors x first. An oval pad becomes a stadium whose half-length is (L − W) ÷ 2 along the pad's axis. Rounded-rectangle pads are checked as full rectangles, which the code comment calls conservative.
board.js
lines 33 to 43
/** World placement of a pad: {x, y, shape, w, h, rot, drill, layers[], net, ref, num, poly (outline for rect-like), cap (for round/oval)}. */
padWorld(part, pad) {
const bottom = part.side === 'bottom', lx = bottom ? -pad.x : pad.x, r = rotPt([lx, pad.y], part.rot), x = part.x + r[0], y = part.y + r[1];
const rot = part.rot + (bottom ? -(pad.rot || 0) : (pad.rot || 0)), thru = (pad.drill || 0) > 0;
const layers = thru ? this.layers.slice() : [bottom ? this.layers[this.layers.length - 1] : this.layers[0]];
const out = { x, y, shape: pad.shape, w: pad.w, h: pad.h, rot, drill: pad.drill || 0, plated: pad.plated !== false, layers, net: part.padNets[pad.num] || '', ref: part.ref, num: pad.num, smd: !thru };
if (pad.shape === 'circle') out.prim = { k: 'cap', a: [x, y], b: [x, y], r: pad.w / 2 };
else if (pad.shape === 'oval') { const L = Math.max(pad.w, pad.h), W = Math.min(pad.w, pad.h), ang = (pad.w >= pad.h ? rot : rot + 90) * Math.PI / 180, d = (L - W) / 2; out.prim = { k: 'cap', a: [x - d * Math.cos(ang), y - d * Math.sin(ang)], b: [x + d * Math.cos(ang), y + d * Math.sin(ang)], r: W / 2 }; }
else out.prim = { k: 'poly', pts: rectPoly(x, y, pad.w, pad.h, rot), r: 0 }; // rect and roundrect (corner radius ignored: checked as the full rectangle, which is conservative)
return out;
}
In detail
Method
Point-to-segment distance by clamped projection; segment crossing by four orientation signs; even-odd ray test; shoelace area; pad placement by mirror, rotation and translation.
Number types
64-bit numbers.
Tolerances
None in these functions: zero is compared exactly.
Limits
The shoelace area uses absolute coordinates, so a small shape far from the origin loses digits (about 1e-16 × (distance ÷ size)²). A rotation by 90° leaves a residue near 6e-17 times the pad offset, because cos(90°) does not come out as 0.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.