Resources Numerical accuracy Calculation policy

Takeoff quantities from marks

The length, area, perimeter and count each mark measures at its sheet's scale, and the totals per item and per sheet that a takeoff reports.

How the precision is kept

Quantities are never stored: every call measures each mark again from its points and the sheet's feet-per-unit scale, in 64-bit numbers. Lengths are scaled segment by segment and areas by the scale twice, then by a slope factor from the roof pitch. Totals are plain running sums in the order the marks are stored; a mark on a sheet with no scale is counted as unscaled and left out of them. The project file writer (toJSON) writes mark points as they are, with no rounding.

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.

model.js TakeoffCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-takeoffcraft.mjs

Length of a linear mark

64-bit

The counted, deducted and gross length of a run in feet, along the slope where a segment is a rake or a hip.

segment ft = segment length × ftPerUnit; sloped = segment ft × slope factor; length = Σ counted sloped segments; gross = length + deducted

Each segment is scaled to feet and multiplied by its own slope factor before the terms are added in drawing order. Deducted segments are summed on their own, and the gross length is the counted sum plus the deducted sum. The plan length of the counted segments is summed beside the sloped length.

  • model.js
  • lines 897 to 903
  • TakeoffCraft 0.8.1
const segs = segmentLengths(m.pts).map((d) => d * ftPerUnit);
const kinds = segs.map((_, i) => segSlope(m, it, i));
const sloped = segs.map((d, i) => d * (kinds[i].k === 'rake' ? slopeFactor(kinds[i].p) : kinds[i].k === 'hip' ? hipFactor(kinds[i].p) : 1));
const ded = new Set(m.deducts || []);
let len = 0, cut = 0, plan = 0;
sloped.forEach((d, i) => { if (ded.has(i)) cut += d; else { len += d; plan += segs[i]; } });
return { lengthFt: len, grossFt: len + cut, deductFt: cut, planFt: plan, segmentsFt: segs, slopedFt: sloped, slopes: kinds, deducts: [...ded] };

Area and perimeter of an area mark

64-bit

The net area of an outline minus its cut-outs in square feet, on the slope when the mark or its item has a pitch, and its perimeter in feet.

area = net × ftPerUnit × ftPerUnit × √(1 + (pitch ÷ 12)²); perimeter = perimeter × ftPerUnit

The scale is squared first, the net area (outline minus cut-outs, subtracted in sheet units by areaWithHoles in geom.js) is multiplied by it, and the slope factor is applied last. The plan area without the slope factor is returned beside it. The perimeter is scaled once and is not sloped.

  • model.js
  • lines 905 to 907
  • TakeoffCraft 0.8.1
const a = areaWithHoles(m.pts, m.holes);
const k2 = ftPerUnit * ftPerUnit, f = slopeFactor(pitch);
return { areaSqft: a.net * k2 * f, planSqft: a.net * k2, factor: f, pitch, grossSqft: a.gross * k2 * f, cutSqft: a.cut * k2 * f, perimeterFt: a.perimeter * ftPerUnit, outerPerimeterFt: a.outerPerimeter * ftPerUnit };

Totals per item and per sheet

64-bit

Each item's total length, area, perimeter and count, and the same per sheet, with back-outs taken out and typical groups counted as many times as they repeat.

item total = Σ marks (measure × repeat); area mark = max(0, area − back-out × slope factor)

Running 64-bit sums in the order the marks are stored, each mark multiplied by its typical group's whole-number repeat. An area mark's back-out is multiplied by the mark's slope factor and subtracted, the result is clamped at zero, and what was taken out is summed separately. A mark whose sheet has no scale adds one to an unscaled count and nothing to the totals.

  • model.js
  • lines 917 to 961
  • TakeoffCraft 0.8.1
/**
 * Totals per item (feet / square feet) and per sheet. Marks on sheets with no scale are
 * counted in `unscaled` and left out of the totals — never measured as zero silently.
 */
export function quantities(state, { area, exclude } = {}) {
  const out = new Map();
  for (const it of state.items) {
    const q = { item: it, lengthFt: 0, areaSqft: 0, perimeterFt: 0, count: 0, marks: 0, unscaled: 0, backoutSqft: 0, bySheet: new Map() };
    if (it.type === 'fixed') {                             // a typed quantity (TK.5+): in its bid area like a mark would be
      const a = it.area || null;
      q.fixed = (area !== undefined && a !== area) || (exclude && a && exclude.has(a)) ? 0 : it.qty || 0;
    }
    out.set(it.id, q);
  }
  const bo = backouts(state);
  const rep = new Map((state.groups || []).map((g) => [g.id, g.repeat]));
  for (const m of state.marks) {
    if (area !== undefined && (m.area || null) !== area) continue;      // one bid area (null: the marks in none)
    if (exclude && m.area && exclude.has(m.area)) continue;              // add alternates, out of the base bid (T17)
    const q = out.get(m.item);
    if (!q) continue;
    q.marks++;
    const sc = state.sheets[m.sheet]?.scale;
    const r = measureMark(m, sc?.ftPerUnit, q.item);
    if (!r) { q.unscaled++; continue; }
    let s = q.bySheet.get(m.sheet);
    if (!s) q.bySheet.set(m.sheet, (s = { lengthFt: 0, areaSqft: 0, perimeterFt: 0, count: 0, marks: 0 }));
    s.marks++;
    const k = rep.get(m.group) || 1;                       // a typical group's mark (T16) counts `repeat` times
    if (m.kind !== q.item.type) {                          // a second condition (T8): its own totals, measured plainly
      const zero = () => ({ lengthFt: 0, areaSqft: 0, perimeterFt: 0, count: 0, marks: 0 });
      const qc = ((q.conds ??= {})[m.kind] ??= zero()), sc2 = ((s.conds ??= {})[m.kind] ??= zero());
      for (const o of [qc, sc2]) { o.marks++; o.count += k; o.lengthFt += (r.lengthFt || 0) * k; o.areaSqft += (r.areaSqft || 0) * k; o.perimeterFt += (r.perimeterFt || 0) * k; }
      continue;
    }
    if (m.kind === 'count') { q.count += k; s.count += k; }
    else if (m.kind === 'linear') { q.lengthFt += r.lengthFt * k; s.lengthFt += r.lengthFt * k; q.count += k; s.count += k; }
    else {
      const cut = bo.cut(m, sc.ftPerUnit) * r.factor;     // back-outs (T14) that cover part of it, on the slope (T15)
      const a = Math.max(0, r.areaSqft - cut);
      q.areaSqft += a * k; s.areaSqft += a * k; q.backoutSqft += (r.areaSqft - a) * k; q.perimeterFt += r.perimeterFt * k; s.perimeterFt += r.perimeterFt * k; q.count += k; s.count += k;
    }
  }
  return out;
}

