Resources Numerical accuracy Calculation policy

Outline checks: inside, crossing, cut-outs

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.

geom.js TakeoffCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-takeoffcraft.mjs, bin/test-takeoffcraft-cad.mjs

Arcs broken into chords

64-bit

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.

Every policy that takes this approach