Resources Numerical accuracy Calculation policy

Drawing values handed to and from LISP routines

The numbers that pass between a LISP routine and the drawing: answers typed for a command, object data lists, object properties, system variables and coordinate transforms.

How the precision is kept

Coordinates, radii and angles cross between routine and drawing as the 64-bit numbers they are. Two things change a value: a plain number a routine gives to a command is passed as text with 15 significant digits, and lineweights are converted between millimeters and whole hundredths of a millimeter. Derived properties such as length and area are single 64-bit expressions.

Open item. Pass a routine's number to a command as the number itself, or as text with 17 significant digits, so it arrives unchanged.

Calculations under this policy

18 calculations in 3 source files. 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.

lisp/host-cad.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-cadcraft-lisp.mjs

A routine's number as command input

64-bit

Turns a number a routine gives to a command into the text the command reads.

text = String(+v.toPrecision(15))

The number is rounded to 15 significant digits before the command parses it again. A 64-bit number needs up to 17 digits to come back unchanged, so a radius or distance given this way can differ from the routine's value in its last digits.

  • lisp/host-cad.js
  • line 689
  • CadCraft 0.8.2
const numText = (v) => (Number.isInteger(v) ? String(v) : String(+v.toPrecision(15)));

Answers to a command's prompts

64-bit

Hands a routine's string, number, point or object to the command that is waiting for input.

point: UCS to world, then given as a click; number: as 15-digit text

A point for a point, distance, angle or selection prompt is given to the command as 64-bit coordinates. A point for any other prompt and every plain number go through the 15-digit text.

  • lisp/host-cad.js
  • lines 303 to 326
  • CadCraft 0.8.2
feed(w, a) {
  const E = this.engine;
  if (typeof a === 'string') { E.text(a); return; }
  if (typeof a === 'number' || isI(a)) { E.text(numText(vnum(a))); return; }
  if (Array.isArray(a) && a.length >= 2 && a.every((x) => typeof x === 'number' || isI(x))) {
    const p = ucsToWorld(activeUcs(this.doc), { x: vnum(a[0]), y: vnum(a[1]) });   // a routine's point is in the UCS
    if (w.kind === 'point' || w.kind === 'distance' || w.kind === 'angle' || w.kind === 'select' || w.kind === 'entity') { if (!E.click(p, p) && w.kind === 'select') { /* nothing there */ } return; }
    E.text(numText(p.x) + ',' + numText(p.y));
    return;
  }
  const pickIds = (ids) => {
    if (w.kind === 'select') { for (const id of ids) if (this.doc.get(id)) w.set.add(id); this.ui.setSelection(w.set); this.print(ids.length + ' found'); return; }
    if (w.kind === 'entity' && ids.length) { const e = this.doc.get(ids[0]); if (e) E.resolve({ id: e.id, point: anyPoint(e) }); }
  };
  if (a && a.e !== undefined) { pickIds([a.e]); return; }
  if (a && Array.isArray(a.ids)) { pickIds(a.ids); return; }
  if (Array.isArray(a) && a[0] && a[0].e !== undefined) {   // an entsel result: (ename point)
    const p = Array.isArray(a[1]) ? ucsToWorld(activeUcs(this.doc), { x: vnum(a[1][0]), y: vnum(a[1][1]) }) : null;
    if (w.kind === 'entity' && p) { E.resolve({ id: a[0].e, point: p }); return; }
    pickIds([a[0].e]);
    return;
  }
  E.text(String(a));
}

Block-to-world matrix of a nested pick

64-bit

The transform from a block's own coordinates to the drawing for an object picked inside nested block references.

M = M · [rotation × scale, insertion − base]; pick in block = M⁻¹ · pick

Each level multiplies a 2 × 3 matrix built from the reference's cosine, sine, scales and base point. The inverse divides by the determinant, which must be at least 1e-18 in size, an absolute limit that a very small scale can fall below.

  • lisp/host-cad.js
  • lines 139 to 172
  • CadCraft 0.8.2
