Resources Numerical accuracy Calculation policy

DXF-AAMA and ASTM pattern files

Writes pattern pieces and their grade rules to DXF-AAMA or ASTM D6673 files with a RUL rule file, and reads such files back (Apparel add-on).

How the precision is kept

Geometry is computed in 64-bit numbers and written as decimal text: coordinates with 6 decimals, grade rules in the RUL file with 2 decimals of the file's unit. Reading parses the text at full precision and then rounds outline points to 6 decimals and rules to 4. A file CadCraft wrote also carries the piece's own data, so it reads back as it was.

Open item. Write RUL rule values with more decimals in inch files, where 2 decimals is a quarter of a millimeter.

Calculations under this policy

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

Tests None found by the review

A number as file text

64-bit

The text of a coordinate, angle or text height in the DXF.

r = round(v × 1e6) ÷ 1e6; −0 → 0

Every coordinate written to the DXF is rounded to 6 decimals of the drawing unit. For values above about 9e9 the product v × 1e6 goes past 2⁵³ and the rounding no longer acts on single units.

  • addons/apparel/lib/aama.js
  • line 36
  • CadCraft 0.8.2
const f = (v) => { const r = Math.round(v * 1e6) / 1e6; return Object.is(r, -0) ? '0' : String(r); };

Piece to drawing coordinates

64-bit

A point of a piece moved to where the piece lies in the drawing.

x′ = x + cos(rot)·sx·pₓ − sin(rot)·sy·pᵧ; y′ = y + sin(rot)·sx·pₓ + cos(rot)·sy·pᵧ

One 64-bit expression per coordinate. A mirrored piece is written with its points in reverse order so that it still runs counter-clockwise.

  • addons/apparel/lib/aama.js
  • lines 47 to 52
  • CadCraft 0.8.2
/** Transform a piece-frame point by its placement. */
function placer(place) {
  const { x = 0, y = 0, rot = 0, sx = 1, sy = 1 } = place || {};
  const c = Math.cos(rot), s = Math.sin(rot);
  return (p) => ({ x: x + c * sx * p.x - s * sy * p.y, y: y + s * sx * p.x + c * sy * p.y });
}

What a piece writes

64-bit

The cut line, sew line, notches, grain line and marks of one piece in one size, in drawing coordinates.

corner on the cut line when the turn > 0.17 rad; notch angle = atan2 of the inward direction × 180 ÷ π

The sew line written is the 16-per-span sampled polyline, and the cut line its offset. A cut line point is marked as a turn point when the direction changes by more than 0.17 rad (about 9.7 degrees), a fixed limit.

  • addons/apparel/lib/aama.js
  • lines 55 to 81
  • CadCraft 0.8.2
