Resources Numerical accuracy Calculation policy

Pattern outlines: curves, lengths and areas

Turns the points of a clothing pattern outline into a smooth curve, and measures lengths, areas and positions along it (Apparel add-on).

How the precision is kept

An outline is a list of corners and curve points in 64-bit numbers. Curves through curve points are centripetal Catmull–Rom splines, sampled at 16 points per span. Every length, area, centroid and position is then measured on that sampled polyline, so the sampling sets the accuracy of the measures.

Calculations under this policy

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

Tests None found by the review

One span of the curve

64-bit

The 16 points of the curve between two neighboring outline points.

tᵢ₊₁ = tᵢ + max(√|pᵢ₊₁ − pᵢ|, 1e-9); point = three levels of linear interpolation between p₀ … p₃ at t

All interpolation is 64-bit. The last sample is replaced by the end point itself, so the curve passes through the outline points exactly. A knot interval shorter than 1e-12 is given weight 0 instead of a division.

  • addons/apparel/lib/curve.js
  • lines 58 to 72
  • CadCraft 0.8.2
/** One centripetal Catmull–Rom span p1 → p2 (p0, p3 its neighbours), `steps` samples after p1 (p2 included). */
function crSpan(p0, p1, p2, p3, steps) {
  const tj = (ti, a, b) => ti + Math.max(Math.sqrt(dst(a, b)), 1e-9);
  const t0 = 0, t1 = tj(t0, p0, p1), t2 = tj(t1, p1, p2), t3 = tj(t2, p2, p3);
  const out = [];
  for (let s = 1; s <= steps; s++) {
    const t = t1 + (t2 - t1) * (s / steps);
    const L = (pa, pb, ta, tb) => { const k = (tb - ta) < 1e-12 ? 0 : (t - ta) / (tb - ta); return { x: pa.x + (pb.x - pa.x) * k, y: pa.y + (pb.y - pa.y) * k }; };
    const A1 = L(p0, p1, t0, t1), A2 = L(p1, p2, t1, t2), A3 = L(p2, p3, t2, t3);
    const B1 = L(A1, A2, t0, t2), B2 = L(A2, A3, t1, t3);
    out.push(L(B1, B2, t1, t2));
  }
  out[out.length - 1] = { x: p2.x, y: p2.y };
  return out;
}

Sampled path of one edge

64-bit

The points of one seam edge from corner to corner.

A span between two corners with no curve point in the edge is one straight piece with the exact end points. At a corner end the missing neighbor is the mirror image of the next point (2·p₁ − p₂), computed in 64-bit numbers.

  • addons/apparel/lib/curve.js
  • lines 74 to 95
  • CadCraft 0.8.2
/** The sampled path of one edge (closed outline, or one run of an open line). */
export function edgePath(pts, edge, closed = true) {
  const n = pts.length;
  const at = (i) => pts[((i % n) + n) % n];
  const idx = edge.idx;
  const out = [{ x: pts[idx[0]].x, y: pts[idx[0]].y }];
  for (let k = 0; k + 1 < idx.length; k++) {
    const p1 = pts[idx[k]], p2 = pts[idx[k + 1]];
    const curved = idx.length > 2 && (p1.c || p2.c || edge.smooth);
    if (!curved) { out.push({ x: p2.x, y: p2.y }); continue; }
    // neighbours inside the edge; at a corner end, reflect (a free end: the curve leaves the corner pointing at the next point)
    let p0, p3;
    if (k > 0) p0 = pts[idx[k - 1]];
    else if (edge.smooth && closed) p0 = at(idx[0] - 1);
    else p0 = { x: 2 * p1.x - p2.x, y: 2 * p1.y - p2.y };
    if (k + 2 < idx.length) p3 = pts[idx[k + 2]];
    else if (edge.smooth && closed) p3 = at(idx[k + 1] + 1);
    else p3 = { x: 2 * p2.x - p1.x, y: 2 * p2.y - p1.y };
    out.push(...crSpan(p0, p1, p2, p3, STEPS));
  }
  return out;
}