/** nentsel: the object inside a block reference nearest the pick, its block-to-world matrix and the references. */
nestedAt(id, pick) {
  const p = [pick.x, pick.y, 0];
  let e = this.doc.get(id);
  if (!e || e.type !== 'insert') return { e: id, p };
  const parents = [];
  let m = [1, 0, 0, 1, 0, 0];   // [a b c d tx ty]: world = M · block
  let ref = null;
  for (let depth = 0; e && e.type === 'insert' && depth < 16; depth++) {
    const b = this.doc.blocks.get(e.block);
    if (!b) break;
    parents.unshift(ref === null ? { e: id } : ref);
    const cs = Math.cos(e.rot || 0), sn = Math.sin(e.rot || 0), sx = e.sx ?? 1, sy = e.sy ?? 1;
    const L = [cs * sx, sn * sx, -sn * sy, cs * sy, e.x - (b.base?.x || 0) * cs * sx + (b.base?.y || 0) * sn * sy, e.y - (b.base?.x || 0) * sn * sx - (b.base?.y || 0) * cs * sy];
    m = [m[0] * L[0] + m[2] * L[1], m[1] * L[0] + m[3] * L[1], m[0] * L[2] + m[2] * L[3], m[1] * L[2] + m[3] * L[3], m[0] * L[4] + m[2] * L[5] + m[4], m[1] * L[4] + m[3] * L[5] + m[5]];
    // the pick in block coordinates
    const det = m[0] * m[3] - m[1] * m[2];
    if (Math.abs(det) < 1e-18) break;
    const dx = pick.x - m[4], dy = pick.y - m[5];
    const q = { x: (m[3] * dx - m[2] * dy) / det, y: (-m[1] * dx + m[0] * dy) / det };
    let best = -1, bd = Infinity;
    b.entities.forEach((be, k) => {
      const bb = entityBounds(be, this.doc.blocks);
      if (!bb) return;
      const d = Math.hypot(Math.max(bb.minX - q.x, 0, q.x - bb.maxX), Math.max(bb.minY - q.y, 0, q.y - bb.maxY));
      if (d < bd) { bd = d; best = k; }
    });
    if (best < 0) break;
    ref = { e: 'b:' + b.name + ':' + best };
    e = b.entities[best];
  }
  if (!ref) return { e: id, p };
  return { e: ref.e, p, matrix: [[m[0], m[2], 0], [m[1], m[3], 0], [0, 0, 1], [m[4], m[5], 0]], parents: parents.slice(1).reverse().concat([{ e: id }]).reverse() };
}

TRANS: between coordinate systems

64-bit

Converts a point or a direction between the world, the user coordinate system and the display.

display: turn by the view's twist; UCS: by the active coordinate system (ucs.js)

The display turn computes the cosine and sine of the twist for each component. The height of a 3D point is passed through unchanged.

  • lisp/host-cad.js
  • lines 330 to 343
  • CadCraft 0.8.2
/**
 * (trans pt from to [disp]): between 0 = world, 1 = the UCS and 2 = the display (the view's turn about its target, the
 * origin); an entity name or an extrusion vector stands for the world (2D objects lie in it). `disp` = a direction.
 */
trans(p, from, to, disp = false) {
  const u = activeUcs(this.doc), tw = this.ui.viewTwist?.() || 0;
  const code = (x) => (typeof x === 'number' && (x === 1 || x === 2) ? x : 0);
  const q = { x: +p[0] || 0, y: +p[1] || 0 };
  const rot = (v, a) => ({ x: v.x * Math.cos(a) - v.y * Math.sin(a), y: v.x * Math.sin(a) + v.y * Math.cos(a) });
  const toW = { 0: (v) => v, 1: (v) => (disp ? vecToWorld(u, v) : ucsToWorld(u, v)), 2: (v) => rot(v, tw) };
  const fromW = { 0: (v) => v, 1: (v) => (disp ? vecToUcs(u, v) : toUcs(u, v)), 2: (v) => rot(v, -tw) };
  const r = fromW[code(to)](toW[code(from)](q));
  return p.length > 2 ? [r.x, r.y, +p[2] || 0] : [r.x, r.y];
}

Current lineweight as a system variable

64-bit

The current lineweight in hundredths of a millimeter.

CELWEIGHT = round(lineweight × 100)

The lineweight in millimeters is multiplied by 100 and rounded to a whole number. Setting the variable, in setvar of the same file, divides by 100.

  • lisp/host-cad.js
  • lines 352 to 353
  • CadCraft 0.8.2
case 'CELTYPE': return doc.current.linetype || 'ByLayer';
case 'CELWEIGHT': return I(Number.isFinite(doc.current.lineweight) ? (doc.current.lineweight < 0 ? doc.current.lineweight : Math.round(doc.current.lineweight * 100)) : -1);

Drawing extents as system variables

64-bit

EXTMIN and EXTMAX: the box around every object.

min and max of every object's bounds

Minimum and maximum of the object bounds that geom.js returns; nothing is rounded. An empty drawing returns ±1e20.

  • lisp/host-cad.js
  • lines 389 to 394
  • CadCraft 0.8.2
case 'EXTMIN': case 'EXTMAX': {
  let b = null;
  for (const e of doc.all()) { const x = entityBounds(e, doc.blocks); if (!x) continue; b = b ? { minX: Math.min(b.minX, x.minX), minY: Math.min(b.minY, x.minY), maxX: Math.max(b.maxX, x.maxX), maxY: Math.max(b.maxY, x.maxY) } : x; }
  if (!b) return n === 'EXTMIN' ? [1e20, 1e20, 1e20] : [-1e20, -1e20, -1e20];
  return n === 'EXTMIN' ? [b.minX, b.minY, 0] : [b.maxX, b.maxY, 0];
}

