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.
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;
}
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.