Resources Numerical accuracy Calculation policy

Seam allowance: the cut line

Draws the cut line round a pattern piece at each edge's seam allowance, with the corner rules pattern makers use (Apparel add-on).

How the precision is kept

The cut line is an offset of the sampled sew line, so it is as fine as that sampling. Each straight piece is moved by the allowance along its unit normal and neighbors are joined where the moved lines meet, all in 64-bit numbers. Joins that would run far from the corner are replaced by a simpler shape.

Open item. Normalize the averaged direction in offsetClosed's fallback so that point is also a full allowance away from the sew line.

Calculations under this policy

7 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/seam.js CadCraft 0.8.2 64-bit 7 calculations Reviewed

Tests None found by the review

Offset of one edge

64-bit

One edge of the sew line moved outward by its allowance.

n = (tᵧ, −tₓ); moved point = p + n·w; join = meeting point of the two moved lines when |t₁ × t₂| > 1e-9 and it is within 4·w of the vertex

The unit direction is formed with Math.hypot and segments shorter than 1e-12 are left out. Where a join is not used, both moved end points are kept, which cuts the corner straight across.

  • addons/apparel/lib/seam.js
  • lines 51 to 69
  • CadCraft 0.8.2
/** A polyline moved w to its right; neighbouring segments joined where their moved lines meet. */
export function offsetOpen(path, w) {
  const p = clean(path);
  if (p.length < 2 || !(w > 0)) return p.map((q) => ({ ...q }));
  const segs = [];
  for (let i = 0; i + 1 < p.length; i++) { const t = unit(p[i], p[i + 1]); if (t) segs.push({ a: p[i], b: p[i + 1], t, n: right(t) }); }
  if (!segs.length) return [p[0]];
  const out = [add(segs[0].a, segs[0].n, w)];
  for (let i = 0; i + 1 < segs.length; i++) {
    const s = segs[i], u = segs[i + 1];
    const a = add(s.b, s.n, w), b = add(u.a, u.n, w);
    const hit = Math.abs(cross(s.t, u.t)) > 1e-9 ? meet(a, s.t, b, u.t) : null;
    if (hit && Math.hypot(hit.x - s.b.x, hit.y - s.b.y) < w * 4) out.push({ x: hit.x, y: hit.y });
    else { out.push(a); out.push(b); }
  }
  const last = segs[segs.length - 1];
  out.push(add(last.b, last.n, w));
  return out;
}

Offset of an outline with no corners

64-bit

A closed smooth outline moved outward by one allowance all round.

vertex = meeting point of the two moved lines; fallback = b + right((t₁ + t₂) ÷ 2)·w

The fallback, used when the two directions are parallel or the meeting point is 4·w or more away, moves the vertex along the average of the two unit directions without scaling that average back to length 1. The point is then less than w from the sew line whenever the two directions differ.

  • addons/apparel/lib/seam.js
  • lines 71 to 87
  • CadCraft 0.8.2
/** The closed path offset by w all round (one smooth edge, no corners). */
function offsetClosed(path, w) {
  const p = clean(path);
  if (p.length > 1 && Math.hypot(p[0].x - p[p.length - 1].x, p[0].y - p[p.length - 1].y) < 1e-9) p.pop();
  if (!(w > 0)) return p;
  const n = p.length;
  const out = [];
  for (let i = 0; i < n; i++) {
    const a = p[(i - 1 + n) % n], b = p[i], c = p[(i + 1) % n];
    const t1 = unit(a, b), t2 = unit(b, c);
    if (!t1 || !t2) continue;
    const q1 = add(b, right(t1), w), q2 = add(b, right(t2), w);
    const hit = Math.abs(cross(t1, t2)) > 1e-9 ? meet(q1, t1, q2, t2) : null;
    out.push(hit && Math.hypot(hit.x - b.x, hit.y - b.y) < w * 4 ? { x: hit.x, y: hit.y } : add(b, right({ x: (t1.x + t2.x) / 2, y: (t1.y + t2.y) / 2 }), w));
  }
  return out;
}

Dropping repeated points

64-bit

Removes points that repeat the one before.

kept when |p − previous| > 1e-9

The limit is 1e-9 drawing units, absolute, for millimeters and inches alike.

  • addons/apparel/lib/seam.js
  • lines 44 to 49
  • CadCraft 0.8.2