Date as a Julian day number

64-bit

The DATE system variable.

Julian date = milliseconds since 1970 ÷ 86 400 000 + 2 440 587.5

One division and one addition. The clock is read in whole milliseconds; the result is a number near 2.46 million with its fraction as the time of day.

  • lisp/host-cad.js
  • lines 401 to 402
  • CadCraft 0.8.2
case 'DATE': { const d = Date.now() / 86400000 + 2440587.5; return d; }
case 'CDATE': { const d = new Date(); const pad = (x, n = 2) => String(x).padStart(n, '0'); return Number(d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '.' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + pad(Math.floor(d.getMilliseconds() / 10))); }

Position of a block attribute

64-bit

The world position of an attribute of a block reference, as entget reports it.

p = insertion + R(rot) · ((d − base) × scale)

The base point is taken off first, then the scale and the rotation are applied and the insertion point added, in 64-bit.

  • lisp/host-cad.js
  • lines 505 to 508
  • CadCraft 0.8.2
// block coordinates → world: translate · rotate · scale · (− base)
const cs = Math.cos(e.rot || 0), sn = Math.sin(e.rot || 0), sx = e.sx ?? 1, sy = e.sy ?? 1, bx = b?.base?.x || 0, by = b?.base?.y || 0;
const lx = ((+d.x || 0) - bx) * sx, ly = ((+d.y || 0) - by) * sy;
const p = [e.x + lx * cs - ly * sn, e.y + lx * sn + ly * cs, 0];
lisp/dxflist.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests bin/test-cadcraft-lisp.mjs

Angles brought into 0 to 2π

64-bit

Normalizes an arc's, text's or block reference's angle for a data list, and when a routine writes one back.

a′ = ((a mod 2π) + 2π) mod 2π

Every angle, also one already inside 0 to 2π, has 2π added before the second remainder. That sum is rounded at the size of 2π, so the angle that comes back can differ from the one that went in by up to about 9e-16 rad. When a routine writes an arc back, the normalized angles are the ones stored.

  • lisp/dxflist.js
  • line 24
  • CadCraft 0.8.2
const na = (a) => ((a % TAU) + TAU) % TAU;

Lineweight in a data list

64-bit

An object's lineweight as group 370, in hundredths of a millimeter.

370 = round(lw × 100)

The value is rounded to a whole number first; the bitwise | 0 of the integer wrapper then acts on a whole number and truncates nothing.

  • lisp/dxflist.js
  • lines 67 to 68
  • CadCraft 0.8.2
if (Number.isFinite(e.lw)) out.push([370, I(e.lw < 0 ? e.lw : Math.round(e.lw * 100))]);
if (e.lts && e.lts !== 1) out.push([48, e.lts]);

Lineweight from a data list

64-bit

Sets an object's lineweight in millimeters from group 370.

lw = value ÷ 100

One division by 100. Negative values are the by-layer and by-block codes and are kept as they are.

  • lisp/dxflist.js
  • lines 155 to 156
  • CadCraft 0.8.2
if (has(370)) { const lw = get(pairs, 370); if (lw === -1) delete e.lw; else e.lw = lw < 0 ? lw : lw / 100; }
if (has(48)) { const s = get(pairs, 48); if (s > 0 && s !== 1) e.lts = s; else delete e.lts; }

Arc from a data list

64-bit

Makes or changes an arc from its center, radius and two angles.

center, radius as given; angles into 0 to 2π

The center and radius are copied; the radius must be greater than 0. The two angles are normalized by remainder before they are stored, which can change them by up to about 9e-16 rad.

  • lisp/dxflist.js
  • lines 175 to 182
  • CadCraft 0.8.2
case 'ARC': {
  need(10); need(40); need(50); need(51);
  e.type = 'arc';
  const c = pt2(pairs, 10, old && { x: old.cx, y: old.cy });
  Object.assign(e, { cx: c.x, cy: c.y, r: num(pairs, 40, old?.r ?? 1), a0: na(num(pairs, 50, old?.a0 ?? 0)), a1: na(num(pairs, 51, old?.a1 ?? 0)) });
  if (!(e.r > 0)) return { error: 'bad radius' };
  break;
}

Viewport scale from a data list

64-bit

Sets a layout viewport's scale from the view height a routine gives.

scale = paper height ÷ view height

One division, done only when the view height is greater than 0. toPairs in the same file reports the view height as paper height ÷ scale, so a value read and written back goes through two divisions.

  • lisp/dxflist.js
  • lines 258 to 267
  • CadCraft 0.8.2