/** Everything one piece shows, in drawing coordinates, at one size: the lists the file needs. */
function geometryOf(data, place, size) {
  const d = size && data.sizes ? gradeData(data, size) : data;
  const P = placer(place);
  const pts = ptsOf(d.pts);
  const sew = sampled(pts, true).path.slice(0, -1);
  const hasSa = !!d.sa && edges(pts, true).some((e) => widthOf(d.sa, e.start) > 0);
  const cut = hasSa ? cutLine(pts, d.sa) : sew;
  // which sew samples are our corners (turn points) — the rest are curve points
  const { at } = sampled(pts, true);
  const corner = new Set(at.filter((s, i) => s >= 0 && !pts[i].c));
  const notches = (d.notches || []).map((n) => { const q = notchPlace(d, n); return q ? { x: q.x, y: q.y, nx: -q.nx, ny: -q.ny, k: n.k } : null; }).filter(Boolean);
  let order = (a) => a;
  if (flipped(place)) order = (a) => a.slice().reverse();   // a mirrored piece runs clockwise: write it counter-clockwise
  return {
    d, hasSa,
    cut: order(cut.map(P)), sew: order(sew.map(P)), sewCorner: order(sew.map((_, i) => corner.has(i))),
    cutCorner: order(cut.map((p, i, arr) => { const a = arr[(i - 1 + arr.length) % arr.length], b = arr[(i + 1) % arr.length]; const t1 = Math.atan2(p.y - a.y, p.x - a.x), t2 = Math.atan2(b.y - p.y, b.x - p.x); let dd = Math.abs(t2 - t1); if (dd > Math.PI) dd = 2 * Math.PI - dd; return dd > 0.17; })),
    notches: notches.map((n) => { const q = P(n); const o = P({ x: n.x + n.nx, y: n.y + n.ny }); return { ...q, ang: Math.atan2(o.y - q.y, o.x - q.x) * 180 / Math.PI, k: n.k }; }),
    grain: d.grain ? [P({ x: d.grain.a[0], y: d.grain.a[1] }), P({ x: d.grain.b[0], y: d.grain.b[1] })] : null,
    fold: d.fold ? [P({ x: d.fold.a[0], y: d.fold.a[1] }), P({ x: d.fold.b[0], y: d.fold.b[1] })] : null,
    lines: (d.lines || []).map((l) => sampled(ptsOf(l.p), false).path.map(P)),
    drills: (d.drills || []).map((q) => P({ x: q[0], y: q[1] })),
    buttons: (d.buttons || []).map((b) => ({ c: P({ x: b[0], y: b[1] }), r: b[2] / 2 })),
    holes: (d.holes || []).map((h) => { const a = h[3] * Math.PI / 180, L = h[2] / 2; return [P({ x: h[0] - Math.cos(a) * L, y: h[1] - Math.sin(a) * L }), P({ x: h[0] + Math.cos(a) * L, y: h[1] + Math.sin(a) * L })]; }),
  };
}

Grade rule of a written point

64-bit

The move of one written point in every size, measured from the base size.

rule = [(xₛ − x_base, yₛ − y_base) for each size s]

The piece is built again in every size with the same sampling and the points are subtracted index by index. If a size gives a different number of points, the rules for that line are left out and a note is added.

  • addons/apparel/lib/aama.js
  • lines 153 to 154
  • CadCraft 0.8.2
ruleAt = (key, i) => (ok(key) ? book.add(per.map((q) => [q[key][i].x - g[key][i].x, q[key][i].y - g[key][i].y])) : null);
ruleAt.of = (pick) => book.add(per.map((q) => { const a = pick(q), b = pick(g); return a && b ? [a.x - b.x, a.y - b.y] : [0, 0]; }));

Rounding and sharing rules

64-bit

Gives every rule a number; identical rules share one.

dx, dy → round(v × 100) ÷ 100

Each move is rounded to 2 decimals of the file's unit before rules are compared and numbered. In an inch file that is 0.01 in, about a quarter of a millimeter.

  • addons/apparel/lib/aama.js
  • lines 87 to 92
  • CadCraft 0.8.2
add(moves) {
  const r = moves.map(([x, y]) => [Math.round(x * 100) / 100, Math.round(y * 100) / 100]);
  const k = JSON.stringify(r);
  if (!this.byKey.has(k)) { this.list.push(r); this.byKey.set(k, this.list.length); }
  return this.byKey.get(k);
}

Rules in the RUL file

64-bit

The lines of the rule file: an x and y pair per size for every rule.

x.toFixed(2), y.toFixed(2), four pairs to a line

The values were already rounded to 2 decimals when the rule was numbered; toFixed(2) here fixes the column width.

  • addons/apparel/lib/aama.js
  • lines 227 to 230
  • CadCraft 0.8.2
book.list.forEach((r, i) => {
  L.push('RULE: DELTA ' + (i + 1));
  for (let k = 0; k < r.length; k += 4) L.push(r.slice(k, k + 4).map(([x, y]) => x.toFixed(2).padStart(9) + y.toFixed(2).padStart(9)).join(''));
});

Reading a RUL file

64-bit

The sizes, sample size and rules of a rule file.

Numbers are parsed with Number at full 64-bit precision; a line with anything that is not a finite number is skipped. Commas are read as separators.

  • addons/apparel/lib/aama.js
  • lines 321 to 345
  • CadCraft 0.8.2