/** Drop points closer than eps to the one before (zero-length segments break the joins). */
function clean(path, eps = 1e-9) {
  const out = [];
  for (const p of path) if (!out.length || Math.hypot(p.x - out[out.length - 1].x, p.y - out[out.length - 1].y) > eps) out.push({ x: p.x, y: p.y });
  return out;
}

Dropping points on a straight line

64-bit

Removes cut line points that lie on the straight line between their neighbors.

kept when |u × v| > 1e-9 × |u| × |v|

The cross product is compared with 1e-9 times the two lengths, so the test is on the sine of the turn and does not depend on the size of the piece. Up to three passes are made.

  • addons/apparel/lib/seam.js
  • lines 89 to 104
  • CadCraft 0.8.2
/** A closed path without points that lie on the straight line between their neighbours (joins leave some). */
function straighten(c) {
  let out = c.slice();
  for (let pass = 0; pass < 3 && out.length > 3; pass++) {
    const keep = out.filter((p, i) => {
      const a = out[(i - 1 + out.length) % out.length], b = out[(i + 1) % out.length];
      const ux = p.x - a.x, uy = p.y - a.y, vx = b.x - p.x, vy = b.y - p.y;
      const lu = Math.hypot(ux, uy), lv = Math.hypot(vx, vy);
      if (lu < 1e-9 || lv < 1e-9) return false;
      return Math.abs(ux * vy - uy * vx) > 1e-9 * lu * lv;
    });
    if (keep.length === out.length) break;
    out = keep;
  }
  return out;
}

Inside corner

64-bit

Trims two cut lines where they cross at an inside corner.

Segment pairs are tested with lineHit and a crossing counts when both parameters are within −1e-9 to 1 + 1e-9. Only the last 64 pieces of one line and the first 64 of the other are searched.

  • addons/apparel/lib/seam.js
  • lines 106 to 116
  • CadCraft 0.8.2
/** Inside corner: cut the tail of A and the head of B where they cross. → { a, b } trimmed, or null. */
function trimCross(A, B) {
  const nA = Math.min(A.length - 1, 64), nB = Math.min(B.length - 1, 64);
  for (let i = A.length - 2; i >= A.length - 1 - nA && i >= 0; i--) {
    for (let j = 0; j < nB && j + 1 < B.length; j++) {
      const h = lineHit(A[i], A[i + 1], B[j], B[j + 1]);
      if (h && h.s >= -1e-9 && h.s <= 1 + 1e-9 && h.t >= -1e-9 && h.t <= 1 + 1e-9) return { a: [...A.slice(0, i + 1), { x: h.x, y: h.y }], b: [{ x: h.x, y: h.y }, ...B.slice(j + 1)] };
    }
  }
  return null;
}

Corner between two allowances

64-bit

The points that join the cut lines of two edges at a corner, by the corner's rule.

turn = tₐ × t_b; turn < −1e-9 → inside corner; meeting points used only within 20 × the wider allowance of the corner

Every join point is a line–line intersection from lineHit. The reverse rule mirrors a direction in the sew line with v′ = 2(v·a)a − v. A meeting point further than 20 allowances away is dropped, which leaves the corner cut straight across.

  • addons/apparel/lib/seam.js
  • lines 118 to 159
  • CadCraft 0.8.2
/**
 * The join at corner C between the cut line A (ending) and B (starting). wA, wB the edges' widths, tA / tB the sew line's
 * directions arriving at and leaving C. → the points to put between A's last point and B's first (A and B may be trimmed).
 */
