Decides whether a point is inside an outline, whether an outline crosses itself, and whether a cut-out sits fully inside its area and clear of the other cut-outs.
How the precision is kept
These are yes-or-no decisions made in 64-bit arithmetic with tolerances, not exact predicates. Arcs are first broken into chords of at most 5°, so a decision about an arc is a decision about its chord polygon. No quantity is taken from the chords.
Calculations under this policy
6 calculations in 1 source file. 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.
Replaces each arc of a run or outline by points on the arc, at most one step apart.
k = max(2, ⌈|θ| ÷ step⌉); pⱼ = center + r·(cos tⱼ, sin tⱼ), tⱼ = a₀ + θ·j ÷ k
Each arc gets k = max(2, ceil(|sweep| ÷ step)) equal pieces; the step is 5° unless the caller passes another. Points are center + r·(cos t, sin t) with t = a0 + sweep·j ÷ k, so the inner points are on the circle to rounding and the arc's own end points are copied, not recomputed. The chord polygon lies inside the arc.
geom.js
lines 68 to 82
TakeoffCraft 0.8.1
/** Points along the outline/run with arcs broken into chords of at most `step` radians. */
export function flatten(pts, closed = false, step = Math.PI / 36) {
const out = [];
const n = pts.length, segs = closed ? n : n - 1;
for (let i = 0; i < segs; i++) {
const p = pts[i], q = pts[(i + 1) % n];
out.push({ x: p.x, y: p.y });
const a = arcOf(p, q, p.b);
if (!a) continue;
const k = Math.max(2, Math.ceil(Math.abs(a.sweep) / step));
for (let j = 1; j < k; j++) { const t = a.a0 + a.sweep * j / k; out.push({ x: a.cx + a.r * Math.cos(t), y: a.cy + a.r * Math.sin(t) }); }
}
if (!closed && n) out.push({ x: pts[n - 1].x, y: pts[n - 1].y });
return out;
}
Point inside an outline
64-bit
Whether a point lies inside a closed outline.
Even-odd ray casting with no tolerance. The division by (b.y − a.y) only runs when the two ends are on opposite sides of the ray, so it cannot be zero. Arcs are tested as 5° chords, so a point between an arc and its chord is classed by the chord.
geom.js
lines 146 to 155
TakeoffCraft 0.8.1
/** Ray-cast point in polygon (arcs followed); the boundary itself is not relied on by callers. */
export function pointInPolygon(p, pts0) {
const pts = hasArcs(pts0) ? flatten(pts0, true) : pts0;
let inside = false;
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
const a = pts[i], b = pts[j];
if ((a.y > p.y) !== (b.y > p.y) && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x) inside = !inside;
}
return inside;
}
Two segments cross or touch
64-bit
Whether two straight segments cross or touch.
d₁ = (d − c) × (a − c), …; cross when d₁, d₂ lie beyond ±s on opposite sides and d₃, d₄ too
Four cross products are compared with s = 1e-9 × max(|ab|, |cd|, 1). The cross products have units of length squared while s is a length, so the tolerance tightens or loosens with the size of the segments; for segments shorter than 1 PDF unit it is an absolute 1e-9. Touching is a near-zero cross product plus a bounding-box test widened by s.
geom.js
lines 159 to 166
TakeoffCraft 0.8.1
/** Do segments ab and cd cross or touch? (eps relative to their size) */
export function segmentsIntersect(a, b, c, d, eps = 1e-9) {
const s = Math.max(dist(a, b), dist(c, d), 1) * eps;
const d1 = cross(c, d, a), d2 = cross(c, d, b), d3 = cross(a, b, c), d4 = cross(a, b, d);
if (((d1 > s && d2 < -s) || (d1 < -s && d2 > s)) && ((d3 > s && d4 < -s) || (d3 < -s && d4 > s))) return true;
const on = (p, q, r, dv) => Math.abs(dv) <= s && Math.min(p.x, q.x) - s <= r.x && r.x <= Math.max(p.x, q.x) + s && Math.min(p.y, q.y) - s <= r.y && r.y <= Math.max(p.y, q.y) + s;
return on(c, d, a, d1) || on(c, d, b, d2) || on(a, b, c, d3) || on(a, b, d, d4);
}
Outline crosses itself
64-bit
Whether a closed outline crosses itself.
Every pair of edges that do not share a corner goes through segmentsIntersect; arcs as 5° chords. An outline with fewer than 4 points after flattening is never reported as crossing.
geom.js
lines 168 to 181
TakeoffCraft 0.8.1
/** Does a closed outline cross itself? (Neighbouring edges share a vertex and are skipped.) */
export function selfIntersects(pts0) {
const pts = hasArcs(pts0) ? flatten(pts0, true) : pts0;
const n = pts.length;
if (n < 4) return false;
for (let i = 0; i < n; i++) {
const a = pts[i], b = pts[(i + 1) % n];
for (let j = i + 1; j < n; j++) {
if (j === i || (j + 1) % n === i || (i + 1) % n === j) continue;
if (segmentsIntersect(a, b, pts[j], pts[(j + 1) % n])) return true;
}
}
return false;
}
Outline that cannot be measured
64-bit
Refuses an outline with too few points, one that crosses itself, or one with no area.
The empty test is area < 1e-9 in square PDF units. It is absolute: it does not scale with the sheet's scale or the size of the outline. The area tested is the one with arcs included.
geom.js
lines 193 to 202
TakeoffCraft 0.8.1
/**
* Why an outline can't be measured, or null. Checks it is a real polygon and doesn't cross
* itself (a crossing outline has no meaningful area: the shoelace would be wrong).
*/
export function outlineProblem(pts, what = 'area') {
if (pts.length < 3 && !(pts.length === 2 && hasArcs(pts))) return t('geom.' + what + '.min');
if (selfIntersects(pts)) return t('geom.' + what + '.cross');
if (polygonArea(pts) < 1e-9) return t('geom.' + what + '.empty');
return null;
}
Cut-out inside its area
64-bit
Checks that a cut-out lies fully inside the outline and does not overlap another cut-out.
Uses the crossing test on the edges and the inside test on every vertex of the cut-out (arcs as 5° chords, both ways for other cut-outs). A cut-out whose arc bulges past the outline between two chord points, without any chord crossing, is not caught by these tests.
geom.js
lines 204 to 214
TakeoffCraft 0.8.1
/** Why a cut-out can't go into an area, or null: it must sit fully inside and clear of other cut-outs. */
export function holeProblem(outer, holes, hole) {
const p = outlineProblem(hole, 'hole');
if (p) return p;
const fh = flatten(hole, true);
if (outlinesCross(outer, hole) || !fh.every((v) => pointInPolygon(v, outer))) return t('geom.hole.inside');
for (const h of holes) {
if (outlinesCross(h, hole) || fh.some((v) => pointInPolygon(v, h)) || flatten(h, true).some((v) => pointInPolygon(v, hole))) return t('geom.hole.overlap');
}
return null;
}
In detail
Method
Even-odd ray casting for point in polygon; orientation (cross product) tests for segment crossing, with a collinear-and-in-box test for touching; every pair of edges is tested (no spatial index).
Number types
64-bit numbers.
Tolerances
Segment crossing: s = 1e-9 × max(|ab|, |cd|, 1). Empty outline: area < 1e-9 square PDF units (absolute). Point in polygon: none.
Limits
The crossing tolerance s is a length, but it is compared with cross products, which are a length squared. The floor of 1 makes it an absolute 1e-9 for segments shorter than one PDF unit. A point exactly on an edge can be reported either way.
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.