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.
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.
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.
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.
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.
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.
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.