Length of a path

64-bit

The length of a sampled edge or outline.

L = Σ |pᵢ − pᵢ₋₁|

Math.hypot per piece, added in order along the path. On a curved edge this is the length of the 16-piece polyline of each span, not of the smooth curve.

  • addons/apparel/lib/curve.js
  • lines 118 to 122
  • CadCraft 0.8.2
export function length(path) {
  let L = 0;
  for (let i = 1; i < path.length; i++) L += dst(path[i - 1], path[i]);
  return L;
}

Running length

64-bit

The distance from the start of a path to each of its points.

cᵢ = cᵢ₋₁ + |pᵢ − pᵢ₋₁|

A running sum in path order; each entry carries the rounding of the sums before it.

  • addons/apparel/lib/curve.js
  • lines 124 to 128
  • CadCraft 0.8.2
export function cumulative(path) {
  const c = [0];
  for (let i = 1; i < path.length; i++) c.push(c[i - 1] + dst(path[i - 1], path[i]));
  return c;
}

Point at a distance along a path

64-bit

The point and direction at a given distance along a path (used to place notches).

k = (d − cᵢ₋₁) ÷ (cᵢ − cᵢ₋₁); point = a + (b − a)·k; tangent = (b − a) ÷ |b − a|

The distance is clamped to the path. The direction is that of the straight piece the point falls on, so on a curve it changes in steps from piece to piece. Pieces shorter than 1e-12 give k = 0.

  • addons/apparel/lib/curve.js
  • lines 130 to 142
  • CadCraft 0.8.2
/** The point at distance d along a path (clamped), with the unit tangent there. */
export function pointAt(path, d, cum = cumulative(path)) {
  if (path.length < 2) return { x: path[0]?.x ?? 0, y: path[0]?.y ?? 0, tx: 1, ty: 0, i: 0 };
  const L = cum[cum.length - 1];
  const t = Math.min(Math.max(d, 0), L);
  let i = 1;
  while (i < cum.length - 1 && cum[i] < t) i++;
  const a = path[i - 1], b = path[i];
  const seg = cum[i] - cum[i - 1];
  const k = seg > 1e-12 ? (t - cum[i - 1]) / seg : 0;
  const len = Math.max(dst(a, b), 1e-12);
  return { x: a.x + (b.x - a.x) * k, y: a.y + (b.y - a.y) * k, tx: (b.x - a.x) / len, ty: (b.y - a.y) / len, i: i - 1 };
}

Nearest point of a path

64-bit

The point of a path nearest to a given point, and its distance along the path.

k = clamp(((p − a)·v) ÷ |v|², 0, 1); q = a + v·k

A projection onto each straight piece in turn; the smallest distance wins. A piece with squared length not above 1e-18 is treated as a point.

  • addons/apparel/lib/curve.js
  • lines 144 to 158
  • CadCraft 0.8.2
/** The nearest point of a path to p: its distance along the path and from p. */
export function nearest(path, p) {
  let best = { d: 0, dist: Infinity, x: path[0]?.x ?? 0, y: path[0]?.y ?? 0, i: 0 };
  let run = 0;
  for (let i = 1; i < path.length; i++) {
    const a = path[i - 1], b = path[i];
    const v = sub(b, a), L2 = v.x * v.x + v.y * v.y;
    const k = L2 > 1e-18 ? Math.min(1, Math.max(0, ((p.x - a.x) * v.x + (p.y - a.y) * v.y) / L2)) : 0;
    const q = { x: a.x + v.x * k, y: a.y + v.y * k };
    const dd = dst(p, q);
    if (dd < best.dist) best = { d: run + Math.sqrt(L2) * k, dist: dd, x: q.x, y: q.y, i: i - 1 };
    run += Math.sqrt(L2);
  }
  return best;
}

