Resources Numerical accuracy Calculation policy

Typed distances and coordinates while drawing

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.

typein.js TakeoffCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-takeoffcraft.mjs

Signed coordinate length

64-bit

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) };
}
takeoffcraft.js TakeoffCraft 0.8.1 64-bit 2 calculations Reviewed

Tests bin/test-takeoffcraft.mjs

Circle radius typed while drawing

64-bit

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.

  • takeoffcraft.js
  • lines 803 to 807
  • TakeoffCraft 0.8.1
const r = U.parseLength(text);
const c = d.pts[0], to = app.cursor && G.dist(app.cursor, c) > 0 ? app.cursor : { x: c.x + 1, y: c.y };
const n = G.dist(to, c);
res = !r.ok ? { ok: false, error: 'typein.err.read' } : !sc ? { ok: false, error: 'typein.err.noscale' }
  : { ok: true, point: { x: c.x + (to.x - c.x) / n * r.ft / sc.ftPerUnit, y: c.y + (to.y - c.y) / n * r.ft / sc.ftPerUnit } };

Typed spot elevation

64-bit

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.

Every policy that takes this approach