/** The RUL file → { units, sizes, sample, rules: Map(n → [[dx, dy] per size]) } | null. */
export function parseRul(text) {
  if (!text) return null;
  const out = { units: 'METRIC', sizes: [], sample: '', rules: new Map(), name: '' };
  let cur = null;
  for (const raw of String(text).split(/\r\n|\n|\r/)) {
    const line = raw.trim();
    if (!line) continue;
    let m;
    if ((m = /^RULE:\s*DELTA\s+(\d+)/i.exec(line))) { cur = []; out.rules.set(+m[1], cur); continue; }
    if ((m = /^UNITS:\s*(\S+)/i.exec(line))) { out.units = m[1].toUpperCase(); continue; }
    if ((m = /^SIZE LIST:\s*(.*)$/i.exec(line))) { out.sizes = m[1].trim().split(/\s+/).filter(Boolean); continue; }
    if ((m = /^SAMPLE SIZE:\s*(.*)$/i.exec(line))) { out.sample = m[1].trim(); continue; }
    if ((m = /^GRADE RULE TABLE:\s*(.*)$/i.exec(line))) { out.name = m[1].trim(); continue; }
    if (/^END$/i.test(line)) { cur = null; continue; }
    if (/^[A-Za-z][A-Za-z /]*:/.test(line)) continue;   // other header lines
    if (cur) {
      const nums = line.replace(/,/g, ' ').trim().split(/\s+/).map(Number);
      if (nums.some((x) => !Number.isFinite(x))) continue;
      for (let k = 0; k + 1 < nums.length; k += 2) cur.push([nums[k], nums[k + 1]]);
    }
  }
  if (!out.sizes.length) return null;
  return out;
}

Corners and curve points of a read outline

64-bit

Decides which points of an outline read from someone else's file are corners.

tol = max(1e-6, 1e-7 × largest |coordinate|) + 0.02 mm (0.001 in); corner when a turn point lies within tol, or the turn > 0.35 rad

The matching tolerance has a part scaled to the coordinates and a fixed part in the file's unit. The 0.35 rad (20 degree) limit is fixed.

  • addons/apparel/lib/aama.js
  • lines 376 to 386
  • CadCraft 0.8.2
const tolOf = (pts) => { let b = 0; for (const p of pts) b = Math.max(b, Math.abs(p.x), Math.abs(p.y)); return Math.max(1e-6, b * 1e-7) + (u === 'in' ? 0.001 : 0.02); };
const flags = (line) => {
  const tol = tolOf(line);
  return line.map((p, i) => {
    if (turn.some((q) => near(q, p, tol))) return 0;
    const a = line[(i - 1 + line.length) % line.length], b = line[(i + 1) % line.length];
    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;
    return d > 0.35 ? 0 : 1;   // a turn over 20° with no point telling us: a corner
  });
};

Outline points kept from a file

64-bit

The outline of a piece read from someone else's file.

x, y → toFixed(6)

The file's coordinates are parsed at full precision and then rounded to 6 decimals of the drawing unit when the piece is made.

  • addons/apparel/lib/aama.js
  • lines 390 to 391
  • CadCraft 0.8.2
pts = orientCCW(pts);
const data = { ...emptyPiece(u), pts: rawOf(pts.map((p) => ({ x: +p.x.toFixed(6), y: +p.y.toFixed(6), c: p.c }))) };

Seam allowance read from a file

64-bit

The allowance of each edge, from how far the file's cut line is from its sew line.

edge width = median of the distances from the edge's sample points to the cut line → 2 decimals (mm) or 4 (in); common width = median of the edges

An estimate from distances between two polylines, not a value stored in the file. An edge keeps its own width only when it differs from the common one by more than 0.05 mm (0.002 in).

  • addons/apparel/lib/aama.js
  • lines 396 to 406
  • CadCraft 0.8.2
