Resources Numerical accuracy Calculation policy

Pattern pieces from drawing geometry

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.

addons/apparel/lib/piece.js CadCraft 0.8.2 64-bit 9 calculations Reviewed

Tests None found by the review

Polyline arc to curve points

64-bit

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.

Every policy that takes this approach