function join(C, A, B, wA, wB, tA, tB, rule) {
  const a = A[A.length - 1], b = B[0];
  const uA = A.length > 1 ? unit(A[A.length - 2], a) || tA : tA;   // the cut lines' own directions at the corner
  const uB = B.length > 1 ? unit(b, B[1]) || tB : tB;
  const turn = cross(tA, tB);
  const wMax = Math.max(wA, wB, 1e-9);
  const near = (h) => h && Math.hypot(h.x - C.x, h.y - C.y) <= FAR * wMax;
  if (turn < -1e-9) {   // inside corner: trim where they cross
    const tr = trimCross(A, B);
    if (tr) { A.splice(0, A.length, ...tr.a); B.splice(0, B.length, ...tr.b); return []; }
    const h = meet(a, uA, b, uB);
    return near(h) ? [h] : [];
  }
  if (Math.abs(turn) <= 1e-9 && tA.x * tB.x + tA.y * tB.y > 0) return [];   // straight on: a change of width is a step
  if (rule === 'mitre') return [];
  const hemB = wB >= wA;   // the wider allowance is the one squared off / turned back
  if (rule === 'square') {
    // the narrower cut line runs on to the wider edge's sew line; from there straight out, at right angles, to its cut line
    if (hemB) { const X = meet(a, uA, C, tB); const Y = near(X) && meet(X, right(tB), b, uB); return near(Y) ? [X, Y] : []; }
    const X = meet(b, uB, C, tA); const Y = near(X) && meet(X, right(tA), a, uA); return near(Y) ? [Y, X] : [];
  }
  if (rule === 'reverse') {
    const refl = (v, axis) => { const d = v.x * axis.x + v.y * axis.y; return { x: 2 * d * axis.x - v.x, y: 2 * d * axis.y - v.y }; };
    if (hemB) {
      // A's cut line runs on to B's sew line (the fold), then mirrored in it down to B's cut line
      const F = meet(a, uA, C, tB);
      if (!near(F)) return [];
      const G = meet(F, refl(uA, tB), b, uB);
      return near(G) ? [F, G] : [F];
    }
    const F = meet(b, uB, C, tA);   // B's cut line traced back to A's sew line
    if (!near(F)) return [];
    const G = meet(F, refl({ x: -uB.x, y: -uB.y }, tA), a, uA);
    return near(G) ? [G, F] : [F];
  }
  const h = meet(a, uA, b, uB);   // follow
  return near(h) ? [h] : [];
}

The whole cut line

64-bit

The closed cut line of a piece from its sew line and allowances.

Each edge is offset by its own width (0 keeps the sew line) and the joins are inserted between them. The closing point is removed when it is within 1e-9 of the first.

  • addons/apparel/lib/seam.js
  • lines 161 to 186
  • CadCraft 0.8.2
/** The cut line of a piece: its sew line (pts, CCW) with the allowance `sa` round it. */
export function cutLine(pts, sa) {
  const es = edges(pts, true);
  if (!es.length) return [];
  const paths = es.map((e) => edgePath(pts, e, true));
  if (es.length === 1 && es[0].smooth) return offsetClosed(paths[0], widthOf(sa, 0));
  const parts = es.map((e, i) => {
    const w = widthOf(sa, e.start);
    return { w, path: paths[i], cut: w > 0 ? offsetOpen(paths[i], w) : clean(paths[i]) };
  });
  const tIn = (p) => unit(p[p.length - 2] || p[0], p[p.length - 1]) || { x: 1, y: 0 };
  const tOut = (p) => unit(p[0], p[1] || p[0]) || { x: 1, y: 0 };
  const joins = [];
  for (let k = 0; k < parts.length; k++) {
    const A = parts[k], B = parts[(k + 1) % parts.length];
    const corner = es[(k + 1) % parts.length].start;
    const C = { x: pts[corner].x, y: pts[corner].y };
    const pa = clean(A.path), pb = clean(B.path);
    joins[k] = join(C, A.cut, B.cut, A.w, B.w, tIn(pa), tOut(pb), ruleOf(sa, corner));
  }
  const out = [];
  for (let k = 0; k < parts.length; k++) { out.push(...parts[k].cut, ...joins[k]); }
  const c = clean(out);
  if (c.length > 1 && Math.hypot(c[0].x - c[c.length - 1].x, c[0].y - c[c.length - 1].y) <= 1e-9) c.pop();
  return straighten(c);
}

In detail

Method
Offset of each segment along its right-hand unit normal; joins by line–line intersection; corner rules follow, square, mitre and reverse.
Number types
64-bit numbers; points are {x, y} objects.
Tolerances
Points closer than 1e-9 are merged. Directions with a cross product of 1e-9 or less count as parallel. A join is used only within 4 × the allowance of the vertex (inside an edge) or 20 × the wider allowance of the corner. Collinear points are dropped when the cross product is not above 1e-9 × the two lengths.
Limits
On a curved edge the cut line is the offset of 16 straight pieces per span. An inside corner is trimmed by searching only the last and first 64 pieces of the two cut 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.

Every policy that takes this approach