Evaluates the formulas typed for parameters and dimensions: arithmetic, units, trigonometry in degrees, rounding functions and comparisons.
How the precision is kept
An expression is parsed to a tree and evaluated in 64-bit numbers; no text is run as code. Lengths are millimeters and angles degrees, with unit factors 25.4 and 304.8. Sine, cosine and tangent results are rounded to 15 significant digits so that sin(30) is 0.5; the inverse functions are not. Comparisons and the ceil, floor, int and round functions allow a relative slack of 1e-9. Division by exactly zero and any result that is not finite are errors.
Calculations under this policy
11 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.
What a number with a unit is multiplied by to give millimeters or degrees.
mm 1; cm 10; m 1000; in 25.4; ft 304.8; deg 1; rad 180 ÷ π
25.4 and 304.8 are the defined values of the inch and the foot in millimeters, written as decimals. The radian factor is 180 ÷ Math.PI, computed once in 64-bit.
Reads a number and converts it by the unit written right after it.
v = Number(text) × factor
The text is converted to the nearest 64-bit number and multiplied once by the unit factor. The result is a plain number in millimeters or degrees.
expr.js
lines 80 to 85
CadCraft 0.8.2
let v = Number(num[0]);
const at = i;
i += num[0].length;
const rest = src.slice(i).replace(/^ (?=[°"'])/, '');
const u = rest.match(UNIT_RE);
if (u) { v *= UNITS[u[1]]; i += u[0].length + (src.length - i - rest.length); }
Sine, cosine and tangent in degrees
64-bit
sin, cos and tan of an angle given in degrees.
sin(x) = clean(Math.sin(x × π ÷ 180)); tan refused when |cos| < 1e-12
The angle is multiplied by the rounded constant π ÷ 180, and the result is passed through clean, which rounds it to 15 significant digits. That makes sin(30) exactly 0.5 and sin(180) exactly 0, and it rounds every other result to 15 significant digits as well.
expr.js
lines 33 to 35
CadCraft 0.8.2
sin: { min: 1, max: 1, fn: (x) => clean(Math.sin(x * D2R)) },
cos: { min: 1, max: 1, fn: (x) => clean(Math.cos(x * D2R)) },
tan: { min: 1, max: 1, fn: (x) => { const c = Math.cos(x * D2R); if (Math.abs(c) < 1e-12) throw new ExprError('tan() of 90° has no value.'); return clean(Math.tan(x * D2R)); } },
Trig result rounded to 15 digits
64-bit
Rounds a sine, cosine or tangent to 15 significant digits and turns a value below 1e-15 into 0.
A deliberate rounding of the value that is kept: the result can differ from the 64-bit sine by up to 5 units in the 16th significant digit. The cut to 0 is absolute at 1e-15.
expr.js
lines 65 to 66
CadCraft 0.8.2
/** sin(180) is 1.2e-16 and sin(30) 0.49999999999999994 in floating point: trig results are kept to 15 digits. */
function clean(x) { return Math.abs(x) < 1e-15 ? 0 : Number(x.toPrecision(15)); }
An argument up to 1e-9 outside −1 to 1 (or below 0 for sqrt) is clamped instead of refused, so rounding in an earlier step cannot produce NaN. The results are not passed through clean: asin(0.5) returns 30.000000000000004 while sin(30) returns 0.5.
expr.js
lines 36 to 40
CadCraft 0.8.2
asin: { min: 1, max: 1, fn: (x) => { if (x < -1 - EPS || x > 1 + EPS) throw new ExprError('asin() takes a value from -1 to 1.'); return Math.asin(Math.max(-1, Math.min(1, x))) / D2R; } },
acos: { min: 1, max: 1, fn: (x) => { if (x < -1 - EPS || x > 1 + EPS) throw new ExprError('acos() takes a value from -1 to 1.'); return Math.acos(Math.max(-1, Math.min(1, x))) / D2R; } },
atan: { min: 1, max: 1, fn: (x) => Math.atan(x) / D2R },
atan2: { min: 2, max: 2, fn: (y, x) => Math.atan2(y, x) / D2R },
sqrt: { min: 1, max: 1, fn: (x) => { if (x < -EPS) throw new ExprError('sqrt() of a negative number has no value.'); return Math.sqrt(Math.max(0, x)); } },
Power, exponential and logarithms
64-bit
pow, exp, ln and log.
pow(a, b) = aᵇ; ln and log need x > 0
The 64-bit library functions with a check that the result is finite; ln and log refuse zero and negative numbers. No rounding is added.
expr.js
lines 41 to 44
CadCraft 0.8.2
pow: { min: 2, max: 2, fn: (a, b) => fin(a ** b, 'pow') },
exp: { min: 1, max: 1, fn: (x) => fin(Math.exp(x), 'exp') },
ln: { min: 1, max: 1, fn: (x) => { if (x <= 0) throw new ExprError('ln() takes a number above zero.'); return Math.log(x); } },
log: { min: 1, max: 1, fn: (x) => { if (x <= 0) throw new ExprError('log() takes a number above zero.'); return Math.log10(x); } },
The slack lets a value such as 2.9999999999 from earlier rounding count as 3. It also means floor(2.9999999995) is 3 and ceil(2.0000000005) is 2: anything within 1e-9 (relative, at least 1e-9) of a whole number is treated as that number.
Rounds to a number of decimal places from 0 to 10, halves away from zero.
y = |x| × 10ᵈ; r = ⌊y + 0.5 + 1e-9 × max(1, y)⌋; result = ± r ÷ 10ᵈ
The value is scaled, rounded with a relative slack of 1e-9 so a decimal tie such as 2.675 rounds up, and divided back. The division by 10ᵈ gives the nearest 64-bit number to the decimal result.
expr.js
lines 50 to 54
CadCraft 0.8.2
round: { min: 1, max: 2, fn: (x, d = 0) => {
if (!Number.isInteger(d) || d < 0 || d > 10) throw new ExprError('round(x, digits) needs whole digits from 0 to 10.');
const f = 10 ** d, y = Math.abs(x) * f, r = Math.floor(y + 0.5 + EPS * Math.max(1, y));
return (x < 0 ? -r : r) / f;
} },
Comparisons
64-bit
The six comparison operators, giving 1 or 0.
tol = 1e-9 × max(1, |a|, |b|); a == b when |a − b| ≤ tol; a < b when a < b − tol
The tolerance is relative above 1 and absolute (1e-9) below. Values closer than that are equal, so a strict less-than is false for them. This decides which branch of if() is taken.
expr.js
lines 206 to 216
CadCraft 0.8.2
case 'cmp': {
const a = ev(n.a), b = ev(n.b), tol = EPS * Math.max(1, Math.abs(a), Math.abs(b));
switch (n.op) {
case '<': return a < b - tol ? 1 : 0;
case '<=': return a <= b + tol ? 1 : 0;
case '>': return a > b + tol ? 1 : 0;
case '>=': return a >= b - tol ? 1 : 0;
case '==': return Math.abs(a - b) <= tol ? 1 : 0;
default: return Math.abs(a - b) > tol ? 1 : 0;
}
}
Arithmetic operators
64-bit
Addition, subtraction, multiplication, division, remainder and power.
a % b = a − b × ⌊a ÷ b⌋; division and remainder refuse b = 0
Each operator is one 64-bit operation in the order the expression gives. Only a divisor of exactly 0 is refused; a very small divisor goes through and a result that is not finite is an error at the end. The remainder takes the sign of the divisor.
expr.js
lines 217 to 228
CadCraft 0.8.2
case 'bin': {
const a = ev(n.a), b = ev(n.b);
switch (n.op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '^': { const r = a ** b; if (!Number.isFinite(r)) throw new ExprError(tr('expr.power_has_no_value'), n.pos); return r; }
default:
if (b === 0) throw new ExprError(tr('expr.expression_divides_zero'), n.pos);
return n.op === '/' ? a / b : a - b * Math.floor(a / b);
}
}
Number as text
64-bit
The text a number is written as when it is put back into an expression or a message.
Number(v.toPrecision(12)) as text
The value is rounded to 12 significant digits. A number stored through this text and read back is therefore the 12-digit value, not the 64-bit one it came from.
expr.js
lines 259 to 264
CadCraft 0.8.2
/** Shortest text for a number that reads back to the same value (to 12 significant digits). */
export function formatNumber(v) {
if (!Number.isFinite(v)) return String(v);
const r = Number(v.toPrecision(12));
return Object.is(r, -0) ? '0' : String(r);
}
In detail
Method
Tokenizer, recursive-descent parser and tree walker. Trigonometry converts with π ÷ 180.
Number types
64-bit numbers. Units: mm 1, cm 10, m 1000, in 25.4, ft 304.8, rad 180 ÷ π.
Tolerances
EPS = 1e-9. Comparisons: 1e-9 × max(1, |a|, |b|). ceil, floor, int and round: 1e-9 × max(1, |x|). asin, acos and sqrt accept arguments up to 1e-9 outside their range and clamp them. tan is refused when |cos| < 1e-12. Trig results below 1e-15 become 0.
Limits
Sine, cosine and tangent carry 15 significant digits, not 17. Two values within 1e-9 (relative) compare as equal. formatNumber writes 12 significant digits.
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.