Moves the points of a pattern piece by its grade rules to draw the piece in each size (Apparel add-on).
How the precision is kept
A rule is an x and y move per size, added to the base size's point in 64-bit numbers. Curve points without a rule move in proportion to their distance along the sampled outline. Every stored rule value and every graded coordinate is rounded to 6 decimals of the drawing unit.
Open item. Keep graded coordinates and rule values unrounded in memory and round only where text is written.
Calculations under this policy
8 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.
Used on every rule value and graded coordinate this file returns. The value kept is rounded to 1e-6 of the drawing unit whatever its size.
addons/apparel/lib/grade.js
line 64
CadCraft 0.8.2
const r6 = (v) => +(+v).toFixed(6);
Even step per size
64-bit
A rule that moves a point by the same step for each size away from the base.
move of size i = (i − base) × step
A whole number times the step, rounded to 6 decimals. The base size's move is exactly 0.
addons/apparel/lib/grade.js
lines 75 to 79
CadCraft 0.8.2
/** A rule moving `step` per size: base 0, each size above +step, each below −step. */
export function uniform(sizes, step) {
const b = sizes.list.indexOf(sizes.base);
return sizes.list.flatMap((_, i) => [r6((i - b) * step[0]), r6((i - b) * step[1])]);
}
Rules in proportion between two points
64-bit
Rules for the outline points between two ruled points, by distance along the outline.
s = distance from A ÷ distance A to B; rule = r_A + (r_B − r_A)·s
Distances are running lengths on the sampled outline, taken modulo its total length. Each value is rounded to 6 decimals.
addons/apparel/lib/grade.js
lines 90 to 104
CadCraft 0.8.2
/** Rules for the sew line points strictly between A and B (going round the way the outline runs), in proportion to distance. */
export function proportional(data, keyA, keyB) {
const ia = /^p(\d+)$/.exec(keyA)?.[1], ib = /^p(\d+)$/.exec(keyB)?.[1];
const out = {};
if (ia === undefined || ib === undefined || ia === ib) return out;
const pts = ptsOf(data.pts), n = pts.length, a = +ia, b = +ib;
const ra = ruleOf(data, keyA) || zero(data), rb = ruleOf(data, keyB) || zero(data);
const { d, L } = along(pts);
const span = ((d[b] - d[a]) % L + L) % L || L;
for (let i = (a + 1) % n; i !== b; i = (i + 1) % n) {
const s = (((d[i] - d[a]) % L + L) % L) / span;
out['p' + i] = ra.map((v, k) => r6(v + (rb[k] - v) * s));
}
return out;
}
Position of each point along the outline
64-bit
How far along the sampled outline each outline point lies, and the outline's length.
Read from the running length of the sampled outline at each point's sample index, so it carries the 16-piece sampling of the curves.
addons/apparel/lib/grade.js
lines 83 to 88
CadCraft 0.8.2
/** Where each sew line point sits along the sampled outline, and the outline's length. */
function along(pts) {
const { path, at } = sampled(pts, true);
const cum = cumulative(path);
return { d: at.map((s) => cum[Math.max(0, s)]), L: cum[cum.length - 1] };
}
The sew line in a size
64-bit
The outline points of a piece in one size.
point = base + move; unruled curve point: move = m_lo + (m_hi − m_lo)·s, s = d ÷ d_hi
The base size returns the stored points unchanged. In other sizes each coordinate is the sum of the base value and the move, rounded to 6 decimals. s is 0 when the span is below 1e-12.
addons/apparel/lib/grade.js
lines 107 to 132
CadCraft 0.8.2
/** The sew line at a size: ruled points move by their rule; unruled curve points in proportion within their edge. */
export function gradedPts(data, size) {
const i = sizeIndex(data, size);
const raw = data.pts || [];
if (i < 0 || i === sizeIndex(data)) return raw.map((p) => [...p]);
const pts = ptsOf(raw);
const move = raw.map((_, k) => { const r = ruleOf(data, 'p' + k); return r ? moveOf(r, i) : null; });
const { d, L } = along(pts);
for (const e of edges(pts, true)) {
// anchors inside this edge: its corners (moved or not) and every ruled point
const idx = e.idx;
const anchor = idx.map((k, j) => move[k] !== null || !pts[k].c || j === 0 || j === idx.length - 1);
for (let j = 1; j + 1 < idx.length; j++) {
const k = idx[j];
if (anchor[j]) continue;
let lo = j - 1; while (!anchor[lo]) lo--;
let hi = j + 1; while (!anchor[hi]) hi++;
const kl = idx[lo], kh = idx[hi];
const ml = move[kl] || [0, 0], mh = move[kh] || [0, 0];
const dl = d[kl], dk = ((d[k] - dl) % L + L) % L, dh = ((d[kh] - dl) % L + L) % L || L;
const s = dh > 1e-12 ? dk / dh : 0;
move[k] = [ml[0] + (mh[0] - ml[0]) * s, ml[1] + (mh[1] - ml[1]) * s];
}
}
return raw.map((p, k) => { const m = move[k] || [0, 0]; return [r6(p[0] + m[0]), r6(p[1] + m[1]), p[2]]; });
}
The whole piece in a size
64-bit
The piece with its grain line, fold line, internal lines, drill holes, buttons and label moved to one size.
mark = base + move of its rule, rounded to 6 decimals
Marks without a rule get a move of 0 and are still rounded to 6 decimals, so a mark of the base piece can change in its last digits when another size is drawn.
addons/apparel/lib/grade.js
lines 134 to 152
CadCraft 0.8.2
/** The whole piece at a size (sew line, marks, label); its size name set; no grading left on it. */
export function gradeData(data, size) {
const i = sizeIndex(data, size);
const d = structuredClone(data);
if (i < 0) return d;
d.pts = gradedPts(data, size);
const mv = (key, p) => { const m = moveOf(ruleOf(data, key), i); return [r6(p[0] + m[0]), r6(p[1] + m[1]), ...p.slice(2)]; };
if (d.grain) { d.grain.a = mv('ga', d.grain.a); d.grain.b = mv('gb', d.grain.b); }
if (d.fold) { d.fold.a = mv('fa', d.fold.a); d.fold.b = mv('fb', d.fold.b); }
d.lines = (d.lines || []).map((l, li) => ({ ...l, p: l.p.map((p, j) => mv('l' + li + '.' + j, p)) }));
d.drills = (d.drills || []).map((p, k) => mv('d' + k, p));
d.buttons = (d.buttons || []).map((p, k) => mv('b' + k, p));
d.holes = (d.holes || []).map((p, k) => mv('h' + k, p));
if (d.label) d.label = mv('lab', d.label);
d.size = data.sizes.list[i];
delete d.grades;
delete d.nest;
return d;
}
Rules after a change of sizes or base
64-bit
The rules re-measured from a new base size.
new move = old move − old move of the new base
One subtraction per value, rounded to 6 decimals. A rule left with all values at 1e-12 or below is dropped.
addons/apparel/lib/grade.js
lines 154 to 174
CadCraft 0.8.2
/** The piece with a new size range: rules kept by size name, new sizes start at 0 (said by `added`). → { data, added } */
export function withSizes(data, sizes) {
const d = structuredClone(data);
const old = data.sizes;
d.sizes = { list: [...sizes.list], base: sizes.base };
d.size = sizes.base;
const added = sizes.list.filter((s) => !old || !old.list.includes(s));
const grades = {};
const nb = sizes.list.indexOf(sizes.base);
for (const [k, r] of Object.entries(data.grades || {})) {
if (!old) continue;
// moves measured from the old base; a new base re-measures them from itself
const ob = old.list.indexOf(sizes.base);
const shift = ob >= 0 ? moveOf(r, ob) : [0, 0];
const nr = sizes.list.flatMap((s) => { const i = old.list.indexOf(s); const m = i >= 0 ? moveOf(r, i) : shift; return [r6(m[0] - shift[0]), r6(m[1] - shift[1])]; });
nr[2 * nb] = 0; nr[2 * nb + 1] = 0;
if (nr.some((v) => Math.abs(v) > 1e-12)) grades[k] = nr;
}
d.grades = grades;
return { data: d, added };
}
Rules read from a file
64-bit
Checks the grade rules of a piece that came from a file or the clipboard.
A rule is kept only when it has two numbers per size, all finite and at most 1e6 in size. The base size's pair is set to exactly 0. The numbers are not rounded here.
addons/apparel/lib/grade.js
lines 46 to 59
CadCraft 0.8.2
/** Grade rules from a file → { key: rule } with every rule the right length and every number finite. */
export function cleanGrades(raw, sizes, data) {
const out = {};
if (!raw || typeof raw !== 'object' || !sizes) return out;
const n = sizes.list.length * 2, b = sizes.list.indexOf(sizes.base);
for (const [k, v] of Object.entries(raw)) {
if (!KEY.test(k) || !Array.isArray(v) || v.length !== n || !v.every((x) => fin(+x))) continue;
if (k[0] === 'p' && !(data?.pts?.[+k.slice(1)])) continue;
const r = v.map(Number);
r[2 * b] = 0; r[2 * b + 1] = 0; // the base size never moves
out[k] = r;
}
return out;
}
In detail
Method
Point plus move; linear interpolation of moves by distance along the outline between the nearest points that have a rule or are corners.
Rounding to 6 decimals (toFixed(6)), absolute. A rule whose values are all below 1e-12 is removed. An interpolation span below 1e-12 gives weight 0. Rule values from a file must be finite and at most 1e6 in size.
Limits
The rounding is a fixed 1e-6 of the drawing unit, not a number of significant digits: a coordinate of 1,000 keeps 10 significant digits.
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.