Turns what a person types (feet and inches, fractions, metric, a point or a polar step, a roof pitch) into millimeters and degrees, and formats areas, angles and pitches for display.
How the precision is kept
Typed text is parsed into 64-bit numbers and multiplied by exact factors: 25.4 mm per inch and 12 inches per foot. Feet, inches and a fraction are added as inches first and multiplied by 25.4 once. A pitch typed as a rise over a run goes through one arctangent. Display functions round only the text they return.
Open item. Take the sign of a feet-and-inches entry from the typed minus sign so that a negative value with 0 feet stays negative.
Calculations under this policy
6 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.
Reads a typed length in feet and inches, inches with a fraction, or any unit mark, as millimeters.
mm = sign × (|feet| × 12 + inches + n ÷ d) × 25.4
Feet, inches and the fraction are summed in inches and multiplied by 25.4 once. The sign is Math.sign of the feet number with 0 replaced by 1, so an entry of minus zero feet and some inches comes out positive. A fraction with a zero denominator is not guarded in the feet-and-inches and whole-plus-fraction forms and gives Infinity.
model-units.js
lines 61 to 77
BimCraft 0.8.1
/** Text → millimetres (NaN when it is not a length). Bare numbers: inches ('ftin') or millimetres ('mm'). */
export function parseLen(text, units = 'ftin') {
const s = String(text ?? '').trim();
if (!s) return NaN;
// 12' 6" without the dash, 12'6, 6 1/2 (inches), 1/2"
let v = parseLength(s, FACTOR[units] || INCH);
if (Number.isFinite(v)) return v;
let m = s.match(/^(-?\d+(?:\.\d+)?)\s*'\s*-?\s*(\d+(?:\.\d+)?)?(?:\s+(\d+)\/(\d+))?\s*"?$/);
if (m) return Math.sign(Number(m[1]) || 1) * (Math.abs(Number(m[1])) * 12 + Number(m[2] || 0) + (m[3] ? Number(m[3]) / Number(m[4]) : 0)) * INCH;
m = s.match(/^(\d+)\s+(\d+)\/(\d+)$/);
if (m && !isMetric(units)) return (Number(m[1]) + Number(m[2]) / Number(m[3])) * INCH;
m = s.match(/^(-?)\s*(\d+)\/(\d+)$/);
if (m && !isMetric(units) && Number(m[3])) return (m[1] ? -1 : 1) * Number(m[2]) / Number(m[3]) * INCH;
// any other length mark in the shared table (0.7 UNITS): 2.5 dm, 3 yd, 1 ch, 10 US ft, 400 nm
const r = parseTo(s, 'length', 'mm');
return r.ok ? r.value : NaN;
}
Typed point, step or polar entry
Exact
Reads a relative step, an absolute point, a length at an angle, or a bare length from the keyboard.
Each number is parsed with parseLen; the angle of a polar entry is read with Number in degrees and is not converted here. A length must be finite and above 0.
model-units.js
lines 79 to 105
BimCraft 0.8.1
/**
* What the wall tool (and anything placed by points) takes from the keyboard after the first point:
* 12'6" a length along the direction the pointer shows → { kind: 'len', len }
* 12'6"<30 a length at an angle (degrees, counter-clockwise from +X) → { kind: 'polar', len, angle }
* @1200,-300 a step from the last point → { kind: 'rel', dx, dy }
* 1200,3400 a point → { kind: 'abs', x, y }
* → null when it is none of these.
*/
export function parseTyped(text, units = 'ftin') {
const s = String(text ?? '').trim();
if (!s) return null;
const pair = (t) => {
const parts = splitPair(t);
if (!parts) return null;
const a = parseLen(parts[0], units), b = parseLen(parts[1], units);
return Number.isFinite(a) && Number.isFinite(b) ? [a, b] : null;
};
if (s.startsWith('@')) { const p = pair(s.slice(1)); return p ? { kind: 'rel', dx: p[0], dy: p[1] } : null; }
const lt = s.indexOf('<');
if (lt > 0) {
const len = parseLen(s.slice(0, lt), units), angle = Number(s.slice(lt + 1).trim().replace(/°$/, ''));
return Number.isFinite(len) && len > 0 && Number.isFinite(angle) ? { kind: 'polar', len, angle } : null;
}
if (/[,;]/.test(s)) { const p = pair(s); return p ? { kind: 'abs', x: p[0], y: p[1] } : null; }
const len = parseLen(s, units);
return Number.isFinite(len) && len > 0 ? { kind: 'len', len } : null;
}
Typed roof pitch to degrees
64-bit
Reads a pitch typed as a rise over a run, or in degrees.
degrees = atan(rise ÷ run) × 180 ÷ π
One division, one Math.atan and one conversion to degrees. A run of 0 returns NaN. A plain number is taken as degrees unchanged.
model-units.js
lines 123 to 130
BimCraft 0.8.1
/** A roof pitch typed as a rise over 12 ("6/12", "6:12", "6 in 12") or in degrees ("30", "30°") → degrees, or NaN. */
export function parsePitch(text) {
const s = String(text ?? '').trim().replace(/°$/, '').trim();
const m = s.match(/^(\d+(?:\.\d+)?)\s*(?:\/|:|in)\s*(\d+(?:\.\d+)?)$/i);
if (m) { const run = Number(m[2]); return run > 0 ? Math.atan(Number(m[1]) / run) * 180 / Math.PI : NaN; }
const d = Number(s);
return Number.isFinite(d) && s !== '' ? d : NaN;
}
Area as text
64-bit
Formats square millimeters as m² or ft² for labels.
The factors 1e6 mm² per m² and 304.8² mm² per ft² are exact. The text is rounded to 0.01 m² or 0.1 ft²; only the returned text is rounded.
model-units.js
lines 117 to 121
BimCraft 0.8.1
/** Square millimetres → m² (to 0.01) or ft² (to 0.1), as text with the unit. */
export function fmtArea(mm2, units = 'ftin') {
if (isMetric(units)) return (Math.round(mm2 / 1e4) / 100).toFixed(2) + ' m²'; // i18n-ignore: unit symbol
return (Math.round(mm2 / (304.8 * 304.8) * 10) / 10).toFixed(1) + ' ft²'; // i18n-ignore: unit symbol
}
Pitch as text
64-bit
Formats a pitch in degrees as a rise in 12, or as degrees.
rise = round(tan(θ) × 12 × 4) ÷ 4
The rise is rounded to a quarter in 12 and degrees to 0.1°; only the returned text is rounded.
model-units.js
lines 131 to 135
BimCraft 0.8.1
/** A pitch as text: a rise over 12 to 1/4 in feet and inches, else degrees to 0.1°. */
export function fmtPitch(deg, units = 'ftin') {
if (!isMetric(units)) { const r = Math.round(Math.tan(deg * Math.PI / 180) * 12 * 4) / 4; return r + '/12'; }
return (Math.round(deg * 10) / 10) + '°';
}
Angle as text
64-bit
Formats an angle in degrees, wrapped to 0 to 360.
a = round(((deg mod 360) + 360) mod 360 × 10) ÷ 10
The angle is wrapped with two remainders and rounded to 0.1°; a result of 360 is shown as 0. Only the returned text is rounded.
model-units.js
lines 115 to 116
BimCraft 0.8.1
/** An angle in degrees as text, to 0.1°. */
export const fmtAngle = (deg) => { let a = Math.round(((deg % 360) + 360) % 360 * 10) / 10; if (a === 360) a = 0; return a + '°'; };
In detail
Method
Regular expressions pick out feet, inches and fractions; other unit marks go through the shared unit table (parseTo). A pitch typed as rise over run becomes atan(rise ÷ run) in degrees.
Number types
64-bit numbers. The inch factor 25.4 and the square foot factor 304.8 × 304.8 are exact.
Limits
A negative feet-and-inches value whose feet part is 0 (typed as -0 feet and some inches) is returned positive, because the sign is taken from the feet number. Area text is rounded to 0.01 m² or 0.1 ft², angle text to 0.1° and pitch text to a quarter in 12.
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.