Finds the closed area round a picked point to hatch, with its islands, and lays the pattern's lines inside it.
How the precision is kept
A hatch boundary is stored as straight pieces: every arc and circle that bounds it is first cut into chords at 128 per full turn. The boundary's corners are merged on a tolerance of 1e-6 of the boundary objects' extent. Both steps, not the 64-bit arithmetic, set how closely the hatch follows the objects round it.
Open item. Keep arcs in a hatch boundary as arcs, so the area of a hatched round shape is not that of a 128-sided polygon.
Calculations under this policy
9 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.
The points that stand in for an arc or circle in a hatch boundary.
n = max(4, ⌈|sweep| ÷ (2π ÷ 128)⌉); point = c + r·(cos a, sin a)
Every arc becomes chords at 128 per full turn, at least 4. A full circle is a 128-sided polygon inscribed in it: each side lies up to r × (1 − cos(π ÷ 128)) ≈ 0.0003 r inside the circle, and the polygon's area is about 0.04% less than the circle's.
hatch.js
lines 173 to 182
CadCraft 0.8.2
/** Points along an arc primitive (CCW a0 → a1, or the other way when rev), as a flat list ending at its end. */
function arcPoints(p, rev) {
const sweep = p.a1 - p.a0, n = Math.max(4, Math.ceil(Math.abs(sweep) / (TAU / 128)));
const flat = [];
for (let i = 0; i <= n; i++) {
const a = rev ? p.a1 - sweep * i / n : p.a0 + sweep * i / n;
flat.push(p.cx + p.r * Math.cos(a), p.cy + p.r * Math.sin(a));
}
return flat;
}
A closed object as a boundary
64-bit
The boundary loop of a circle, a closed polyline or a full ellipse picked as an object.
A circle and a polyline's arc segments go through arcPoints; a full ellipse is taken at 144 steps per turn. An ellipse counts as full when t1 − t0 ≥ 2π − 1e-9.
hatch.js
lines 154 to 171
CadCraft 0.8.2
/** A closed object as a flat loop (arcs and ellipses as short straight pieces), else null. */
export function loopsOf(e) {
if (e.type === 'circle') return [arcPoints({ cx: e.cx, cy: e.cy, r: e.r, a0: 0, a1: TAU }, false)];
if (e.type === 'poly' && e.closed && e.pts.length >= 2) {
const flat = [];
for (const p of polyPrims(e)) {
const pts = p.k === 'L' ? [p.x1, p.y1] : arcPoints(p, p.rev).slice(0, -2);
flat.push(...pts);
}
return flat.length >= 6 ? [flat] : null;
}
if (e.type === 'ellipse' && e.t1 - e.t0 >= TAU - 1e-9) {
const pts = ellipsePoints(e, 144), flat = [];
for (let i = 0; i + 1 < pts.length; i++) flat.push(pts[i].x, pts[i].y);
return [flat];
}
return null;
}
Boundary tolerance
64-bit
The distance within which boundary pieces count as touching.
tol = 1e-9 × max(width, height, 1)
Scaled to the larger side of the box round all boundary objects considered, and at least 1e-9. It does not depend on how far the objects are from the origin.
The parameters at which two segments cross or touch, so each can be split there.
u = ((c − a) × q) ÷ (r × q); v = ((c − a) × r) ÷ (r × q)
Two pieces are parallel when |r × q| ≤ 1e-12 × |r|·|q|, a relative test. A crossing counts when both parameters are within tol ÷ length of [0, 1]; the parameters are then clamped to [0, 1]. Parallel pieces on one line split each other at the ends that lie within tol of the other piece.
hatch.js
lines 308 to 323
CadCraft 0.8.2
function crossSplit(s, t, ss, ts, tol) {
const [ax, ay, bx, by] = s, [cx, cy, dx, dy] = t;
const rx = bx - ax, ry = by - ay, qx = dx - cx, qy = dy - cy;
const den = rx * qy - ry * qx, lr = Math.hypot(rx, ry), lq = Math.hypot(qx, qy);
const onSeg = (px, py, x1, y1, vx, vy, L) => { const u = ((px - x1) * vx + (py - y1) * vy) / (L * L); const d = Math.abs((px - x1) * vy - (py - y1) * vx) / L; return d <= tol && u > -tol / L && u < 1 + tol / L ? Math.max(0, Math.min(1, u)) : null; };
if (Math.abs(den) <= 1e-12 * lr * lq) {
// parallel: when they lie on one line, each splits the other at its ends
for (const [px, py] of [[cx, cy], [dx, dy]]) { const u = onSeg(px, py, ax, ay, rx, ry, lr); if (u !== null) ss.push(u); }
for (const [px, py] of [[ax, ay], [bx, by]]) { const u = onSeg(px, py, cx, cy, qx, qy, lq); if (u !== null) ts.push(u); }
return;
}
const u = ((cx - ax) * qy - (cy - ay) * qx) / den, v = ((cx - ax) * ry - (cy - ay) * rx) / den;
const eu = tol / lr, ev = tol / lq;
if (u < -eu || u > 1 + eu || v < -ev || v > 1 + ev) return;
ss.push(Math.max(0, Math.min(1, u))); ts.push(Math.max(0, Math.min(1, v)));
}
Merging boundary corners
64-bit
The corner points of the boundary, with points that nearly coincide made one.
Split points are computed back from their parameters and then merged: a point within snap in both x and y of an earlier vertex takes that vertex's coordinates. A boundary corner can therefore sit up to 1e-6 of the boundary objects' extent from the computed crossing. Math.round(x ÷ snap) is used only as a grid key to find neighbors.
hatch.js
lines 228 to 249
CadCraft 0.8.2
// 3. a planar graph: vertices merged within the tolerance, edges once each
const vid = new Map(), verts = [], snap = tol * 1000;
const vertex = (x, y) => {
const gx = Math.round(x / snap), gy = Math.round(y / snap);
for (let i = -1; i <= 1; i++) for (let j = -1; j <= 1; j++) {
for (const v of vid.get((gx + i) + ',' + (gy + j)) || []) if (Math.abs(verts[v].x - x) <= snap && Math.abs(verts[v].y - y) <= snap) return v;
}
const v = verts.length, k = gx + ',' + gy;
verts.push({ x, y, out: [], i: v });
const a = vid.get(k); if (a) a.push(v); else vid.set(k, [v]);
return v;
};
const edgeSet = new Set(), edges = [];
segs.forEach((s, i) => {
const ts = [...new Set(splits[i])].sort((p, q) => p - q);
let prev = vertex(s[0] + (s[2] - s[0]) * ts[0], s[1] + (s[3] - s[1]) * ts[0]);
for (let k = 1; k < ts.length; k++) {
const v = vertex(s[0] + (s[2] - s[0]) * ts[k], s[1] + (s[3] - s[1]) * ts[k]);
if (v !== prev) { const ek = prev < v ? prev + ':' + v : v + ':' + prev; if (!edgeSet.has(ek)) { edgeSet.add(ek); edges.push([prev, v]); } }
prev = v;
}
});
Faces and their areas
64-bit
Every closed region the boundary pieces make, with the signed area used to choose among them.
area = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)
Edges at each vertex are ordered by their atan2 angle and faces are traced from that order. The area is the shoelace sum over raw coordinates; it is used for its sign and to pick the smallest face round the picked point, and is not reported. Its products are of the size of the coordinates squared, so far from the origin the comparison between nearly equal faces has fewer digits to go on.
hatch.js
lines 269 to 283
CadCraft 0.8.2
// 4. every face: from a half-edge u→v, turn to the edge leaving v just clockwise of v→u (face on the left)
const next = (h) => {
const out = verts[h.to].out, i = out.indexOf(h.twin);
return out[(i - 1 + out.length) % out.length];
};
const faces = [];
for (const h0 of half) {
if (h0.used) continue;
const ring = [];
let h = h0, guard = 0;
do { h.used = true; ring.push(verts[h.from]); h = next(h); } while (h !== h0 && ++guard < half.length + 2);
let area = 0;
for (let i = 0; i < ring.length; i++) { const p = ring[i], q = ring[(i + 1) % ring.length]; area += p.x * q.y - q.x * p.y; }
faces.push({ ring, area: area / 2, comp: null });
}
Point inside a face
64-bit
Whether the picked point, or an island, lies inside a face.
An even-odd ray-crossing test with no tolerance. The division by (b.y − a.y) is reached only when the two ends are on different sides of the ray, so it is never a division by zero.
hatch.js
lines 325 to 332
CadCraft 0.8.2
function inRing(q, ring) {
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const a = ring[i], b = ring[j];
if ((a.y > q.y) !== (b.y > q.y) && q.x < (b.x - a.x) * (q.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return inside;
}
Pattern scale and angle
64-bit
The pattern's line families at the hatch's scale, angle and origin.
k = scale ÷ (1 metric, 25.4 inch); origin = R(angle)·(x, y)·k; spacing and dashes × k
The inch definitions are the metric ones divided by 25.4, the exact factor. The pattern numbers are those of the table in this file, some written to 6 decimals (4.490128). The hatch angle is turned into degrees here (× 180 ÷ π) and back into radians when the lines are generated.
hatch.js
lines 58 to 78
CadCraft 0.8.2
/** The pattern's lines as L primitives, clipped even-odd to the loops; null when it cannot or should not be drawn. */
export function hatchLines(e, doc = null) {
if (e.solid) return null;
if (cache.has(e)) return cache.get(e);
let fams = null;
if (Array.isArray(e.patlines) && e.patlines.length) fams = e.patlines;
else {
const def = PATTERNS[String(e.pattern || '').toUpperCase()];
if (def) {
const k = (e.pscale > 0 ? e.pscale : 1) / (hatchIsMetric(e, doc) ? 1 : 25.4), turn = e.pangle || 0;
const c = Math.cos(turn), s = Math.sin(turn);
fams = def.lines.map((f) => ({ angle: f.angle + turn * 180 / Math.PI, x: (f.x * c - f.y * s) * k, y: (f.x * s + f.y * c) * k, dx: f.dx * k, dy: f.dy * k, dashes: f.dashes.map((d) => d * k) }));
}
}
// the pattern origin (Properties › Origin X / Y, P.7): every family moves with it
const ox = Number.isFinite(e.ox) ? e.ox : 0, oy = Number.isFinite(e.oy) ? e.oy : 0;
if (fams && (ox || oy)) fams = fams.map((f) => ({ ...f, x: f.x + ox, y: f.y + oy }));
const out = fams ? clipFamilies(fams, e.loops) : null;
cache.set(e, out);
return out;
}
Pattern lines clipped to the boundary
64-bit
The dashes of every pattern line that fall inside the hatch.
line k origin = O + k·(dx·u + dy·n); crossing: t = d₁ ÷ (d₁ − d₂) along an edge
Each line's origin is k times the family's offset from the pattern origin, not a running sum, so the spacing does not drift across a large hatch. A crossing is a linear interpolation between the signed distances of an edge's two ends; a vertex exactly on a line counts once. Dash positions along a line advance by repeated addition of the dash lengths, starting from floor(t0 ÷ period) × period.
hatch.js
lines 94 to 146
CadCraft 0.8.2
function clipFamilies(fams, loops) {
const edges = loopEdges(loops);
if (!edges.length) return null;
const out = [];
let lineCount = 0;
for (const f of fams) {
const a = f.angle * Math.PI / 180, ux = Math.cos(a), uy = Math.sin(a), nx = -uy, ny = ux;
const dy = f.dy;
if (!(Math.abs(dy) > 1e-12)) continue;
// the loops' extent across the lines (along n) and along them (along u), from the family's origin
let nMin = Infinity, nMax = -Infinity;
for (const [x1, y1, x2, y2] of edges) for (const [x, y] of [[x1, y1], [x2, y2]]) { const v = (x - f.x) * nx + (y - f.y) * ny; if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
const k0 = Math.ceil(Math.min(nMin / dy, nMax / dy) - 1e-9), k1 = Math.floor(Math.max(nMin / dy, nMax / dy) + 1e-9);
lineCount += Math.max(0, k1 - k0 + 1);
if (lineCount > MAX_LINES) return null;
const period = f.dashes.reduce((s, d) => s + Math.abs(d), 0);
for (let k = k0; k <= k1; k++) {
// the line through O + k (dx u + dy n), direction u
const ox = f.x + k * (f.dx * ux + dy * nx), oy = f.y + k * (f.dx * uy + dy * ny);
const ts = [];
for (const [x1, y1, x2, y2] of edges) {
const d1 = (x1 - ox) * nx + (y1 - oy) * ny, d2 = (x2 - ox) * nx + (y2 - oy) * ny;
if ((d1 > 0) === (d2 > 0)) continue; // half-open: a vertex on the line counts once
const tt = d1 / (d1 - d2);
const px = x1 + (x2 - x1) * tt, py = y1 + (y2 - y1) * tt;
ts.push((px - ox) * ux + (py - oy) * uy);
}
if (ts.length < 2) continue;
ts.sort((p, q) => p - q);
for (let i = 0; i + 1 < ts.length; i += 2) {
const t0 = ts[i], t1 = ts[i + 1];
if (t1 - t0 < 1e-12) continue;
if (!f.dashes.length || !(period > 1e-12)) { out.push(seg(ox, oy, ux, uy, t0, t1)); continue; }
// dashes repeat from the line's origin: walk the periods that overlap [t0, t1]
let start = Math.floor(t0 / period) * period;
for (; start < t1; start += period) {
let p = start;
for (const d of f.dashes) {
const len = Math.abs(d);
if (d > 0 || d === 0) {
const a0 = Math.max(p, t0), a1 = Math.min(p + (d === 0 ? 0 : len), t1);
if (d === 0 ? p >= t0 && p <= t1 : a1 > a0) out.push(d === 0 ? dot(ox, oy, ux, uy, p, period) : seg(ox, oy, ux, uy, a0, a1));
}
p += len;
}
if (out.length > MAX_SEGS) return null;
}
}
if (out.length > MAX_SEGS) return null;
}
}
return out;
}
In detail
Method
All boundary objects as segments, split where they cross, built into a planar graph, faces traced by angle order, the smallest bounded face round the point kept, other pieces inside it added as islands (even-odd). Pattern lines are clipped by signed distances to the boundary edges.
Number types
64-bit numbers.
Tolerances
tol = 1e-9 × max(width, height, 1) of the boundary objects; vertices merge within 1000 × tol; parallel test 1e-12 relative; segments shorter than 1e-9 in both x and y are dropped. Pattern lines: 1e-9 allowance on the line index range, pieces under 1e-12 dropped.
Limits
A circle becomes a 128-sided polygon inside it: up to about 0.0003 r from the circle and about 0.04% less area. More than 80,000 boundary segments is refused; a pattern with more than 6,000 lines or 60,000 dashes is not drawn as lines.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.