Rounds and reports the coordinates, lengths, areas and sheet sizes that an assistant or a script reads from a drawing, and writes a plot as SVG.
How the precision is kept
Every number handed to an assistant is rounded to 9 decimal places with Math.round(v × 10⁹) ÷ 10⁹, an absolute step, so a value below 1 keeps fewer than 15 significant digits. Lengths and areas are computed by geom.js in 64-bit and rounded only at the end. The SVG plot writes coordinates with 3 decimals in units of 0.1 mm and replaces arcs with chords of at most 2°.
Open item. Round the values given to assistants to 15 significant digits instead of 9 decimal places, so small values keep their digits.
Calculations under this policy
12 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 value is multiplied by 10⁹, rounded to a whole number and divided again: an absolute step of 10⁻⁹ in whatever unit the value has. A value below 1 therefore keeps fewer than 15 significant digits, and the division by 10⁹ is itself rounded to the nearest 64-bit number. Values that are not finite numbers pass through unchanged.
The drawing's extents and each sheet's paper and viewport sizes in the drawing's description.
The extents (plot.js extentsOf) and each viewport's width and height are rounded to 9 decimals; a sheet's paper width and height to 6 decimals. A viewport's scale is given as a label from sheets.js scaleLabel, not as a number.
One object as plain values, with its length and area where it has them.
length = Σ primLength(primitive); area = entityArea(object).area
Every numeric field at the top level of the object is rounded to 9 decimals as it is copied; numbers inside lists, such as a polyline's vertices, are copied as stored. The length is a sum over the object's primitives in order, rounded once at the end, and the area is reported only for shapes that are not open. If either calculation throws, the value is left out.
agents.js
lines 119 to 131
CadCraft 0.8.2
/** One object as plain JSON (+ length / area where it has one, and a one-line description in the drawing's units). */
export function plainObject(doc, e, { full = false } = {}) {
const o = {};
for (const [k, v] of Object.entries(e)) {
if (k.startsWith('_') || typeof v === 'function') continue;
if (!full && Array.isArray(v) && v.length > 200) { o[k] = v.slice(0, 200); o[k + 'Total'] = v.length; continue; }
o[k] = typeof v === 'number' ? num(v) : v;
}
try { const ps = prims(e, null); if (['line', 'arc', 'poly', 'circle', 'ellipse'].includes(e.type) && ps?.length) o.length = num(ps.reduce((s, q) => s + primLength(q), 0)); } catch { /* none */ }
try { if (['circle', 'poly', 'ellipse', 'hatch'].includes(e.type)) { const a = entityArea(e); if (a && Number.isFinite(a.area) && a.kind !== 'open') o.area = num(a.area); } } catch { /* none */ }
try { o.description = describeEntity(e, unitsOf(doc.meta)); } catch { o.description = ''; }
return o;
}
An object's state for change checks
64-bit
A flat set of an object's values that is compared later to see whether the object changed.
Numbers are rounded to 9 decimals before they are stored or hashed, so a change that does not move a field across a 10⁻⁹ step is not seen as a change. A long point list is reduced to a count and a 32-bit hash of its rounded text; the hash is only compared for equality.
agents.js
lines 133 to 153
CadCraft 0.8.2
/**
* An object's state as a flat set of values (EG.64, markups that act): what MarkupCraft's tasks_check compares to see whether the object
* changed. Numbers, short strings, booleans; point lists become a short list of numbers, or a count and a fingerprint when long.
* The id and the key are not state; fields that change without the object changing (selected, hover) are left out.
*/
export function elementState(e) {
const out = {};
const fnv = (str) => { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0).toString(16); };
for (const [k, v] of Object.entries(e)) {
if (k === 'id' || k === 'key' || k.startsWith('_') || typeof v === 'function' || !/^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(k)) continue;
if (typeof v === 'number') { if (Number.isFinite(v)) out[k] = num(v); }
else if (typeof v === 'boolean' || v === null) out[k] = v;
else if (typeof v === 'string') out[k] = v.slice(0, 200);
else if (Array.isArray(v)) {
const flat = v.every((x) => typeof x === 'number') ? v : v.every((x) => x && typeof x === 'object' && Number.isFinite(x.x) && Number.isFinite(x.y)) ? v.flatMap((q) => (q.b ? [num(q.x), num(q.y), num(q.b)] : [num(q.x), num(q.y)])) : null;
if (flat && flat.length <= 16) out[k] = flat.map((n) => (typeof n === 'number' ? num(n) : n));
else { out[k + 'Count'] = v.length; out[k + 'Print'] = fnv(JSON.stringify(v, (kk, vv) => (typeof vv === 'number' ? num(vv) : vv))); }
} else if (v && typeof v === 'object') out[k + 'Print'] = fnv(JSON.stringify(v));
}
return out;
}
Text height against the allowed list
64-bit
Whether a text's height is one of the heights a standards check allows.
|h − e.h| ≤ 10⁻⁶ × max(1, h)
The tolerance is relative to the allowed height for heights above 1 and a fixed 10⁻⁶ at or below 1. Text whose height is not a finite number is not checked.
The scale number for text such as 1:100, an architectural scale label, or fit.
a:b → n = b ÷ a
The two sides of a typed ratio are read from the text and divided once. Any other text must equal a label in the drawing's scale list after spaces and quote marks are normalized; text that matches nothing throws an error rather than giving a scale.
agents.js
lines 495 to 505
CadCraft 0.8.2
/** "1:100" / "1/4\" = 1'-0\"" / "fit" → n (sheets.js counting) or null for fit. */
export function parseScale(doc, s) {
if (s === undefined || s === null || s === '' || /^fit/i.test(String(s))) return null;
const str = String(s).trim();
const r = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(str);
if (r && +r[1] > 0 && +r[2] > 0) return +r[2] / +r[1];
const norm = (x) => x.replace(/\s+/g, '').replace(/[’′]/g, "'").replace(/[”″]/g, '"').toLowerCase();
const hit = scaleList(doc).find((o) => norm(o.label) === norm(str));
if (hit) return hit.n;
throw new Error('Unknown scale "' + str + '". Use 1:n, an architectural scale such as 1/4" = 1\'-0", or fit.');
}
Sheet a plot prints
64-bit
The sheet and its size for a plot asked for by an assistant: a layout, the extents, or a window of the model.
A window is put in order with Math.min and Math.max, with no arithmetic. The sheet width and height that are reported are rounded to 9 decimals; the display list is built by plot.js from the unrounded values.
agents.js
lines 516 to 532
CadCraft 0.8.2
/** The sheet a plot prints and its display list (plot.js), as the Plot dialog makes them. */
export function plotList(doc, { layout = null, area = 'extents', paper = 'iso-a3', portrait = false, scale = null, mono = true } = {}) {
let sheet, lay = null;
if (layout) {
lay = (doc.layouts || []).find((l) => l.name.toLowerCase() === String(layout).toLowerCase());
if (!lay) throw new Error('There is no sheet "' + layout + '". Sheets: ' + ((doc.layouts || []).map((l) => l.name).join(', ') || 'none') + '.');
sheet = layoutSheet(doc, lay);
} else {
if (!paperByKey(paper)) throw new Error('Papers: ' + PAPERS.map((p) => p.key).join(', ') + '.');
const win = area && typeof area === 'object' ? area : null;
const box = win ? { minX: Math.min(win.minX, win.maxX), minY: Math.min(win.minY, win.maxY), maxX: Math.max(win.minX, win.maxX), maxY: Math.max(win.minY, win.maxY) } : extentsOf(doc);
if (!box) throw new Error('There is nothing to plot.');
sheet = modelSheet(doc, { area: box, paper, portrait: !!portrait, n: parseScale(doc, scale) });
}
const list = displayList(doc, sheet, plotStyleOptions(doc, lay, mono));
return { list, sheet: { w: num(list.w), h: num(list.h), scale: sheet.scale?.label || null, fits: sheet.scale ? sheet.scale.fits !== false : true } };
}
Size and units of the SVG plot
64-bit
The SVG file's size in millimeters and the number of file units per millimeter.
file units = mm × 10; width = sheet width in mm, rounded to 9 decimals
The plot is drawn at K = 10 file units per millimeter, so the 3-decimal rounding of coordinates is a step of 0.0001 mm on paper. The width and height attributes are the sheet size in millimeters rounded to 9 decimals.
agents.js
lines 541 to 549
CadCraft 0.8.2
/** An SVG of a sheet or of the model, sized in mm: {bytes, sheet}. Pictures are left out (as in the plot preview). */
export async function plotSvg(doc, o = {}) {
const { list, sheet } = plotList(doc, o);
const K = 10; // drawing units per mm in the file (text under 0.06 mm high is left out, as the preview does)
const g = svgContext();
drawPreview(g, list, { pxPerMm: K, pen: +o.pen || 0.25, font: 'Helvetica, Arial, sans-serif' });
const text = g.svg(list.w * K, list.h * K, { width: num(list.w) + 'mm', height: num(list.h) + 'mm', title: (doc.meta.name || 'Drawing1') + (o.layout ? ' – ' + o.layout : '') });
return { bytes: new TextEncoder().encode(text), sheet };
}
Coordinates in the SVG plot
64-bit
How points, widths and matrix entries are transformed and written into the SVG file.
(x′, y′) = M · (x, y, 1); text = round(v × 1000) ÷ 1000; width scale = √|det M|
Points are transformed by a 2 × 3 matrix in 64-bit and then every number written to the file is rounded to 3 decimals with Math.round(v × 1000) ÷ 1000. The stroke width is multiplied by the square root of the matrix's determinant before it is rounded.
n = max(2, ⌈|sweep| ÷ 2°⌉); pᵢ = center + r·(cos, sin)(a₀ + sweep·i ÷ n)
An arc is written as chords of at most 2° (π ÷ 90) with their ends on the arc. The largest gap between a chord and the arc is r × (1 − cos 1°), about 0.00015 × r. Each point's angle is a₀ + sweep × i ÷ n, computed directly, not by adding steps.
agents.js
lines 585 to 590
CadCraft 0.8.2
arc(cx, cy, r, a0, a1, ccw = false) {
let sweep = a1 - a0;
if (ccw) { while (sweep > 0) sweep -= 2 * Math.PI; } else { while (sweep < 0) sweep += 2 * Math.PI; }
const n = Math.max(2, Math.ceil(Math.abs(sweep) / (Math.PI / 90)));
for (let i = 0; i <= n; i++) { const a = a0 + (sweep * i) / n; path.push([i || path.length ? 'L' : 'M', ...P(cx + r * Math.cos(a), cy + r * Math.sin(a))]); }
},
Text in the SVG plot
64-bit
The matrix, position and size written for a piece of text.
transform = matrix(a b c d e f), each entry rounded to 3 decimals
A text element carries the current matrix as six numbers, each rounded to 3 decimals, so the direction and scale of rotated text are held to 3 decimals as well as its position. The font size is read from the canvas font string and rounded the same way.
The command-line input that places a block at a given point, scale and rotation.
The point is written as text after rounding to 9 decimals and is then read by the command line's point parser, so the stored insertion point is the given one rounded to 10⁻⁹ drawing units. Scale and rotation are written with String, which gives the shortest text that reads back to the same 64-bit number.
Length = the sum of geom.js primLength over the object's primitives; area from geom.js entityArea; rounding by Math.round at a fixed decimal place.
Number types
64-bit numbers, rounded to 9 decimals (paper sizes to 6) before they are returned. SVG coordinates are text with 3 decimals.
Tolerances
checkDrawing accepts a text height within 10⁻⁶ × max(1, h) of an allowed height h.
Limits
The 9-decimal rounding is absolute: 0.000123456789012 is returned as 0.000123457. From about 9 × 10⁶ upward v × 10⁹ is beyond 2⁵³ and the rounding step no longer changes the value. In the SVG a full circle is 180 chords and a text's matrix entries are held to 3 decimals.
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.