Resources Numerical accuracy Calculation policy

Area markup with cut-outs, and clipboard placement

The area and perimeter of an area markup less its cut-outs, whether cut-outs fit, and where pasted markups land.

How the precision is kept

Shoelace areas and perimeters are plain 64-bit sums in points. Crossing tests use the sign of a cross product with no tolerance. Pasting adds one offset to every coordinate.

Calculations under this policy

6 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.

holes.js MarkupCraft 0.8.1 64-bit 4 calculations Reviewed

Tests None found by the review

Ring area

64-bit

The area inside a closed outline.

A = |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)| ÷ 2

The terms use raw coordinates, so a ring far from the origin subtracts large nearly equal products. The absolute value is taken once after the sum.

  • holes.js
  • lines 17 to 21
  • MarkupCraft 0.8.1
export function ringArea(pts) {
  let a = 0;
  for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; a += p[0] * q[1] - q[0] * p[1]; }
  return Math.abs(a) / 2;
}

Ring perimeter

64-bit

The closed length of an outline.

P = Σ hypot(Δx, Δy)

Edge lengths come from Math.hypot and are added in order in 64-bit numbers.

  • holes.js
  • lines 22 to 26
  • MarkupCraft 0.8.1
export function ringPerimeter(pts) {
  let s = 0;
  for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; s += Math.hypot(q[0] - p[0], q[1] - p[1]); }
  return s;
}

Segment crossing test

64-bit

Whether two segments properly cross, used to reject cut-outs that cross each other or the outline.

cross = (b − a) × (c − a); crossing if signs differ on both pairs

The cross product is taken with Math.sign and no tolerance, so a near-touch decided by rounding can go either way. Touching at an end does not count as a crossing.

  • holes.js
  • lines 35 to 40
  • MarkupCraft 0.8.1
const orient = (a, b, c) => Math.sign((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]));
/** Proper crossing of segments ab and cd (touching at an end does not count). */
function segsCross(a, b, c, d) {
  const o1 = orient(a, b, c), o2 = orient(a, b, d), o3 = orient(c, d, a), o4 = orient(c, d, b);
  return o1 * o2 < 0 && o3 * o4 < 0;
}

Point in outline

64-bit

Whether a point is inside an outline.

x < (xb − xa)(py − ya) ÷ (yb − ya) + xa

Ray casting in 64-bit numbers. The division is guarded because the edge is only used when its two ends are on opposite sides of the point's y.

  • holes.js
  • lines 27 to 34
  • MarkupCraft 0.8.1
export function inRing(p, pts) {
  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[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;
}
clip.js MarkupCraft 0.8.1 64-bit 2 calculations Reviewed

Tests None found by the review

Box round copied markups

64-bit

The bounding box of all points and rectangles in a clip.

min and max of all x and y

Only Math.min and Math.max over 64-bit coordinates, with no rounding.

  • clip.js
  • lines 88 to 97
  • MarkupCraft 0.8.1
/** The box round every point and rectangle of the items (user space). */
export function clipBounds(items) {
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
  const add = (x, y) => { x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y); };
  for (const m of items || []) {
    if (m.rect) { add(m.rect[0], m.rect[1]); add(m.rect[2], m.rect[3]); }
    if (m.pts) for (const p of m.pts) add(p[0], p[1]);
  }
  return x0 <= x1 ? [x0, y0, x1, y1] : null;
}

Paste offset

64-bit

Moves pasted markups by an offset, or centers them on a point.

dx = cx − (x₀ + x₁) ÷ 2; x′ = x + dx

One offset is computed once and added to every point, rectangle corner and cut-out point in 64-bit numbers, with no rounding.

  • clip.js
  • lines 99 to 116
  • MarkupCraft 0.8.1
/**
 * New markups for a paste. `delta` moves every item by [dx, dy] (paste in place = [0, 0]); `center` puts the middle
 * of the items' box there instead. `rot` (the target sheet's rotation) keeps text reading upright on that sheet.
 */
export function placeClip(clip, { sheet, author = '', rot = null, delta = null, center = null } = {}) {
  if (!clip?.items?.length) return [];
  let [dx, dy] = delta || [0, 0];
  if (center) { const b = clipBounds(clip.items); if (b) { dx = center[0] - (b[0] + b[2]) / 2; dy = center[1] - (b[1] + b[3]) / 2; } }
  return clip.items.map((it) => makeMarkup({
    ...it,
    sheet, author,
    rot: rot === null ? it.rot : rot,
    pts: it.pts ? it.pts.map(([x, y]) => [x + dx, y + dy]) : null,
    rect: it.rect ? [it.rect[0] + dx, it.rect[1] + dy, it.rect[2] + dx, it.rect[3] + dy] : null,
    holes: it.holes ? it.holes.map((h) => h.map(([x, y]) => [x + dx, y + dy])) : undefined,
    style: { ...it.style },
  }));
}

In detail

Method
Shoelace formula; perimeter as the sum of Math.hypot edge lengths; orientation sign tests for crossings; ray casting for point in polygon.
Number types
64-bit numbers.
Tolerances
A cut-out whose area is under 1e-6 pt² is refused. The crossing test uses exact sign, with a tolerance of 0. Coordinates must be under 1e7 in absolute value.
Limits
Coordinates are not shifted to a local origin before the shoelace sum, so very large coordinates lose digits in the cross terms.

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