Resources Numerical accuracy Calculation policy

Marks and sheets written to PDF

Draws the takeoff's marks into a copy of the PDF, with a legend of quantities, and writes a DWG or DXF sheet as a vector PDF page.

How the precision is kept

Marks are already in PDF user space, so straight segments are written with their own coordinates. An arc is drawn as cubic Bézier curves of at most 90° each, which is a close approximation of a circle and not a circle. The quantities in the legend come from the takeoff's own measurement, not from the drawn curves, and are printed rounded.

Calculations under this policy

4 calculations in 3 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.

geom.js TakeoffCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-takeoffcraft.mjs, bin/test-takeoffcraft-cad.mjs

Arcs as Bézier curves for drawing

64-bit

Turns a run or an outline into move, line and curve commands.

k = 4 ÷ 3 × tan(φ ÷ 4); c₁ = p₀ + k·r·tangent₀; c₂ = p₃ − k·r·tangent₁

Each arc is split into pieces = max(1, ceil(|sweep| ÷ 90° − 1e-9)) equal parts with control points at k·r along the tangents, k = 4 ÷ 3 × tan(φ ÷ 4). The last piece ends exactly on the stored end point, not on a recomputed one. Used to draw only; lengths and areas never come from these curves.

  • geom.js
  • lines 84 to 110
  • TakeoffCraft 0.8.1
/**
 * A run or outline as drawing commands: ['M', p], ['L', p], ['C', c1, c2, p] (arcs as cubic
 * Béziers of at most 90° each — what PDF content streams and the canvas both draw). Affine
 * transforms keep Béziers exact, so callers may map the points to the screen afterwards.
 */
export function pathOps(pts, closed = false) {
  const ops = [];
  const n = pts.length;
  if (!n) return ops;
  ops.push(['M', { x: pts[0].x, y: pts[0].y }]);
  const segs = closed ? n : n - 1;
  for (let i = 0; i < segs; i++) {
    const p = pts[i], q = pts[(i + 1) % n];
    const a = arcOf(p, q, p.b);
    if (!a) { ops.push(['L', { x: q.x, y: q.y }]); continue; }
    const pieces = Math.max(1, Math.ceil(Math.abs(a.sweep) / (Math.PI / 2) - 1e-9));
    const phi = a.sweep / pieces, k = 4 / 3 * Math.tan(phi / 4);
    for (let j = 0; j < pieces; j++) {
      const t0 = a.a0 + phi * j, t1 = t0 + phi;
      const p0 = { x: a.cx + a.r * Math.cos(t0), y: a.cy + a.r * Math.sin(t0) };
      const p3 = j === pieces - 1 ? { x: q.x, y: q.y } : { x: a.cx + a.r * Math.cos(t1), y: a.cy + a.r * Math.sin(t1) };
      ops.push(['C', { x: p0.x - k * a.r * Math.sin(t0), y: p0.y + k * a.r * Math.cos(t0) }, { x: p3.x + k * a.r * Math.sin(t1), y: p3.y - k * a.r * Math.cos(t1) }, p3]);
    }
  }
  if (closed) ops.push(['Z']);
  return ops;
}
pdfmarks.js TakeoffCraft 0.8.1 64-bit 2 calculations Reviewed

Tests bin/test-takeoffcraft.mjs, bin/test-takeoffcraft-cad.mjs

A mark's path written to the page

64-bit

Writes a mark's commands as PDF path operators.

Coordinates are passed to pdf-lib's moveTo, lineTo and appendBezierCurve as they are, with no scaling or rounding in this file.

  • pdfmarks.js
  • lines 61 to 68
  • TakeoffCraft 0.8.1
const path = (pts, close) => {
  for (const op of pathOps(pts, close)) {
    if (op[0] === 'M') ops.push(moveTo(op[1].x, op[1].y));
    else if (op[0] === 'L') ops.push(lineTo(op[1].x, op[1].y));
    else if (op[0] === 'C') ops.push(appendBezierCurve(op[1].x, op[1].y, op[2].x, op[2].y, op[3].x, op[3].y));
    else ops.push(closePath());
  }
};