case 'VIEWPORT': {
  if (!old) return { error: 'make viewports with MVIEW' };
  const c = pt2(pairs, 10), v = pt2(pairs, 12);
  if (c) { e.x = c.x; e.y = c.y; }
  if (v) { e.cx = v.x; e.cy = v.y; }
  if (has(40)) e.w = num(pairs, 40, e.w);
  if (has(41)) e.h = num(pairs, 41, e.h);
  if (has(45) && num(pairs, 45) > 0) e.s = e.h / num(pairs, 45);
  break;
}

Linetype pattern length

64-bit

The total length of a linetype's dashes and gaps in its table record.

length = Σ |xᵢ|

A sum of magnitudes in pattern order, so nothing cancels.

  • lisp/dxflist.js
  • line 343
  • CadCraft 0.8.2
export const ltypePairs = (lt) => [[0, 'LTYPE'], [2, lt.name], [70, I(0)], [3, lt.description || ''], [72, I(65)], [73, I((lt.pattern || []).length)], [40, (lt.pattern || []).reduce((s, x) => s + Math.abs(x), 0)], ...(lt.pattern || []).map((x) => [49, x])];
lisp/vla-host.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-cadcraft-lisp.mjs

Length and angle of a line

64-bit

The Length, Angle and Delta properties of a line.

length = √(Δx² + Δy²); angle = atan2(Δy, Δx) into 0 to 2π

One Math.hypot and one Math.atan2 of the end point differences.

  • lisp/vla-host.js
  • lines 245 to 247
  • CadCraft 0.8.2
if (P === 'LENGTH') return Math.hypot(e.x2 - e.x1, e.y2 - e.y1);
if (P === 'ANGLE') { const a = Math.atan2(e.y2 - e.y1, e.x2 - e.x1); return a < 0 ? a + 2 * Math.PI : a; }
if (P === 'DELTA') return P3(e.x2 - e.x1, e.y2 - e.y1);

Circle and arc properties

64-bit

Diameter, area and circumference of a circle; total angle, arc length and end points of an arc.

area = π r²; circumference = 2π r; sweep = ((a₁ − a₀) mod 2π + 2π) mod 2π; arc length = r × sweep; end = c + r (cos a, sin a)

Each is one 64-bit expression with Math.PI. The sweep has 2π added and removed, which rounds it at the size of 2π, up to about 9e-16 rad. An arc whose two angles are equal has a sweep of 0, not a full turn.

  • lisp/vla-host.js
  • lines 253 to 263
  • CadCraft 0.8.2
  if (P === 'DIAMETER') return 2 * e.r;
  if (P === 'AREA') return Math.PI * e.r * e.r;
  if (P === 'CIRCUMFERENCE') return 2 * Math.PI * e.r;
} else {
  const sweep = (((e.a1 - e.a0) % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
  if (P === 'STARTANGLE') return e.a0;
  if (P === 'ENDANGLE') return e.a1;
  if (P === 'TOTALANGLE') return sweep;
  if (P === 'ARCLENGTH') return e.r * sweep;
  if (P === 'STARTPOINT') return P3(e.cx + e.r * Math.cos(e.a0), e.cy + e.r * Math.sin(e.a0));
  if (P === 'ENDPOINT') return P3(e.cx + e.r * Math.cos(e.a1), e.cy + e.r * Math.sin(e.a1));

Ellipse radii

64-bit

The major and minor radius of an ellipse.

major = √(m_x² + m_y²); minor = major × ratio

One Math.hypot of the major axis vector, and one multiplication for the minor radius.

  • lisp/vla-host.js
  • lines 281 to 282
  • CadCraft 0.8.2
if (P === 'MAJORRADIUS') return Math.hypot(e.mx, e.my);
if (P === 'MINORRADIUS') return Math.hypot(e.mx, e.my) * e.ratio;

Lineweight property set by a routine

64-bit

Sets an object's lineweight in millimeters from a value in hundredths of a millimeter.

lw = value ÷ 100

One division by 100. The value −1 removes the object's own lineweight and other negative codes are kept as they are. The same file reports the property as round(lw × 100).

  • lisp/vla-host.js
  • lines 353 to 354
  • CadCraft 0.8.2
case 'LINEWEIGHT': if (n === -1) delete e.lw; else e.lw = n < 0 ? n : n / 100; break;
case 'LINETYPESCALE': e.lts = n; break;

In detail

Method
Direct copies of the object's fields; angles normalized by remainder; 2 × 3 affine matrices for nested block references; Julian date from the system clock.
Number types
64-bit numbers. Integer group values are wrapped to 32 bits (n | 0); they are counts, flags, color numbers and rounded lineweights.
Tolerances
A nested block transform is abandoned when its determinant is below 1e-18 (absolute). A named coordinate system matches the active one within 1e-9 in position and 1e-12 rad in angle.
Limits
A number passed to a command prompt carries 15 significant digits, not 17. Points passed to point prompts are not affected.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach