Resources Numerical accuracy Calculation policy

3D check view: walls, slabs and their quantities

Stands the takeoff up in 3D (walls from runs with a height, slabs from areas with a depth) and reports each object's length, wall area, net area and volume.

How the precision is kept

The numbers each object reports are not taken from its mesh. Length, area and cut-outs come from measureMark in model.js, in 64-bit, and wall area and volume are one multiplication each. The mesh is built separately, with arcs as 2.5° chords, to be looked at and exported.

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.

scene3d.js TakeoffCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-takeoffcraft.mjs

Sheet coordinates to feet on the ground

64-bit

Maps a point of the sheet to feet in the 3D view.

k = ftPerUnit ÷ √|det T|; (x, y) → (dx·k, (height − dy)·k)

s = √|det T| of the page transform and k = ftPerUnit ÷ s; a point is transformed, then × k, with y flipped as (height − y) × k. A determinant of 0 is replaced by a scale of 1.

  • scene3d.js
  • lines 40 to 55
  • TakeoffCraft 0.8.1
/** The mapping from PDF user space to the ground, in feet. */
export function sheetFrame(ftPerUnit, { transform = null, width = 0, height = 0 } = {}) {
  const T = transform || [1, 0, 0, -1, 0, height];
  const s = Math.sqrt(Math.abs(T[0] * T[3] - T[1] * T[2])) || 1;
  const k = ftPerUnit / s;
  return {
    k,
    w: width * k,
    h: height * k,
    /** {x, y} user space → [x, y] feet on the ground. */
    at: (p) => {
      const dx = T[0] * p.x + T[2] * p.y + T[4], dy = T[1] * p.x + T[3] * p.y + T[5];
      return [dx * k, (height - dy) * k];
    },
  };
}

Wall area of a run

64-bit

The area of the wall a run with a height stands for.

wall area = L × H

wallSqft = r.lengthFt × h, one 64-bit multiplication, where the length is the run's length from measureMark in model.js. Totals are added mark by mark.

  • scene3d.js
  • lines 218 to 220
  • TakeoffCraft 0.8.1
const wallSqft = r.lengthFt * h;
totals.wallSqft += wallSqft; t.wallSqft += wallSqft; totals.walls++;
o = { ...base, kind: 'wall', tris, lines, anchor, measure: { lengthFt: r.lengthFt, heightFt: h, wallSqft, thickFt: thick, baseFt: z0, headFt: head } };

Net area and volume of a slab

64-bit

An area's net square feet after back-outs, and its volume when it has a depth.

net = max(0, A − cut); V = net × D

net = max(0, area − back-out overlap × slope factor); volume = net × depth. An area with no depth is drawn THIN_FT thick (2 ÷ 12 ft) and reports no volume. Sums are added in mark order.

  • scene3d.js
  • lines 233 to 241
  • TakeoffCraft 0.8.1
const thin = !(it.heightFt > 0);
const d = thin ? THIN_FT : it.heightFt;
const zb = it.baseFt > 0 ? it.baseFt : 0;                    // a ceiling or a raised floor (T5)
const mesh = prismMesh(outer, holes, zb, zb + d);
if (!mesh.triangulated) untriangulated++;
const net = Math.max(0, r.areaSqft - bo.cut(m, ftPerUnit) * r.factor);
const volumeCuft = thin ? null : net * d;
totals.netSqft += net; t.netSqft += net; t.count++;
if (thin) totals.plates++; else { totals.slabs++; totals.volumeCuft += volumeCuft; t.volumeCuft += volumeCuft; }

Totals over stacked sheets

64-bit

Adds the totals of each sheet when several sheets are stacked as floors.

Each total is a 64-bit sum over the sheets in the order given; per-item totals are added the same way. Elevations are added to z values only.

  • scene3d.js
  • lines 291 to 292
  • TakeoffCraft 0.8.1
for (const k of Object.keys(totals)) totals[k] += r.totals[k];
for (const [id, v] of r.byItem) { const b = byItem.get(id); if (!b) byItem.set(id, { ...v }); else for (const k of Object.keys(v)) b[k] += v[k]; }

Outline of a wall with a thickness

64-bit

Offsets a run half the thickness to each side, with mitred corners, into one closed outline.