Quantities printed in the legend

64-bit

The quantity of each item on that sheet, as text in the page's legend.

The quantity comes from the takeoff's quantities for that sheet and is rounded by fixed to 2 decimals (0 for a count item) for the printed text. Nothing is read back from the PDF.

  • pdfmarks.js
  • lines 186 to 189
  • TakeoffCraft 0.8.1
const tq = q.get(it.id).bySheet.get(key);
const qty = tq ? [fixed(itemQty(it, tq), it.type === 'count' ? 0 : 2) + ' ' + UNITS[it.unit].label,         // T8: + each condition's
  ...(it.conds || []).filter((c) => tq.conds?.[c.kind]).map((c) => fixed(condQty(c, tq.conds[c.kind]), c.kind === 'count' ? 0 : 2) + ' ' + UNITS[c.unit].label)].join(' + ') : t('pdf.legend.noscale');
lines.push({ color: it.color, text: winAnsi(it.name.length > 60 ? it.name.slice(0, 59) + '…' : it.name) + '  ' + winAnsi(qty) });
cadpdf.js TakeoffCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-takeoffcraft-cad.mjs

A drawing's lines written as vector paths

64-bit

Writes each batch of the drawing's lines to the PDF page under the sheet's matrix.

width = max(0.25, lw × 72 ÷ 25.4 [× 0.5]) ÷ scale

The line and fill coordinates stay in the space's own units; the sheet or viewport matrix is written once with concatTransformationMatrix and the PDF viewer applies it. The line width is the drawing's lineweight in millimeters × 72 ÷ 25.4 (halved for model lines, at least 0.25 point) divided by the matrix scale.

  • cadpdf.js
  • lines 60 to 72
  • TakeoffCraft 0.8.1
const ops = [pushGraphicsState(), concatTransformationMatrix(M[0], M[1], M[2], M[3], M[4], M[5]), setLineJoin(1), setLineCap(1), setStrokingColor(rgb(r, g, bl)), setFillingColor(rgb(r, g, bl))];
let any = false;
for (const f of b.fills) {
  ops.push(pushGraphicsState());
  if (f.pattern) ops.push(setGraphicsState(patName));
  for (const ring of f.rings) { if (ring.length < 6) continue; ops.push(moveTo(ring[0], ring[1])); for (let i = 2; i + 1 < ring.length; i += 2) ops.push(lineTo(ring[i], ring[i + 1])); ops.push(closePath()); }
  ops.push(evenOdd(), popGraphicsState()); any = true;
}
if (b.lines.length) {
  const lw = Math.max(0.25, paper ? (b.lw ?? 0.25) * PT_MM : (b.lw ?? 0.25) * PT_MM * 0.5) / s;   // points, then into the space's own units
  ops.push(setLineWidth(lw));
  for (const pl of b.lines) { if (pl.length < 4) continue; ops.push(moveTo(pl[0], pl[1])); for (let i = 2; i + 1 < pl.length; i += 2) ops.push(lineTo(pl[i], pl[i + 1])); }
  ops.push(stroke()); any = true;

In detail

Method
Arc to Bézier: pieces of at most 90°, control distance k·r with k = 4 ÷ 3 × tan(φ ÷ 4) along the tangents. Legend: each item's quantity on that sheet, 2 decimals (0 for counts).
Number types
64-bit numbers handed to pdf-lib's path operators. How pdf-lib prints them into the file is outside these files.
Tolerances
The number of pieces is ceil(|sweep| ÷ (π ÷ 2) − 1e-9), at least 1.
Limits
A 90° Bézier piece departs from the true arc by about 0.027 % of the radius at most, the usual figure for this construction. Text the standard Helvetica font cannot draw prints as a question mark. A drawing's sheet is not a plot: no plot styles are applied.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach