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