Places the next point of a run or outline from a typed distance, a distance and angle, or a pair of coordinates, and reads a typed spot elevation.
How the precision is kept
A typed length is read into feet and turned into sheet units by the sheet's scale. Directions are the screen's, taken through the inverse of the view's 2 × 2 matrix and normalized, so a rotated sheet reads the way it looks. All 64-bit; the point is stored as computed.
Calculations under this policy
5 calculations in 2 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.
Reads one typed coordinate, which may be negative or zero, into feet.
The sign is taken off, the rest is read by parseLength (which refuses zero and negatives) and the sign is put back. Text that is only zeros is returned as 0 without parsing.
typein.js
lines 21 to 29
TakeoffCraft 0.8.1
/** A length that may be signed or zero (coordinates). → feet | null */
export function signedLength(text) {
let s = String(text ?? '').trim().replace(/[−–—]/g, '-');
let sign = 1;
if (s.startsWith('-')) { sign = -1; s = s.slice(1).trim(); } else if (s.startsWith('+')) s = s.slice(1).trim();
if (/^0*(\.0*)?\s*('|"|ft|in|m|cm|mm)?$/i.test(s) && /0|\./.test(s)) return 0;
const r = parseLength(s);
return r.ok ? sign * r.ft : null;
}
Sheet direction of a screen direction
64-bit
The unit vector on the sheet that points along a given direction on the screen.
u = M⁻¹·(sx, −sy); u ÷ |u|
The 2 × 2 view matrix is inverted by its adjugate ÷ determinant; only an exactly zero determinant is refused. The result is divided by its Math.hypot length.
typein.js
lines 31 to 40
TakeoffCraft 0.8.1
/** The user-space unit vector that points along a screen direction (sx right, sy UP), or null. */
export function screenDir(linear, sx, sy) {
const [a, b, c, d] = linear;
const det = a * d - b * c;
if (!det) return null;
const px = sx, py = -sy; // screen pixels have y down
const ux = (d * px - c * py) / det, uy = (-b * px + a * py) / det;
const n = Math.hypot(ux, uy);
return n ? { x: ux / n, y: uy / n } : null;
}
Point from typed text
64-bit
Works out the next point from a distance, a distance and angle, or coordinates.
p = last + dir × ft × k, k = 1 ÷ ftPerUnit
k = 1 ÷ ftPerUnit is formed once and each length is multiplied by it. The polar angle is deg·π ÷ 180 through Math.cos and Math.sin. Coordinates combine the screen-right and screen-up unit vectors: from + (ex·dx + ey·dy)·k. Nothing is rounded.
typein.js
lines 42 to 80
TakeoffCraft 0.8.1
export function parseEntry(text, ctx) {
const fail = (error) => ({ ok: false, error });
const s = String(text ?? '').trim();
if (!s) return fail('typein.err.read');
if (!(ctx.ftPerUnit > 0)) return fail('typein.err.noscale');
const k = 1 / ctx.ftPerUnit; // PDF units per foot
const along = (from, dir, ft) => ({ x: from.x + dir.x * ft * k, y: from.y + dir.y * ft * k });
if (s.startsWith('@') || s.includes(',')) {
const rel = s.startsWith('@');
const body = s.replace(/^[@#]/, '');
const parts = body.split(',');
if (parts.length !== 2) return fail('typein.err.read');
const dx = signedLength(parts[0]), dy = signedLength(parts[1]);
if (dx === null || dy === null) return fail('typein.err.read');
const from = rel ? ctx.last : ctx.first;
if (!from) return fail('typein.err.first');
if (!dx && !dy) return fail('typein.err.zero');
const ex = screenDir(ctx.linear, 1, 0), ey = screenDir(ctx.linear, 0, 1);
if (!ex || !ey) return fail('typein.err.read');
return { ok: true, kind: rel ? 'rel' : 'abs', point: { x: from.x + (ex.x * dx + ey.x * dy) * k, y: from.y + (ex.y * dx + ey.y * dy) * k } };
}
if (!ctx.last) return fail('typein.err.first');
const lt = s.indexOf('<');
if (lt >= 0) {
const r = parseLength(s.slice(0, lt));
const deg = Number(s.slice(lt + 1).trim().replace(/°$/, ''));
if (!r.ok || !s.slice(lt + 1).trim() || !Number.isFinite(deg)) return fail('typein.err.read');
const dir = screenDir(ctx.linear, Math.cos(deg * Math.PI / 180), Math.sin(deg * Math.PI / 180));
if (!dir) return fail('typein.err.read');
return { ok: true, kind: 'polar', point: along(ctx.last, dir, r.ft) };
}
const r = parseLength(s);
if (!r.ok) return fail('typein.err.read');
const c = ctx.cursor;
const dx = c ? c.x - ctx.last.x : 0, dy = c ? c.y - ctx.last.y : 0, n = Math.hypot(dx, dy);
if (!(n > 0)) return fail('typein.err.dir');
return { ok: true, kind: 'dist', point: along(ctx.last, { x: dx / n, y: dy / n }, r.ft) };
}
Places a circle's edge point at a typed radius from its center, toward the pointer.
edge = c + (to − c) ÷ |to − c| × ft ÷ ftPerUnit
The unit vector from the center to the pointer is (to − c) ÷ n; the edge point is c + that × ft ÷ ftPerUnit, evaluated left to right. With no pointer direction, the direction is taken to the right of the center.
Reads the elevation typed on a count mark, in decimal feet, feet-inches or meters.
A plain decimal number is stored through Number as typed; anything else goes through parseLength with the sign handled apart. The value must be below 1e5 in size. The stored elevation is not rounded; the field shows it rounded to 4 decimals.
takeoffcraft.js
lines 2152 to 2161
TakeoffCraft 0.8.1
applyZ = (raw, z) => {
const v = raw.trim();
if (!v) { model.updateMark(m.id, { z: undefined }, t('undo.mark.z')); return; }
const num = /^-?\d+(?:\.\d+)?$/.test(v) ? Number(v) : null;
const r = num === null ? U.parseLength(v.replace(/^-/, ''), { defaultUnit: 'ft' }) : null;
const ft = num !== null ? num : r.ok ? (v.startsWith('-') ? -r.ft : r.ft) : null;
if (ft === null || !(Math.abs(ft) < 1e5)) { z.classList.add('is-invalid'); status(t('spots.err')); return; }
model.updateMark(m.id, { z: ft }, t('undo.mark.z'));
};
const zv = typeof m.z === 'number' ? String(U.roundTo(m.z, 4)) : '';
In detail
Method
Direct distance: last point + unit vector to the pointer × length. Polar: the same with the unit vector of the typed angle. Relative and absolute coordinates: from the last or the first point along the screen's right and up directions.
Number types
64-bit numbers.
Tolerances
The view matrix is refused only when its determinant is exactly 0; a zero-length direction is refused.
Limits
The length is multiplied by k = 1 ÷ ftPerUnit, a rounded reciprocal, where a division would round once. cos(90°) evaluates to 6.1e-17, so a length typed at exactly 90° carries a sideways part of that size before the direction is normalized.
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.