for (const ed of edges(sp, true)) {
  const path = edgePath(sp, ed, true);
  const ds = path.slice(1, -1).concat(path.length <= 2 ? [{ x: (path[0].x + path[1].x) / 2, y: (path[0].y + path[1].y) / 2 }] : []).map((q) => nearest([...cut, cut[0]], q).dist).sort((a, b) => a - b);
  const w = ds.length ? +ds[Math.floor(ds.length / 2)].toFixed(u === 'in' ? 4 : 2) : 0;
  e2[ed.start] = w; all.push(w);
}
all = all.sort((a, b) => a - b);
const common = all[Math.floor(all.length / 2)] || 0;
const e3 = {};
for (const [k, w] of Object.entries(e2)) if (Math.abs(w - common) > (u === 'in' ? 0.002 : 0.05)) e3[k] = w;
data.sa = { w: common, e: e3, k: {} };

Rules from a file with every size drawn

64-bit

Grade rules worked out from blocks that draw the piece in each size.

rule = pointₛ − point_base → toFixed(4)

Differences are taken point by point and rounded to 4 decimals. They are used only when every size has the same number of outline points; otherwise the piece comes in ungraded with a note.

  • addons/apparel/lib/aama.js
  • lines 501 to 506
  • CadCraft 0.8.2
