Resources Numerical accuracy Calculation policy

Values given to assistants and scripts

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.

agents.js CadCraft 0.8.2 64-bit 12 calculations Reviewed

Tests bin/test-agents.mjs, bin/test-cadcraft.mjs

Rounding of reported numbers

64-bit

Rounds a number before it is handed to an assistant or a script.

num(v) = round(v × 10⁹) ÷ 10⁹; rnd6(v) = num(round(v × 10⁶) ÷ 10⁶)

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.

  • agents.js
  • lines 46 to 47
  • CadCraft 0.8.2
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.round(v * 1e9) / 1e9 : v);
const rnd6 = (v) => num(Math.round(v * 1e6) / 1e6);

Extents, paper and viewport sizes

64-bit

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.

  • agents.js
  • lines 106 to 111
  • CadCraft 0.8.2
extents: b ? { minX: num(b.minX), minY: num(b.minY), maxX: num(b.maxX), maxY: num(b.maxY) } : null,
objects: doc.model.size, byType: counts,
layers: [...doc.layers.values()].map((l) => ({ name: l.name, color: l.color, on: l.on !== false, frozen: !!l.frozen, locked: !!l.locked, plot: l.plot !== false, linetype: l.linetype || null, lineweight: l.lineweight ?? null, objects: perLayer.get(l.name) || 0 })),
blocks: [...doc.blocks.values()].filter((bk) => !bk.name.startsWith('*')).map((bk) => ({ name: bk.name, objects: bk.entities?.length || 0, uses: uses.get(bk.name) || 0, ...(nested.get(bk.name) ? { nested: nested.get(bk.name) } : {}), attributes: (bk.attdefs || []).map((a) => a.tag) })),
layouts: (doc.layouts || []).map((l) => ({ name: l.name, paper: l.paper ? { w: rnd6(l.paper.w), h: rnd6(l.paper.h), unit: l.paper.unit } : null, objects: l.entities.size,
  viewports: [...l.entities.values()].filter((e) => e.type === 'viewport').map((v) => ({ id: v.id, w: num(v.w), h: num(v.h), scale: v.s ? scaleLabel(doc, l.paper?.unit || 'mm', v.s) : null })) })),

An object's fields, length and area

64-bit

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.

  • agents.js
  • lines 234 to 235
  • CadCraft 0.8.2
const okH = O.textHeights.map(Number);
const ids = all.filter((e) => (e.type === 'text' || e.type === 'mleader') && Number.isFinite(e.h) && !okH.some((h) => Math.abs(h - e.h) <= 1e-6 * Math.max(1, h))).map((e) => e.id);

Plot scale from text

64-bit

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.

  • agents.js
  • lines 557 to 563
  • CadCraft 0.8.2
const f = (v) => String(Math.round(v * 1000) / 1000);
let st = { m: [1, 0, 0, 1, 0, 0], fillStyle: '#000', strokeStyle: '#000', lineWidth: 1, globalAlpha: 1, font: '10px sans-serif', textAlign: 'left', groups: 0 };
const stack = [];
let path = [], clips = 0;
const mul = (a, b) => [a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3], a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]];
const P = (x, y) => { const m = st.m; return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]]; };
const scaleOf = () => Math.sqrt(Math.abs(st.m[0] * st.m[3] - st.m[1] * st.m[2]));

Arcs in the SVG plot

64-bit

An arc written as a run of straight chords.

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.

  • agents.js
  • lines 595 to 598
  • CadCraft 0.8.2
fillText(text, x, y) {
  const m = /([\d.]+)px\s+(.*)$/.exec(st.font) || [0, '10', 'sans-serif'];
  const anchor = st.textAlign === 'center' ? 'middle' : st.textAlign === 'right' ? 'end' : 'start';
  out.push('<text transform="matrix(' + st.m.map(f).join(' ') + ')" x="' + f(x) + '" y="' + f(y) + '" font-size="' + f(+m[1]) + '" font-family="' + esc(m[2]) + '" text-anchor="' + anchor + '" fill="' + esc(st.fillStyle) + '"' + alpha() + ' xml:space="preserve">' + esc(text) + '</text>');

Insertion point of a block placed by an assistant

64-bit

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.

  • agents.js
  • line 466
  • CadCraft 0.8.2
const input = ['INSERT', name, 'S', String(+scale || 1), 'R', String(+rotation || 0), num(at[0]) + ',' + num(at[1]), ...asked.map((d) => (d.tag in given ? given[d.tag] : ''))];

In detail

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

Every policy that takes this approach