The numbers a LISP routine gets from the built-in arithmetic, number and angle formatting, unit conversion and point geometry functions.
How the precision is kept
Reals are 64-bit numbers. Integers are 32-bit, as in the language: a whole-number result is wrapped to 32 bits with a bitwise operation, so an all-integer sum, product, power or FIX beyond ±2³¹ wraps around without a message. Unit factors are the exact defined values. Geometry functions are single closed-form expressions.
Open item. Decide whether a whole-number result outside the 32-bit range should become a real instead of wrapping around.
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 four arithmetic functions on integers and reals.
all integers: 32-bit result; any real: 64-bit result; integer ÷ integer truncates toward zero
With a real among the arguments the result is a plain 64-bit chain in argument order. With integers only, the sum is formed in 64-bit and wrapped to 32 bits, and the product uses Math.imul, so a result beyond ±2³¹ wraps around silently. Division by zero is an error.
lisp/builtins.js
lines 142 to 162
CadCraft 0.8.2
d('+', (a) => { a.forEach(num); return allInt(a) ? int(a.reduce((s, x) => s + x.v, 0)) : a.reduce((s, x) => s + numVal(x), 0); });
d('-', (a) => {
a.forEach(num);
if (!a.length) return int(0);
if (a.length === 1) return isInt(a[0]) ? int(-a[0].v) : -a[0];
const r = a.slice(1).reduce((s, x) => s - numVal(x), numVal(a[0]));
return allInt(a) ? int(r) : r;
});
d('*', (a) => { a.forEach(num); if (!a.length) return int(0); return allInt(a) ? int(a.reduce((s, x) => Math.imul(s, x.v), 1)) : a.reduce((s, x) => s * numVal(x), 1); });
d('/', (a) => {
a.forEach(num);
if (!a.length) return int(0);
let ints = isInt(a[0]), r = numVal(a[0]);
for (const x of a.slice(1)) {
const v = numVal(x);
if (v === 0) throw new LispError('divide by zero');
ints = ints && isInt(x);
r = ints ? Math.trunc(r / v) : r / v;
}
return ints ? int(r) : r;
});
FIX: a real to an integer
64-bit
Drops the fraction of a number.
int(trunc(x))
Math.trunc drops the fraction, then the integer type applies value | 0. For |x| of 2³¹ or more that bitwise step wraps the value: (fix 3e9) gives −1294967296.
A negative square root and a logarithm of zero or less are errors, not NaN. EXPT with an integer base and a non-negative integer exponent returns a 32-bit integer, so (expt 2 31) wraps to −2147483648.
lisp/builtins.js
lines 181 to 184
CadCraft 0.8.2
d('SQRT', (a) => { arity(a, 1); const v = num(a[0]); if (v < 0) throw new LispError('function undefined for argument: ' + show(a[0])); return Math.sqrt(v); });
d('EXP', (a) => { arity(a, 1); return Math.exp(num(a[0])); });
d('LOG', (a) => { arity(a, 1); const v = num(a[0]); if (v <= 0) throw new LispError('function undefined for argument: ' + show(a[0])); return Math.log(v); });
d('EXPT', (a) => { arity(a, 2); const b = num(a[0]), e = num(a[1]); return isInt(a[0]) && isInt(a[1]) && e >= 0 ? int(Math.pow(b, e)) : Math.pow(b, e); });
EQUAL with a fuzz
64-bit
Whether two numbers or lists of numbers are equal within a given amount.
|a − b| ≤ fuzz
The fuzz is an absolute difference chosen by the routine; with none given the numbers must be identical.
lisp/builtins.js
lines 37 to 49
CadCraft 0.8.2
// ---- equality ----
export function equal(a, b, fuzz = 0) {
if (a === b) return true;
if (isNum(a) && isNum(b)) return Math.abs(numVal(a) - numVal(b)) <= fuzz;
if (a instanceof Cons && b instanceof Cons) {
while (a instanceof Cons && b instanceof Cons) { if (!equal(a.car, b.car, fuzz)) return false; a = a.cdr; b = b.cdr; }
return equal(a, b, fuzz);
}
if (a instanceof Ename && b instanceof Ename) return a.id === b.id;
if (a instanceof PickSet && b instanceof PickSet) return a === b;
if (a && b && typeof a.same === 'function') return a.same(b);
return false;
}
RTOS: a number as text
64-bit
Formats a length in decimal or in the drawing's other length formats.
decimal: v.toFixed(prec); 0 when |v| < 0.5 × 10⁻ᵖʳᵉᶜ
The decimal mode rounds once with toFixed; the precision is limited to 0 to 8 decimals by the caller. A routine that reads the text back gets the rounded value. Other modes go through the app's length formatter in units.js.
lisp/builtins.js
lines 63 to 72
CadCraft 0.8.2
/** rtos: mode 1 scientific, 2 decimal, 3 engineering, 4 architectural, 5 fractional; DIMZIN 8 drops trailing zeros. */
export function rtos(v, mode, prec, dimzin = 0) {
if (mode === 2) {
let s = (Math.abs(v) < 0.5 * Math.pow(10, -prec) ? 0 : v).toFixed(prec);
if (dimzin & 8 && s.includes('.')) s = s.replace(/0+$/, '').replace(/\.$/, '');
if (dimzin & 4 && /^-?0\./.test(s)) s = s.replace(/^(-?)0\./, '$1.');
return s;
}
return formatLength(v, { lunits: mode, prec });
}
ANGTOS: an angle as text
64-bit
Formats an angle in degrees, degrees-minutes-seconds, grads, radians or surveyor's bearings.
The angle is first brought into 0 to 2π by two remainders. Splitting into degrees and minutes adds 1e-12 and 1e-9 before taking the whole part, so a value a hair under a whole number is not printed one unit low. The cardinal bearings are recognized within 1e-9 degree.
lisp/builtins.js
lines 73 to 102
CadCraft 0.8.2
/** angtos: 0 degrees, 1 deg/min/sec, 2 grads, 3 radians, 4 surveyor's units. */
export function angtos(a, mode, prec) {
const TAU = Math.PI * 2;
a = ((a % TAU) + TAU) % TAU;
const fx = (v) => v.toFixed(prec);
if (mode === 0) return fx(a * 180 / Math.PI);
if (mode === 2) return fx(a * 200 / Math.PI) + 'g';
if (mode === 3) return fx(a) + 'r';
const dms = (deg) => {
let d = Math.floor(deg + 1e-12), m = (deg - d) * 60;
if (prec <= 0) return Math.round(deg) + 'd';
let mi = Math.floor(m + 1e-9), s = (m - mi) * 60;
if (prec <= 2) { mi = Math.round(m); if (mi === 60) { d++; mi = 0; } return d + 'd' + mi + "'"; }
const sd = Math.max(0, prec - 4);
let ss = Number(s.toFixed(sd));
if (ss >= 60) { ss = 0; mi++; } if (mi >= 60) { mi = 0; d++; }
return d + 'd' + mi + "'" + ss.toFixed(sd) + '"';
};
if (mode === 1) return dms(a * 180 / Math.PI);
// surveyor: N/S <angle> E/W from north/south
const deg = a * 180 / Math.PI;
if (Math.abs(deg - 90) < 1e-9) return 'E';
if (Math.abs(deg - 270) < 1e-9) return 'W';
if (Math.abs(deg) < 1e-9 || Math.abs(deg - 360) < 1e-9) return 'N';
if (Math.abs(deg - 180) < 1e-9) return 'S';
const ns = deg < 180 ? 'N' : 'S';
const off = deg < 180 ? Math.abs(90 - deg) : Math.abs(270 - deg);
const ew = deg > 90 && deg < 270 ? 'W' : 'E';
return ns + dms(off).replace(/^/, '') + ew;
}
ANGTOF: text to an angle
64-bit
Reads an angle written in any of the angle formats and returns radians.
degrees × π ÷ 180; d + m ÷ 60 + s ÷ 3600; grads × π ÷ 200
The text is converted with parseFloat or the unary plus, each to the nearest 64-bit value, then multiplied by the 64-bit π.
lisp/builtins.js
lines 103 to 118
CadCraft 0.8.2
/** angtof: text in a mode → radians, or null. */
export function angtof(s, mode = 0) {
const t = String(s).trim();
let m;
if (mode === 3 || /r$/i.test(t)) { const v = parseFloat(t); return Number.isFinite(v) ? v : null; }
if (mode === 2 || /g$/i.test(t)) { const v = parseFloat(t); return Number.isFinite(v) ? v * Math.PI / 200 : null; }
if ((m = t.match(/^([NS])\s*(.*?)\s*([EW])$/i))) {
const off = angtof(m[2], 1);
if (off === null) return null;
const base = m[1].toUpperCase() === 'N' ? 90 : 270, sgn = (m[1].toUpperCase() === 'N') === (m[3].toUpperCase() === 'E') ? -1 : 1;
return ((base * Math.PI / 180) + sgn * off + 2 * Math.PI) % (2 * Math.PI);
}
if ((m = t.match(/^(-?\d+(?:\.\d*)?)d(?:(\d+(?:\.\d*)?)')?(?:(\d+(?:\.\d*)?)")?$/i))) return (+m[1] + (+(m[2] || 0)) / 60 + (+(m[3] || 0)) / 3600) * Math.PI / 180;
if (/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t)) return parseFloat(t) * Math.PI / 180;
return null;
}
Unit factors for CVUNIT
64-bit
The factor of each unit to its base unit: meter, radian, kilogram or square meter.
inch 0.0254; foot 0.3048; yard 0.9144; mile 1609.344; pound 0.45359237; sq inch 0.00064516; sq foot 0.09290304; acre 4046.8564224
Every length, mass and area factor is the exact defined value, written in full, and stored as the nearest 64-bit number. The angle factors are π ÷ 180 and π ÷ 200 from the 64-bit π.
Converts a number or a list of numbers from one unit to another of the same kind.
result = v × from ÷ to
The value is multiplied by the source factor and divided by the target factor, two rounded operations through the base unit. Inches to millimeters is therefore v × 0.0254 ÷ 0.001, not v × 25.4, and can differ from it in the last digit.
lisp/builtins.js
lines 328 to 336
CadCraft 0.8.2
d('CVUNIT', (a) => {
arity(a, 3);
const from = UNIT[str(a[1]).trim().toLowerCase()], to = UNIT[str(a[2]).trim().toLowerCase()];
if (!from || !to || from[0] !== to[0]) return null;
const conv = (v) => v * from[1] / to[1];
if (isNum(a[0])) return conv(numVal(a[0]));
if (a[0] instanceof Cons) return fromArray(toArray(a[0]).map((x) => conv(num(x))));
throw bad('numberp', a[0]);
});
POLAR, DISTANCE and ANGLE
64-bit
A point at an angle and distance from another, the distance between two points, and the angle of the line between them.
q = p + d (cos a, sin a); dist = √(Δx² + Δy² + Δz²); angle = atan2(Δy, Δx) into 0 to 2π
Each is one 64-bit expression; the distance uses Math.hypot. The height difference counts only when both points have a z. A negative angle has 2π added.
The intersection of two lines or segments given by their end points.
den = Δ₁ × Δ₂; u = ((p₃ − p₁) × Δ₂) ÷ den; q = p₁ + u Δ₁
Lines count as parallel when |den| is below 1e-12 × the product of the two lengths, a relative test. For nearly parallel lines above that limit the division by a small den enlarges rounding errors. The on-segment test allows 1e-10 in the parameters. Only x and y are intersected; z is interpolated along the first line.
JavaScript 64-bit arithmetic and Math functions; Math.imul for integer products; toFixed for number text.
Number types
64-bit reals; 32-bit integers (value | 0).
Tolerances
INTERS: lines are parallel when the cross product is below 1e-12 × the product of their lengths (relative); the on-segment test allows 1e-10 in the line parameter. EQUAL's fuzz is an absolute difference given by the routine. Angle to text adds 1e-12 degree and 1e-9 minute before taking whole parts.
Limits
Integers wrap at 32 bits. RTOS and ANGTOS give at most 8 decimals.
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.