Area of an outline

64-bit

The signed area inside a closed sampled outline.

A = Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ) ÷ 2

The shoelace sum about the origin of the piece's frame, added in path order. A piece made by the PIECE command keeps the drawing coordinates of its source geometry, so far from the drawing origin the sum subtracts large products. The area is that of the sampled polygon.

  • addons/apparel/lib/curve.js
  • lines 160 to 165
  • CadCraft 0.8.2
/** Signed area (counter-clockwise positive) of a closed path. */
export function area(path) {
  let a = 0;
  for (let i = 0, n = path.length; i < n; i++) { const p = path[i], q = path[(i + 1) % n]; a += p.x * q.y - q.x * p.y; }
  return a / 2;
}

Centroid of an outline

64-bit

The middle of area of a closed outline (where the label goes by default).

kᵢ = xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ; a = Σ kᵢ; c = Σ (pᵢ + pᵢ₊₁)·kᵢ ÷ (3·a)

When |a| is below 1e-12 (absolute, in squared drawing units) the middle of the bounding box is returned instead of dividing.

  • addons/apparel/lib/curve.js
  • lines 167 to 175
  • CadCraft 0.8.2
export function centroid(path) {
  let a = 0, cx = 0, cy = 0;
  for (let i = 0, n = path.length; i < n; i++) {
    const p = path[i], q = path[(i + 1) % n], k = p.x * q.y - q.x * p.y;
    a += k; cx += (p.x + q.x) * k; cy += (p.y + q.y) * k;
  }
  if (Math.abs(a) < 1e-12) { const b = bounds(path); return { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 }; }
  return { x: cx / (3 * a), y: cy / (3 * a) };
}

Where two lines meet

64-bit

The meeting point of two lines and where it lies along each.

den = r × s; u = ((c − a) × s) ÷ den; point = a + r·u

Parallel when |den| < 1e-14. The limit is absolute on a product of two lengths, so it is not the same angle for short and long segments.

  • addons/apparel/lib/curve.js
  • lines 191 to 199
  • CadCraft 0.8.2
/** Where segments ab and cd cross: { x, y, s, t } (s along ab, t along cd, both 0…1 inside) | null when parallel. */
export function lineHit(a, b, c, d) {
  const r = sub(b, a), s = sub(d, c);
  const den = r.x * s.y - r.y * s.x;
  if (Math.abs(den) < 1e-14) return null;
  const q = sub(c, a);
  const u = (q.x * s.y - q.y * s.x) / den, v = (q.x * r.y - q.y * r.x) / den;
  return { x: a.x + r.x * u, y: a.y + r.y * u, s: u, t: v };
}

Outline direction

64-bit

Makes an outline run counter-clockwise.

The sign of the sampled outline's area decides; a negative area reverses the points after the first. No coordinate is changed.

  • addons/apparel/lib/curve.js
  • lines 183 to 189
  • CadCraft 0.8.2
/** The same points, counter-clockwise (pieces are kept CCW so "outward" is always to the right of travel). */
export function orientCCW(pts) {
  if (pts.length < 3) return pts;
  const { path } = sampled(pts, true);
  if (area(path) >= 0) return pts;
  return [pts[0], ...pts.slice(1).reverse()];
}

In detail

Method
Centripetal Catmull–Rom (knot spacing = square root of the point distance) evaluated by repeated linear interpolation; chord-length sums; shoelace area and centroid.
Number types
64-bit numbers; points are {x, y, c} objects.
Tolerances
Knot spacing at least 1e-9. A knot interval below 1e-12 gives weight 0. Segment length below 1e-12 and squared length below 1e-18 are treated as zero. Twice the area below 1e-12 falls back to the box center. Lines count as parallel when their cross product is below 1e-14. All are absolute.
Limits
A curved span is 16 straight pieces. The measured length of a curve is the sum of those pieces, which is never longer than the smooth curve.

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