Back-out area taken from an area mark

64-bit

How many square feet the back-out marks on the same sheet cover of one area mark.

cut = (Σ overlap areas in sheet units) × ftPerUnit × ftPerUnit

The overlaps are added in sheet units and scaled to square feet once, after the sum. A back-out whose bounding box only touches the mark's box is skipped by a plain comparison with no tolerance. Two back-outs that overlap each other are both added.

  • model.js
  • lines 984 to 995
  • TakeoffCraft 0.8.1
cut(m, ftPerUnit) {
  const list = bySheet.get(m.sheet);
  if (!list || m.kind !== 'area' || !(ftPerUnit > 0)) return 0;
  const ob = bounds(m.pts);
  let a = 0;
  for (const b of list) {
    if (!b.of.has(m.item) || b.m.id === m.id) continue;
    if (b.box.minX >= ob.maxX || b.box.maxX <= ob.minX || b.box.minY >= ob.maxY || b.box.maxY <= ob.minY) continue;
    a += overlapArea(m.pts, m.holes || [], b.m.pts);
  }
  return a * ftPerUnit * ftPerUnit;
},

Totals in the item's unit

64-bit

An item's length, area, perimeter, height and count in the unit the item reports in, as the values assembly formulas use.

L = feet × 0.3048 for a metric item; A = square feet × the unit's factor; wall area = length × height

One multiplication by the unit's factor from units.js. The factors 0.3048 and 0.09290304 are the defined values; 1 ÷ 9, 1 ÷ 100 and 1 ÷ 43560 are 64-bit quotients. A linear item's wall area is length times height in feet, converted after the product.

  • model.js
  • lines 999 to 1008
  • TakeoffCraft 0.8.1
/** Formula variables for an item's totals (or one sheet's), in the item's own units. */
export function itemVars(it, t) {
  const lu = lengthUnitOf(it.unit);
  if (it.type === 'linear') {
    return { L: ftTo(t.lengthFt, lu), H: ftTo(it.heightFt, lu), A: sqftTo(t.lengthFt * it.heightFt, areaUnitOf(it.unit)), N: t.count };
  }
  if (it.type === 'area') return { A: sqftTo(t.areaSqft, it.unit), P: ftTo(t.perimeterFt, lu), H: ftTo(it.heightFt, lu), N: t.count };
  if (it.type === 'fixed') return { Q: t.fixed ?? it.qty ?? 0 };
  return { N: t.count };
}

Quantity of a second condition

64-bit

The quantity of an item's second kind of mark in that condition's own unit.

The same single conversion as the item's own quantity: feet × 0.3048 for a metric length, square feet × the unit's factor for an area, the count as it is. No totals gives 0.

  • model.js
  • lines 1009 to 1013
  • TakeoffCraft 0.8.1
/** A second condition's quantity (T8) in its unit, from its totals ({ lengthFt, areaSqft, count } or none). */
export function condQty(c, t) {
  if (!t) return 0;
  return c.kind === 'linear' ? ftTo(t.lengthFt, lengthUnitOf(c.unit)) : c.kind === 'area' ? sqftTo(t.areaSqft, c.unit) : t.count;
}

In detail

Method
Segment lengths and outline areas come from geom.js (segmentLengths, areaWithHoles). The slope factor is √(1 + (rise ÷ 12)²) for rakes and sloped areas and √(1 + (rise ÷ 12)² ÷ 2) for hips, from units.js. Back-outs are taken out with overlapArea from geom.js.
Number types
64-bit numbers throughout. Mark counts and typical-group repeats are whole numbers.
Tolerances
None in this file. The convex-outline test for a back-out (isConvex in geom.js) ignores a turn smaller than 1e-12 × the square of the outline's largest |x| + |y|.
Limits
A back-out that crosses the edge of an area is clipped with its arcs flattened to 0.5° chords (overlapArea in geom.js). Back-outs that overlap each other are each deducted. A back-out whose outline is not convex, or has cut-outs of its own, deducts nothing and is listed as bad.

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