Makes a pattern piece from polylines, arcs, circles and ellipses of the drawing, places notches on it and moves its points by grips (Apparel add-on).
How the precision is kept
Arithmetic is 64-bit. An arc or circle of the drawing is not kept as an arc: it becomes curve points every 15 degrees (30 degrees on a circle) and the piece's outline is the spline through them. Outline points and marks are rounded to 6 decimals of the drawing unit when they are stored.
Open item. Keep arcs of the source outline as arcs, or state the largest distance between the spline and the arc on the piece.
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 curve points that stand for one arc segment of a polyline.
θ = 4·atan(bulge); r = L ÷ (2·sin(|θ| ÷ 2)); n = max(2, ⌈|θ| ÷ 15°⌉) points at equal angles
The points lie on the arc in 64-bit numbers and the last one is replaced by the segment's exact end. A segment of zero length with a bulge is not guarded: the direction divides by L = 0.
addons/apparel/lib/piece.js
lines 265 to 281
CadCraft 0.8.2
/** The points along a bulged segment a → b (excluding a, including b), all curve points but the last. */
export function bulgePoints(a, b, bulge) {
if (!bulge) return [{ x: b.x, y: b.y, c: 0 }];
const th = 4 * Math.atan(bulge); // included angle (CCW +)
const L = Math.hypot(b.x - a.x, b.y - a.y);
const r = L / (2 * Math.sin(Math.abs(th) / 2));
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
const h = r * Math.cos(Math.abs(th) / 2) * (Math.abs(th) > Math.PI ? -1 : 1);
const nx = -(b.y - a.y) / L, ny = (b.x - a.x) / L, s = th > 0 ? 1 : -1;
const cx = mx + nx * h * s, cy = my + ny * h * s;
const a0 = Math.atan2(a.y - cy, a.x - cx);
const n = Math.max(2, Math.ceil(Math.abs(th) / ARC_STEP));
const out = [];
for (let i = 1; i <= n; i++) { const ang = a0 + (th * i) / n; out.push({ x: cx + r * Math.cos(ang), y: cy + r * Math.sin(ang), c: i < n ? 1 : 0 }); }
out[out.length - 1] = { x: b.x, y: b.y, c: 0 };
return out;
}
Outline from a closed polyline, circle or ellipse
64-bit
The sew line of a new piece from one closed object.
circle: 12 curve points at 30°; ellipse: 16 curve points at 22.5° of its parameter
A circle or ellipse is replaced by curve points on it; the piece's outline is then the Catmull–Rom spline through them, which meets the circle at those points only. A polyline counts as closed when its flag says so or its ends are within 1e-6.
addons/apparel/lib/piece.js
lines 307 to 328
CadCraft 0.8.2
/** A closed polyline (or a circle / an ellipse) → the sew line. */
export function fromPoly(e) {
if (!e) return { error: 'type' };
if (e.type === 'circle') { const list = []; for (let i = 0; i < 12; i++) { const a = (i * Math.PI) / 6; list.push({ x: e.cx + e.r * Math.cos(a), y: e.cy + e.r * Math.sin(a), c: 1 }); } return finish(list); }
if (e.type === 'ellipse') {
const mx = e.mx, my = e.my, R = Math.hypot(mx, my), ang = Math.atan2(my, mx), r = R * e.ratio, list = [];
for (let i = 0; i < 16; i++) { const t = (i * Math.PI) / 8; const x = R * Math.cos(t), y = r * Math.sin(t); list.push({ x: e.cx + x * Math.cos(ang) - y * Math.sin(ang), y: e.cy + x * Math.sin(ang) + y * Math.cos(ang), c: 1 }); }
return finish(list);
}
if (e.type !== 'poly') return { error: 'type' };
const v = e.pts || [];
const closed = e.closed || (v.length > 2 && Math.hypot(v[0].x - v[v.length - 1].x, v[0].y - v[v.length - 1].y) < 1e-6);
if (!closed) return { error: 'open' };
const list = [{ x: v[0].x, y: v[0].y, c: 0 }];
const n = v.length;
for (let i = 0; i < n; i++) {
const a = v[i], b = v[(i + 1) % n];
if (i === n - 1 && !e.closed) break;
list.push(...bulgePoints(a, b, a.b || 0));
}
return finish(list);
}
Outline from lines and arcs that join up
64-bit
The sew line of a new piece from separate lines, arcs and open polylines.
arc: n = max(2, ⌈sweep ÷ 15°⌉) steps; ends join when |p − q| ≤ 1e-4
The join tolerance is 1e-4 drawing units, absolute. Where two pieces join, the end point of the first is kept and the start of the next is dropped, so a gap up to the tolerance is closed by moving nothing.
addons/apparel/lib/piece.js
lines 330 to 360
CadCraft 0.8.2
/** Lines, arcs and open polylines that join end to end into one closed outline → the sew line. */
export function fromChain(ents, tol = 1e-4) {
const segs = [];
for (const e of ents) {
if (e.type === 'line') segs.push([{ x: e.x1, y: e.y1, c: 0 }, { x: e.x2, y: e.y2, c: 0 }]);
else if (e.type === 'arc') {
const sweep = ((e.a1 - e.a0) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) || 2 * Math.PI;
const n = Math.max(2, Math.ceil(sweep / ARC_STEP));
const s = [];
for (let i = 0; i <= n; i++) { const a = e.a0 + (sweep * i) / n; s.push({ x: e.cx + e.r * Math.cos(a), y: e.cy + e.r * Math.sin(a), c: i > 0 && i < n ? 1 : 0 }); }
segs.push(s);
} else if (e.type === 'poly' && !e.closed) {
const s = [{ x: e.pts[0].x, y: e.pts[0].y, c: 0 }];
for (let i = 0; i + 1 < e.pts.length; i++) s.push(...bulgePoints(e.pts[i], e.pts[i + 1], e.pts[i].b || 0));
segs.push(s);
} else return { error: 'type' };
}
if (!segs.length) return { error: 'few' };
const near = (p, q) => Math.hypot(p.x - q.x, p.y - q.y) <= tol;
const chain = segs.shift();
while (segs.length) {
const end = chain[chain.length - 1];
const k = segs.findIndex((s) => near(s[0], end) || near(s[s.length - 1], end));
if (k < 0) return { error: 'gap' };
let s = segs.splice(k, 1)[0];
if (!near(s[0], end)) s = s.slice().reverse().map((p, i, arr) => ({ ...p, c: i === 0 || i === arr.length - 1 ? 0 : p.c }));
chain.push(...s.slice(1));
}
if (!near(chain[0], chain[chain.length - 1])) return { error: 'open' };
return finish(chain);
}
Smooth vertices
64-bit
Decides which vertices of the outline are curve points.
turn = |atan2(next − p) − atan2(p − previous)|; next to an arc and turn < 2° → curve point
The turn is the difference of two atan2 angles, folded to 0…π. The 2 degree limit is fixed.
addons/apparel/lib/piece.js
lines 283 to 294
CadCraft 0.8.2
/** Vertices where the outline runs on smoothly (tangent arcs, smooth polylines) become curve points. */
function markSmooth(pts) {
const n = pts.length;
return pts.map((p, i) => {
if (p.c) return p;
const a = pts[(i - 1 + n) % n], b = pts[(i + 1) % n];
const t1 = Math.atan2(p.y - a.y, p.x - a.x), t2 = Math.atan2(b.y - p.y, b.x - p.x);
let d = Math.abs(t2 - t1); if (d > Math.PI) d = 2 * Math.PI - d;
const nearArc = a.c || b.c;
return nearArc && d < SMOOTH ? { ...p, c: 1 } : p;
});
}
Stored outline points
64-bit
Cleans the new outline and rounds it for storage.
x, y → toFixed(6)
The outline points kept in the drawing are rounded to 6 decimals of the drawing unit. An outline whose sampled area is below 1e-9 (absolute) is refused as flat.
addons/apparel/lib/piece.js
lines 296 to 305
CadCraft 0.8.2
function finish(list) {
let pts = [];
for (const p of list) if (!pts.length || Math.hypot(p.x - pts[pts.length - 1].x, p.y - pts[pts.length - 1].y) > 1e-9) pts.push(p);
if (pts.length > 1 && Math.hypot(pts[0].x - pts[pts.length - 1].x, pts[0].y - pts[pts.length - 1].y) <= 1e-6) pts.pop();
if (pts.length < 3) return { error: 'few' };
pts = orientCCW(markSmooth(pts));
if (Math.abs(area(sampled(pts, true).path)) < 1e-9) return { error: 'flat' };
if (pts.length > MAX_PTS) return { error: 'many' };
return { pts: rawOf(pts.map((p) => ({ x: +p.x.toFixed(6), y: +p.y.toFixed(6), c: p.c }))) };
}
Where a notch sits
64-bit
The position of a notch on the cut line, opposite its place on the sew line.
q = point at distance d along the edge; notch = q + outward normal × allowance
The point and normal come from the straight piece of the sampled edge that the distance falls on. The notch is moved out by the edge's allowance along that normal; it is not intersected with the drawn cut line.
addons/apparel/lib/piece.js
lines 102 to 112
CadCraft 0.8.2
/** Where a notch sits: on the cut line, opposite its place on the sew line. n = outward normal. */
export function notchPlace(data, n) {
const pts = ptsOf(data.pts);
const e = edges(pts, true).find((x) => x.start === n.i);
if (!e) return null;
const path = edgePath(pts, e, true);
const q = pointAt(path, n.d);
const o = outward(q.tx, q.ty);
const w = widthOf(data.sa, e.start);
return { x: q.x + o.x * w, y: q.y + o.y * w, sx: q.x, sy: q.y, tx: q.tx, ty: q.ty, nx: o.x, ny: o.y, w };
}
A new notch from a picked point
64-bit
The edge and distance along it for a notch placed at a picked point.
nearest point over all edges; d → toFixed(4)
The distance along the edge that is stored is rounded to 4 decimals of the drawing unit.
addons/apparel/lib/piece.js
lines 114 to 124
CadCraft 0.8.2
/** A notch at the edge nearest p (piece frame). → the notch | null. */
export function placeNotch(data, p, kind = 'slit') {
const pts = ptsOf(data.pts);
let best = null;
for (const e of edges(pts, true)) {
const path = edgePath(pts, e, true);
const h = nearest(path, p);
if (!best || h.dist < best.dist) best = { i: e.start, d: h.d, dist: h.dist };
}
return best ? { i: best.i, d: +best.d.toFixed(4), k: KINDS.includes(kind) ? kind : 'slit' } : null;
}
Notch geometry
64-bit
The lines of a slit, V or T notch.
a = ang × π ÷ 180; direction = −R(a)·normal; tip = notch + direction × depth
The inward normal is turned by the notch's angle with one sine and cosine. Depth and width come from the piece's unit defaults (6 and 3 mm, or 0.25 and 0.125 in) unless the notch has its own depth.
addons/apparel/lib/piece.js
lines 186 to 201
CadCraft 0.8.2
for (const n of data.notches || []) {
const q = notchPlace(data, n);
if (!q) continue;
const depth = n.depth || S.notch;
const a = ((n.ang || 0) * Math.PI) / 180;
// the notch runs inward from the cut line; ang turns it off square
const dx = -(q.nx * Math.cos(a) - q.ny * Math.sin(a)), dy = -(q.nx * Math.sin(a) + q.ny * Math.cos(a));
const tip = { x: q.x + dx * depth, y: q.y + dy * depth };
if (n.k === 'v') {
const h = S.notchW / 2;
out.push(poly([{ x: q.x + q.tx * h, y: q.y + q.ty * h }, tip, { x: q.x - q.tx * h, y: q.y - q.ty * h }], false, { role: 'notch' }));
} else {
out.push(line(q, tip, { role: 'notch' }));
if (n.k === 't') { const h = S.notchW / 2; out.push(line({ x: tip.x + q.tx * h, y: tip.y + q.ty * h }, { x: tip.x - q.tx * h, y: tip.y - q.ty * h }, { role: 'notch' })); }
}
}
Moving a point by its grip
64-bit
The piece after one of its points or marks was dragged.
new position → toFixed(6)
The dragged position is rounded to 6 decimals before it is stored. A move that turns the outline inside out (sampled area below 0) is refused.
addons/apparel/lib/piece.js
lines 239 to 259
CadCraft 0.8.2
/** A grip dragged to `to` (piece frame). → new data | null (unknown grip). Moving an outline point keeps the piece CCW. */
export function gripMove(data, key, to) {
const d = structuredClone(data);
const xy2 = [+to.x.toFixed(6), +to.y.toFixed(6)];
let m;
if ((m = /^p(\d+)$/.exec(key)) && d.pts[+m[1]]) {
d.pts[+m[1]] = [xy2[0], xy2[1], d.pts[+m[1]][2]];
if (area(sampled(ptsOf(d.pts), true).path) < 0) return null; // turned inside out: refuse
return d;
}
if (key === 'ga' && d.grain) { d.grain.a = xy2; return d; }
if (key === 'gb' && d.grain) { d.grain.b = xy2; return d; }
if (key === 'fa' && d.fold) { d.fold.a = xy2; return d; }
if (key === 'fb' && d.fold) { d.fold.b = xy2; return d; }
if ((m = /^l(\d+)\.(\d+)$/.exec(key)) && d.lines[+m[1]]?.p[+m[2]]) { const q = d.lines[+m[1]].p[+m[2]]; d.lines[+m[1]].p[+m[2]] = [xy2[0], xy2[1], q[2] || 0]; return d; }
if ((m = /^d(\d+)$/.exec(key)) && d.drills[+m[1]]) { d.drills[+m[1]] = xy2; return d; }
if ((m = /^b(\d+)$/.exec(key)) && d.buttons[+m[1]]) { d.buttons[+m[1]] = [...xy2, d.buttons[+m[1]][2]]; return d; }
if ((m = /^h(\d+)$/.exec(key)) && d.holes[+m[1]]) { d.holes[+m[1]] = [...xy2, d.holes[+m[1]][2], d.holes[+m[1]][3]]; return d; }
if (key === 'lab') { d.label = xy2; return d; }
return null;
}
In detail
Method
Bulge to center and radius; points at equal angle steps; chains joined end to end; smooth vertices marked as curve points.
Number types
64-bit numbers; stored points are arrays [x, y, c].
Tolerances
Repeated points within 1e-9 are merged; an outline closes when its ends are within 1e-6; chain ends join within 1e-4; an outline with an area below 1e-9 is refused. A vertex next to an arc that turns less than 2 degrees becomes a curve point. All lengths are absolute, in drawing units.
Limits
A circle becomes 12 curve points and an ellipse 16; between those points the outline follows the spline, not the circle or ellipse. Coordinates are stored at 1e-6 of the drawing unit; a notch position at 1e-4.
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.