offset = min(half ÷ max(cos, 1e-6), 4 × half) along the bisector

Each corner moves along the bisector of the two left normals by half ÷ cos, where cos is floored at 1e-6 and the offset is capped at 4 × the half thickness. This outline is for the picture and the exported mesh; no reported quantity uses it.

  • scene3d.js
  • lines 89 to 110
  • TakeoffCraft 0.8.1
/**
 * A wall with a thickness (T5): the run offset half the thickness to each side (mitred joints, limited to 4× the half
 * width at sharp turns), closed into one outline and extruded from z0 to z1.
 */
export function wallOutline(run, thick) {
  const half = thick / 2, n = run.length;
  const dir = (a, b) => { const dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1; return [dx / l, dy / l]; };
  const left = [], right = [];
  for (let i = 0; i < n; i++) {
    const d0 = i > 0 ? dir(run[i - 1], run[i]) : dir(run[0], run[1]);
    const d1 = i < n - 1 ? dir(run[i], run[i + 1]) : d0;
    let mx = -(d0[1] + d1[1]), my = d0[0] + d1[0];               // the bisector of the two left normals
    const ml = Math.hypot(mx, my);
    if (ml < 1e-9) { mx = -d0[1]; my = d0[0]; } else { mx /= ml; my /= ml; }
    const cos = mx * -d0[1] + my * d0[0];                         // between the bisector and the first segment's normal
    const k = Math.min(half / Math.max(cos, 1e-6), half * 4);
    left.push([run[i][0] + mx * k, run[i][1] + my * k]);
    right.push([run[i][0] - mx * k, run[i][1] - my * k]);
  }
  const ring = [...right, ...left.reverse()];
  return ringArea(ring) < 0 ? ring.reverse() : ring;
}

Solid from an outline and its cut-outs

64-bit

Triangulates the top and bottom faces and builds the side faces of a slab or wall.

The faces are triangulated by earcut (m3d/earcut.js) over the chord points; each triangle is turned counter-clockwise by the sign of one cross product. The mesh is for display and export; the function also returns whether earcut produced any triangle.

  • scene3d.js
  • lines 116 to 137
  • TakeoffCraft 0.8.1
/** A prism: outline (CCW) minus holes (CW), from z0 to z1, closed and facing out. */
function prismMesh(outer, holes, z0, z1) {
  const tris = [], lines = [];
  const flat = [], holeIdx = [], all = [];
  for (const p of outer) { flat.push(p[0], p[1]); all.push(p); }
  for (const h of holes) { holeIdx.push(all.length); for (const p of h) { flat.push(p[0], p[1]); all.push(p); } }
  const idx = earcut(flat, holeIdx);
  for (let i = 0; i < idx.length; i += 3) {
    let a = all[idx[i]], b = all[idx[i + 1]], c = all[idx[i + 2]];
    if ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]) < 0) [b, c] = [c, b];
    tris.push(a[0], a[1], z1, b[0], b[1], z1, c[0], c[1], z1);          // top, facing up
    tris.push(a[0], a[1], z0, c[0], c[1], z0, b[0], b[1], z0);          // bottom, facing down
  }
  for (const r of [outer, ...holes]) {
    for (let i = 0; i < r.length; i++) {
      const a = r[i], b = r[(i + 1) % r.length];
      quad(tris, a, b, z0, z1);                                           // outward: the ring runs with the solid on its left
      seg(lines, a, z0, b, z0); seg(lines, a, z1, b, z1);
    }
  }
  return { tris, lines, triangulated: idx.length > 0 };
}

In detail

Method
Wall area = run length × height. Slab volume = net area × depth, net area = area − back-outs, not below 0. Totals are sums over the marks of a sheet, and over the sheets of a stack.
Number types
64-bit numbers; meshes are plain arrays of 64-bit numbers until the view or an export converts them.
Tolerances
Mesh only: points closer than 1e-9 ft are merged; a mitre is limited to 4 × the half thickness; a bisector shorter than 1e-9 falls back to the segment's normal.
Limits
The sheet-to-ground mapping takes one scale from the page transform's determinant, so it assumes the transform is a uniform scale and a rotation. A wall with a thickness is drawn mitred; the reported wall area ignores thickness and corners.

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