const same = per.every((q) => q && q.data.pts.length === d.pts.length);
d.sizes = { list: sizes, base: baseEntry.size };
d.size = baseEntry.size;
d.grades = {};
if (same) {
  d.pts.forEach((p, i) => { const r = per.flatMap((q) => [+(q.data.pts[i][0] - p[0]).toFixed(4), +(q.data.pts[i][1] - p[1]).toFixed(4)]); if (r.some((v) => Math.abs(v) > 1e-9)) d.grades['p' + i] = r; });

Rules from a RUL file

64-bit

Grade rules of a piece from the rule numbers on its points.

k = (25.4 or 1) ÷ (25.4 or 1); move = (ruleₛ − rule_sample) × k → toFixed(4)

The unit factor is 25.4, 1 ÷ 25.4 or 1, from the units the two files state. Each move is re-measured from the sample size and rounded to 4 decimals.

  • addons/apparel/lib/aama.js
  • lines 510 to 519
  • CadCraft 0.8.2
const k = (rul.units.startsWith('ENG') ? 25.4 : 1) / (u === 'in' ? 25.4 : 1);
d.sizes = { list: sizes, base: bi >= 0 ? rul.sample : sizes[0] };
d.size = d.sizes.base;
d.grades = {};
const put = (key, n) => {
  const r = rul.rules.get(n);
  if (!r || r.length < sizes.length) return;
  const b = bi >= 0 ? r[bi] : [0, 0];
  const flat = sizes.flatMap((_, i) => [+((r[i][0] - b[0]) * k).toFixed(4), +((r[i][1] - b[1]) * k).toFixed(4)]);
  if (flat.some((v) => Math.abs(v) > 1e-9)) d.grades[key] = flat;

Fitting a large piece into one object

64-bit

Shrinks the data of a piece that is too large for one drawing object.

rules → 2 decimals (mm) or 4 (in); points → 3 decimals (mm) or 5 (in); then curve points thinned at 0.02, 0.05, 0.1, 0.2, 0.3, 0.5, 1 mm

Runs only when the piece's data is over 11,500 characters. Rounding is tried first; if that is not enough, curve points within the tolerance of the line between kept points are removed, with the tolerance growing until the piece fits. Corners and points a notch refers to are always kept.

  • addons/apparel/lib/aama.js
  • lines 550 to 587
  • CadCraft 0.8.2
/**
 * A piece that fits in a CadCraft object (its JSON ≤ limit): grade rules rounded, then curve points thinned within a growing
 * tolerance (corners, and every point a notch or allowance refers to, are kept; a thinned point's rule goes — the points
 * either side carry the curve, and unruled curve points follow in proportion). → piece | null (still too big).
 */
export function fitPiece(d, limit = 11500) {
  if (!d) return null;
  const size = (x) => JSON.stringify(x).length;
  if (size(d) <= limit) return d;
  const r = structuredClone(d);
  for (const k of Object.keys(r.grades || {})) r.grades[k] = r.grades[k].map((v) => +v.toFixed(r.u === 'in' ? 4 : 2));
  r.pts = r.pts.map((p) => [+p[0].toFixed(r.u === 'in' ? 5 : 3), +p[1].toFixed(r.u === 'in' ? 5 : 3), p[2]]);
  if (size(r) <= limit) return r;
  const unit = r.u === 'in' ? 1 / 25.4 : 1;
  for (const tol of [0.02, 0.05, 0.1, 0.2, 0.3, 0.5, 1]) {
    const pts = ptsOf(r.pts), n = pts.length;
    const keep = new Set(pts.map((p, i) => (!p.c ? i : -1)).filter((i) => i >= 0));
    for (const nn of r.notches || []) keep.add(nn.i);
    if (!keep.size) keep.add(0);
    const ks = [...keep].sort((a, b) => a - b);
    for (let k = 0; k < ks.length; k++) {
      const a = ks[k], b = k + 1 < ks.length ? ks[k + 1] : ks[0] + n;
      const run = []; for (let i = a; i <= b; i++) run.push(pts[i % n]);
      const kk = new Set();
      keepRun(run, 0, run.length - 1, tol * unit, kk);
      for (const j of kk) keep.add((a + j) % n);
    }
    const order = [...keep].sort((a, b) => a - b);
    const map = new Map(order.map((old, i) => [old, i]));
    const t = structuredClone(r);
    t.pts = order.map((i) => r.pts[i]);
    t.notches = (r.notches || []).map((x) => ({ ...x, i: map.get(x.i) })).filter((x) => x.i !== undefined);
    if (t.sa) { const re = (o) => Object.fromEntries(Object.entries(o || {}).map(([k2, v]) => [map.get(+k2), v]).filter(([k2]) => k2 !== undefined)); t.sa = { ...t.sa, e: re(t.sa.e), k: re(t.sa.k) }; }
    if (t.grades) t.grades = Object.fromEntries(Object.entries(t.grades).map(([k2, v]) => { const m = /^p(\d+)$/.exec(k2); if (!m) return [k2, v]; const ni = map.get(+m[1]); return ni === undefined ? null : ['p' + ni, v]; }).filter(Boolean));
    if (size(t) <= limit) return t;
  }
  return null;
}

Thinning a run of points

64-bit

Which curve points of a run must stay so the line stays within a tolerance.

d = |(P − A) × (B − A)| ÷ |B − A|; the farthest point stays when d > tol

Douglas–Peucker by recursion. For a chord shorter than 1e-12 the plain distance to its start is used.

  • addons/apparel/lib/aama.js
  • lines 536 to 548
  • CadCraft 0.8.2
/** Douglas–Peucker over a run of points: the indices to keep (first and last always). */
function keepRun(pts, a, b, tol, keep) {
  let worst = -1, wd = tol;
  const A = pts[a], B = pts[b], dx = B.x - A.x, dy = B.y - A.y, L = Math.hypot(dx, dy);
  for (let i = a + 1; i < b; i++) {
    const d = L > 1e-12 ? Math.abs((pts[i].x - A.x) * dy - (pts[i].y - A.y) * dx) / L : Math.hypot(pts[i].x - A.x, pts[i].y - A.y);
    if (d > wd) { wd = d; worst = i; }
  }
  if (worst < 0) return;
  keep.add(worst);
  keepRun(pts, a, worst, tol, keep);
  keepRun(pts, worst, b, tol, keep);
}

In detail

Method
R12 ASCII DXF, one block per piece; rules as the per-size difference of each written point; reading matches rule numbers and turn points to outline points by distance.
Number types
64-bit numbers; decimal text in the files.
Tolerances
Written coordinates: 6 decimals. RUL rule values: 2 decimals. Matching tolerance when reading: max(1e-6, 1e-7 × largest coordinate) + 0.02 mm (0.001 in). A turn above 0.35 rad with no turn point is a corner; written corners are turns above 0.17 rad. Seam allowance read per edge is rounded to 2 decimals in mm (4 in inches).
Limits
In a RUL file a rule value is no finer than 0.01 of the file's unit: 0.01 mm in metric files and 0.01 in (0.254 mm) in inch files. Curves are written as the sampled polyline. A piece too large for one drawing object is thinned on reading, with tolerances from 0.02 mm to 1 mm.

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