Resources Numerical accuracy Calculation policy

Typed lengths and length readouts in 3D

Reads a length typed in feet, inches, fractions or metric units into meters, and writes lengths, areas and volumes in the model's unit.

How the precision is kept

The model is in meters. A typed value is multiplied by its unit's factor in 64-bit; the inch (0.0254), foot (0.3048), yard (0.9144) and mile (1609.344) are the legal definitions, held as the nearest 64-bit numbers. Rounding happens only when a value is written as text: to a fraction of an inch or to a number of decimals. The stored length is not rounded.

Calculations under this policy

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

m3d/units.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-modelcraft.mjs

One typed length to meters

64-bit

Text such as 10'-6 1/2", 3/4", 320cm or a bare number, as meters.

m = sign × (feet × 0.3048 + (inches + num ÷ den) × 0.0254); m = sign × value × factor(suffix or model unit)

Feet and inches are each multiplied by their factor and then added: two products and one sum. The fraction is one 64-bit division. Metric suffixes use decimal factors (0.001, 0.01, 1e-6) that are the nearest 64-bit numbers.

  • m3d/units.js
  • lines 52 to 85
  • CadCraft 0.8.2
function parseOneLength(text, unit) {
  if (typeof text !== 'string') return null;
  let s = text.trim().replace(/[‘’′]/g, "'").replace(/[“”″]/g, '"').toLowerCase();
  if (!s) return null;
  let sign = 1;
  if (s[0] === '-') { sign = -1; s = s.slice(1).trim(); }
  else if (s[0] === '+') s = s.slice(1).trim();
  const N = '(\\d+(?:\\.\\d*)?|\\.\\d+)';
  // feet with optional inches and fraction: 10'  10'6"  10'-6 1/2"  10' 6.5
  let m = s.match(new RegExp(`^${N}\\s*'\\s*(?:-?\\s*${N}?\\s*(?:(\\d+)\\s*/\\s*(\\d+))?\\s*"?)?$`));
  if (m) {
    const ft = +m[1], inch = m[2] ? +m[2] : 0, fr = m[3] && +m[4] ? +m[3] / +m[4] : 0;
    return sign * (ft * FT + (inch + fr) * IN);
  }
  // inches with a fraction: 6 1/2"  6-1/2"
  m = s.match(new RegExp(`^${N}\\s*[- ]\\s*(\\d+)\\s*/\\s*(\\d+)\\s*("|in)?$`));
  if (m && +m[3]) {
    const v = (+m[1] + +m[2] / +m[3]);
    return sign * (unit === 'ft-in' || unit === 'in' || m[4] ? v * IN : v * unitScale(unit));
  }
  // a fraction alone: 3/4"  3/4
  m = s.match(/^(\d+)\s*\/\s*(\d+)\s*("|in)?$/);
  if (m && +m[2]) { const v = +m[1] / +m[2]; return sign * v * (m[3] ? IN : unitScale(unit)); }
  // number + suffix
  m = s.match(new RegExp(`^${N}\\s*(mm|cm|km|um|µm|μm|inches|inch|in|feet|foot|ft|yards|yard|yd|miles|mile|mi|mils|mil|thou|m|"|')?$`));
  if (m) {
    const v = +m[1];
    if (!Number.isFinite(v)) return null;
    return sign * v * (m[2] ? SUFFIX[m[2]] : unitScale(unit));
  }
  // every other length mark of the shared table (0.7 UNITS): 2 dm, 3 ch, 10 US ft, 400 nm, 1 nmi, 12 pt
  const r = parseUnit(text, 'length');   // the text as typed: case matters there (nm is not NM, the nautical mile)
  return r.ok ? r.value : null;
}

Meters per unit

64-bit

The factor that turns a bare number in the model's unit into meters.

in = 0.0254; ft = 0.3048; yd = 0.9144; mi = 1609.344; mil = 0.0254 ÷ 1000

A table lookup. The mil is computed as 0.0254 ÷ 1000 in 64-bit. A unit not in the table takes its factor from the shared unit table, or 1 when that has none.

  • m3d/units.js
  • lines 101 to 102
  • CadCraft 0.8.2
/** Meters per bare number in a unit (feet-and-inches: inches). */
export function unitScale(unit) { return { 'ft-in': IN, in: IN, ft: FT, yd: YD, mi: MI, mil: MIL, m: 1, cm: 0.01, mm: 0.001, um: 1e-6, km: 1000 }[unit] ?? (factor(unit) || 1); }

Arithmetic over typed lengths

64-bit

A typed expression such as 12'6"+3/4" or 2*450mm as one length in meters.

Each operand that carries a unit is converted to meters before the arithmetic; a bare number is passed on as a plain number and the unit factor is given separately. The arithmetic itself is done by lencalc.js.

  • m3d/units.js
  • lines 35 to 51
  • CadCraft 0.8.2
/**
 * Arithmetic over typed lengths (0.7 MC-CALC; the rules of lencalc.js, never eval): a bare number is in the model's unit
 * when it is used as a length (added to one, or the whole answer: 2.5/3 in a meter model is 0.833 m) and a plain factor
 * when it multiplies or divides one (2*450mm). → {ok: true, value (meters), plain} | {ok: false, message (says where)} |
 * null when the text holds no arithmetic.
 */
export function calcLength(text, unit = 'm') {
  if (typeof text !== 'string') return null;
  const s = text.trim().replace(/[‘’′]/g, "'").replace(/[“”″]/g, '"').toLowerCase();
  if (!hasArithmetic(s)) return null;
  const r = calc(s, (x) => {
    const bare = /^[\d.\s/-]+$/.test(x.trim()), v = parseOneLength(x, bare ? 'm' : unit);
    return v === null ? null : { value: v, unit: !bare };
  }, { plain: unitScale(unit) });
  if (!r.ok) return { ok: false, message: t('calc.err.' + r.code, { pos: r.pos + 1, text: r.text }) };
  return { ok: true, value: r.value, plain: r.unit === 0 };
}

The value box

64-bit

What was typed as a length, a list of lengths, a point, an offset, a count or a plain number for angles and factors.

plain number = value in meters ÷ meters per unit (for arithmetic and bare fractions)

A bare decimal number is read directly as the plain number. For typed arithmetic or a bare fraction the plain number is the meter value divided by the unit factor, a multiply followed by a divide that can differ from the typed value in the last digit.

  • m3d/units.js
  • lines 175 to 214
  • CadCraft 0.8.2
/**
 * The value box. Returns one of
 *   {kind: 'len', v}           a length (m)                    3m · 10'6"
 *   {kind: 'list', vs: [m…]}   lengths separated by commas      4m,3m (rectangle width, height)
 *   {kind: 'abs', p}           an absolute point                [1,2,0]
 *   {kind: 'rel', p}           an offset                        <1,0,0>
 *   {kind: 'sides', n}         segments/sides                   24s
 *   {kind: 'array', n}         copies in a row                  3x  *3
 *   {kind: 'divide', n}        copies between                   /3
 *   {kind: 'num', v}           a plain number (angles, factors) — also offered as `num` on 'len'
 * or null when it cannot be read.
 */
export function parseValue(text, unit = 'm') {
  const s = (text || '').trim();
  if (!s) return null;
  let m;
  if ((m = s.match(/^([[<])\s*(.+?)\s*([\]>])$/))) {
    const parts = m[2].split(/[,;]/).map((x) => parseLength(x, unit));
    if (parts.length !== 3 || parts.some((x) => x === null)) return null;
    return { kind: m[1] === '[' ? 'abs' : 'rel', p: parts };
  }
  if ((m = s.match(/^(\d{1,4})\s*s$/i))) return { kind: 'sides', n: +m[1] };
  if ((m = s.match(/^(?:(\d{1,4})\s*x|[*x]\s*(\d{1,4}))$/i))) return { kind: 'array', n: +(m[1] || m[2]) };
  if ((m = s.match(/^\/\s*(\d{1,4})$/))) return { kind: 'divide', n: +m[1] };
  if (/[,;]/.test(s)) {
    const vs = s.split(/[,;]/).map((x) => (x.trim() ? parseLength(x, unit) : null));
    if (vs.some((x) => x === null)) return null;
    return { kind: 'list', vs };
  }
  // arithmetic (0.7 MC-CALC): a length, or for an angle or a factor the plain number it comes to (90/2 = 45)
  const c = calcLength(s, unit);
  if (c && !c.ok) return { kind: 'error', message: c.message };
  if (c) return { kind: 'len', v: c.value, num: c.plain ? c.value / unitScale(unit) : null };
  const v = parseLength(s, unit);
  // a bare number (or a bare fraction: 90/2) is also offered as a plain number, for angles and factors
  const num = /^[-+]?(\d+(\.\d*)?|\.\d+)$/.test(s) ? +s : v !== null && /^[-+]?[\d.\s/-]+$/.test(s) ? v / unitScale(unit) : null;
  if (v !== null) return { kind: 'len', v, num };
  if (num !== null) return { kind: 'num', v: num };
  return null;
}

Length readout

64-bit

A length in meters as text in the model's unit.

ft-in: total = round(m ÷ 0.0254 × den) sixteenths (or the chosen fraction); others: toFixed with the unit's decimals

The division by the unit factor and the rounding happen only for the text. Feet and inches are split from one whole number of fractions of an inch, so a value that rounds up to 12 inches is written as the next foot. Default decimals: 4 for ft, 3 for yd and m, 2 for cm, 1 for mm.

  • m3d/units.js
  • lines 127 to 147
  • CadCraft 0.8.2
export function formatLength(m, unit = 'm') {
  if (!Number.isFinite(m)) return '';
  const neg = m < -1e-9, a = Math.abs(m);
  let s;
  if (unit === 'ft-in') {
    const den = fracDen();
    const total = Math.round((a / IN) * den);
    const ft = Math.floor(total / (12 * den)), rest = (total - ft * 12 * den) / den;
    s = ft ? `${ft}' ${inchText(rest)}"` : `${inchText(rest)}"`;
  } else if (unit === 'in') s = inchText(a / IN) + '"';
  else if (unit === 'ft') s = trim(a / FT, places(4)) + ' ft';
  else if (unit === 'yd') s = trim(a / YD, places(3)) + ' yd';
  else if (unit === 'mi') s = a >= MI ? trim(a / MI, places(4)) + ' mi' : trim(a / FT, places(3)) + ' ft';   // a mile model shows the small lengths in feet
  else if (unit === 'mil') s = trim(a / MIL, places(2)) + ' mil';
  else if (unit === 'um') s = trim(a * 1e6, places(2)) + ' µm';
  else if (unit === 'mm') s = trim(a * 1000, places(1)) + ' mm';
  else if (unit === 'cm') s = trim(a * 100, places(2)) + ' cm';
  else if (unit === 'km') s = a >= 1000 ? trim(a / 1000, places(3)) + ' km' : trim(a, places(3)) + ' m';   // a kilometer model shows the small lengths in meters
  else s = trim(a, places(3)) + ' m';
  return (neg ? '-' : '') + s;
}

Inches as a fraction

64-bit

Inches written as a whole number and a reduced fraction.

sx = round(inches × den); whole = ⌊sx ÷ den⌋; fraction = (sx mod den) ÷ den, reduced by the greatest common divisor

One rounding to a whole number of fractions, then integer arithmetic. The denominator is 16 unless the file chose another power of two up to 64.

  • m3d/units.js
  • lines 117 to 125
  • CadCraft 0.8.2
/** Inches to "6 1/2" (nearest 1/16, or the chosen fraction). */
function inchText(inches) {
  const den = fracDen();
  const sx = Math.round(inches * den);
  const whole = Math.floor(sx / den), frac = sx % den;
  if (!frac) return String(whole);
  const g = gcd(frac, den);
  return (whole ? whole + ' ' : '') + frac / g + '/' + den / g;
}

Area readout

64-bit

An area in square meters as text in the model's unit.

ft²: a ÷ (0.3048 × 0.3048); in²: a ÷ (0.0254 × 0.0254); mm model: m² from 0.01 m², else mm²

The squared factor is formed by one multiplication and the area is divided by it. The text is rounded to 0 to 4 decimals depending on the unit.

  • m3d/units.js
  • lines 148 to 161
  • CadCraft 0.8.2
// millimeter models show areas from 0.01 m² and volumes from 0.01 m³ in square/cubic meters
// (a 4 × 3 m room reads "12 m²", not "12000000 mm²"); smaller ones stay in mm² / mm³
export function formatArea(a, unit = 'm') {
  if (unit === 'ft-in' || unit === 'ft') return t('m3d.units.sqft', { v: trim(a / (FT * FT), 2) });
  if (unit === 'yd') return t('m3d.units.sqyd', { v: trim(a / (YD * YD), 2) });
  if (unit === 'mi') return Math.abs(a) >= MI * MI * 0.01 ? t('m3d.units.sqmi', { v: trim(a / (MI * MI), 3) }) : t('m3d.units.sqft', { v: trim(a / (FT * FT), 2) });
  if (unit === 'um') return trim(a * 1e12, 1) + ' µm²';
  if (unit === 'mil') return t('m3d.units.sqin', { v: trim(a / (IN * IN), 4) });
  if (unit === 'in') return t('m3d.units.sqin', { v: trim(a / (IN * IN), 2) });
  if (unit === 'mm') return Math.abs(a) >= 0.01 ? trim(a, 3) + ' m²' : trim(a * 1e6, 0) + ' mm²';
  if (unit === 'cm') return trim(a * 1e4, 1) + ' cm²';
  if (unit === 'km') return Math.abs(a) >= 1e6 ? trim(a / 1e6, 3) + ' km²' : trim(a, 3) + ' m²';
  return trim(a, 3) + ' m²';
}

Volume readout

64-bit

A volume in cubic meters as text in the model's unit.

ft³: v ÷ 0.3048³; in³: v ÷ 0.0254³; mm model: m³ from 0.01 m³, else mm³

The cubed factor comes from the ** operator and the volume is divided by it. The text is rounded to 0 to 4 decimals depending on the unit.

  • m3d/units.js
  • lines 162 to 173
  • CadCraft 0.8.2
export function formatVolume(v, unit = 'm') {
  if (unit === 'ft-in' || unit === 'ft') return t('m3d.units.cuft', { v: trim(v / (FT ** 3), 2) });
  if (unit === 'yd') return t('m3d.units.cuyd', { v: trim(v / (YD ** 3), 2) });
  if (unit === 'mi') return Math.abs(v) >= MI ** 3 * 0.01 ? t('m3d.units.cumi', { v: trim(v / (MI ** 3), 3) }) : t('m3d.units.cuft', { v: trim(v / (FT ** 3), 2) });
  if (unit === 'um') return trim(v * 1e18, 1) + ' µm³';
  if (unit === 'mil') return t('m3d.units.cuin', { v: trim(v / (IN ** 3), 4) });
  if (unit === 'in') return t('m3d.units.cuin', { v: trim(v / (IN ** 3), 1) });
  if (unit === 'mm') return Math.abs(v) >= 0.01 ? trim(v, 3) + ' m³' : trim(v * 1e9, 0) + ' mm³';
  if (unit === 'cm') return trim(v * 1e6, 1) + ' cm³';
  if (unit === 'km') return Math.abs(v) >= 1e9 ? trim(v / 1e9, 3) + ' km³' : trim(v, 3) + ' m³';
  return trim(v, 3) + ' m³';
}

In detail

Method
Regular expressions pick the feet, inches, fraction and suffix out of the text; the parts are converted with unary plus and combined in one expression. Arithmetic in the typed text is handed to lencalc.js. Other unit marks go to the shared unit table.
Number types
64-bit numbers. The typed digits are converted by the language's own correctly rounded string-to-number conversion.
Tolerances
A length below −1e-9 m is shown with a minus sign. Readouts round to 1/16 inch (or the chosen 1/1 to 1/64) for feet and inches, and to a fixed number of decimals per unit otherwise.
Limits
Decimal factors such as 0.0254 and 0.001 are not exact in binary, so a typed value is within one rounding of the exact product. A plain number taken from typed arithmetic is converted to meters and divided back by the unit factor.

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