Resources Numerical accuracy Calculation policy

Lengths and areas taken from CAD objects

Turns the lines, arcs, circles, polylines, hatches, ellipses and splines of a DWG or DXF sheet into runs and outlines that can be measured.

How the precision is kept

Lines, arcs, circles and polylines keep their exact shape: arcs become end points plus a bulge and are measured by the arc formulas. Ellipses, splines, curve-fit polylines and hatches bounded by edges are broken into chords and marked as approximate. Coordinates are 64-bit and are moved to the page by the sheet's matrix before anything is measured.

Calculations under this policy

6 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.

cadgeom.js TakeoffCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-takeoffcraft-cad.mjs

One CAD object as points and bulges

64-bit

The run or outline that stands for a line, arc, circle, polyline, ellipse, spline or 2D solid.

arc: p = c + r·(cos a, sin a), b = tan(sweep ÷ 4); ellipse: n = max(24, ⌈1440·sweep ÷ 2π⌉) chords

An arc is stored as center + r·(cos, sin) at its two angles with bulge tan(sweep ÷ 4); the radius is not carried on, so the length is later rebuilt from the chord. A circle is two points at x ± r with bulge 1. An ellipse is sampled at n = max(24, ceil(1440·sweep ÷ 2π)) parameter steps and flagged approximate; a spline's points come from the drawing compiler and are flagged approximate.

  • cadgeom.js
  • lines 43 to 90
  • TakeoffCraft 0.8.1
/** One entity → { pts, closed, holes, approx } in the entity's own coordinates (before the sheet transform), or null / a reason. */
function shapeOf(e) {
  const bulgeVs = (vs, closed) => ({ pts: vs.filter(isPt).map((v) => ({ x: v.x, y: v.y, ...(fin(v.bulge) && v.bulge ? { b: v.bulge } : {}) })), closed: !!closed });
  switch (e.type) {
    case 'LINE': return isPt(e.start) && isPt(e.end) ? { pts: [{ x: e.start.x, y: e.start.y }, { x: e.end.x, y: e.end.y }], closed: false } : null;
    case 'ARC': {
      if (!isPt(e.center) || !(e.radius > 0) || !fin(e.startAngle) || !fin(e.endAngle)) return null;
      let sweep = e.endAngle - e.startAngle; while (sweep <= 0) sweep += TAU;
      const a = e.startAngle, b = e.endAngle, r = e.radius;
      return { pts: [{ x: e.center.x + r * Math.cos(a), y: e.center.y + r * Math.sin(a), b: Math.tan(sweep / 4) }, { x: e.center.x + r * Math.cos(b), y: e.center.y + r * Math.sin(b) }], closed: false };
    }
    case 'CIRCLE': {
      if (!isPt(e.center) || !(e.radius > 0)) return null;
      const { x, y } = e.center, r = e.radius;
      return { pts: [{ x: x + r, y, b: 1 }, { x: x - r, y, b: 1 }], closed: true };   // two half circles: exact length and area
    }
    case 'LWPOLYLINE': { const s = bulgeVs(e.vertices || [], e.closed); return s.pts.length >= 2 ? s : null; }
    case 'POLYLINE_2D': {
      const s = bulgeVs((e.vertices || []).filter((v) => !(v.flags & 16)), e.closed);
      return s.pts.length >= 2 ? { ...s, approx: !!(e.flags & 6) || (e.curveType || 0) > 4 } : null;   // a curve- or spline-fit polyline is only its frame
    }
    case 'ELLIPSE': {
      if (!isPt(e.center) || !isPt(e.majorAxis) || !(e.ratio > 0)) return null;
      let s0 = fin(e.startParam) ? e.startParam : 0, s1 = fin(e.endParam) ? e.endParam : TAU;
      let sweep = s1 - s0; while (sweep <= 0) sweep += TAU;
      const full = Math.abs(sweep - TAU) < 1e-9;
      const n = Math.max(24, Math.ceil(1440 * sweep / TAU)), mx = e.majorAxis.x, my = e.majorAxis.y, len = Math.hypot(mx, my), rot = Math.atan2(my, mx), cr = Math.cos(rot), sr = Math.sin(rot);
      const pts = [];
      for (let i = 0; i < (full ? n : n + 1); i++) {
        const t = s0 + sweep * i / n, x = len * Math.cos(t), y = len * e.ratio * Math.sin(t);
        pts.push({ x: e.center.x + x * cr - y * sr, y: e.center.y + x * sr + y * cr });
      }
      return { pts, closed: full, approx: true };
    }
    case 'SPLINE': {
      const flat = splinePoints(e);
      const pts = []; for (let i = 0; i + 1 < flat.length; i += 2) if (fin(flat[i]) && fin(flat[i + 1])) pts.push({ x: flat[i], y: flat[i + 1] });
      return pts.length >= 2 ? { pts, closed: !!(e.flags & 1) || !!e.closed, approx: true } : null;
    }
    case 'SOLID': case 'TRACE': {
      const p = (e.points || []).filter(isPt);
      if (p.length < 3) return null;
      const q = p.length > 3 ? [p[0], p[1], p[3], p[2]] : [p[0], p[1], p[2]];   // DXF order 1, 2, 4, 3 is the outline
      return { pts: q.map((v) => ({ x: v.x, y: v.y })), closed: true };
    }
    default: return null;
  }
}

Hatch as outlines with holes

64-bit

The outer loops of a hatch, each with the loops inside it as cut-outs.

A polyline loop keeps its bulges. A loop made of edges is flattened by the drawing compiler and flagged approximate. Nesting is decided by sorting the loops by area and testing one point of each loop against the larger ones; a loop at odd depth is a hole.

  • cadgeom.js
  • lines 92 to 117
  • TakeoffCraft 0.8.1
/** A hatch → area shapes: the outer loops, each with the loops inside it as holes. */
function hatchShapes(e) {
  const loops = [];
  for (const loop of e.loops || []) {
    if (loop.polyline) {
      const pts = (loop.polyline.vertices || []).filter(isPt).map((v) => ({ x: v.x, y: v.y, ...(fin(v.bulge) && v.bulge ? { b: v.bulge } : {}) }));
      if (pts.length >= 2) loops.push({ pts, approx: false });
    } else {
      for (const ring of hatchRings({ loops: [loop] })) {   // edges (lines, arcs, ellipses, splines) joined and flattened finely
        const pts = []; for (let i = 0; i + 1 < ring.length; i += 2) pts.push({ x: ring[i], y: ring[i + 1] });
        if (pts.length >= 3) loops.push({ pts, approx: true });
      }
    }
  }
  for (const l of loops) { l.area = Math.abs(G.signedArea(l.pts)); }
  loops.sort((a, b) => b.area - a.area);
  // nesting: a loop's depth is how many bigger loops contain one of its points (even = an outer boundary, odd = a hole)
  const depth = loops.map((l, i) => loops.slice(0, i).filter((o) => l.area < o.area && G.pointInPolygon(G.flatten(l.pts, true)[0] || l.pts[0], o.pts)).length);
  const out = [];
  loops.forEach((l, i) => {
    if (depth[i] % 2) return;
    const holes = loops.filter((h, j) => depth[j] === depth[i] + 1 && G.pointInPolygon(G.flatten(h.pts, true)[0] || h.pts[0], l.pts)).map((h) => h.pts);
    out.push({ pts: l.pts, closed: true, holes, approx: l.approx || loops.some((h, j) => holes.includes(h.pts) && h.approx) });
  });
  return out;
}

Object placed on the sheet

64-bit

Applies the sheet or viewport matrix to an object's points and decides whether a viewport shows it.

p′ = T·(mirror·x, y); b′ = b × sign(det T × mirror)

Each point is one matrix-times-point in 64-bit. The bulge is carried through with only its sign flipped under a mirror, which holds because the sheet and viewport matrices built in cadset.js are a uniform scale and a rotation. The viewport test compares the object's box (arcs as 5° chords) with the viewport rectangle ± 1e-6 page units; an object partly outside is left out and counted as clipped.

  • cadgeom.js
  • lines 128 to 153
  • TakeoffCraft 0.8.1
const take = (entities, T, vp, onTaken = null) => {
  const det = T[0] * T[3] - T[1] * T[2];
  for (const e of entities) {
    if (!e || e.invisible) continue;
    if (items.length >= cap.items) { truncated = true; return; }
    if (SKIP_NAMES.has(e.type)) continue;
    const layer = e.layer || '0';
    if (!layerShown(cad, layer)) continue;
    if (vp?.frozen?.size && vp.frozen.has(cad.layerHandles.get(layer))) continue;
    const mirror = e.extrusion && e.extrusion.z < -0.5 ? -1 : 1;   // a -Z extrusion mirrors X (as the sheet compiler draws it)
    const shapes = e.type === 'HATCH' ? hatchShapes(e) : (() => { const s = shapeOf(e); return s ? [s] : null; })();
    if (!shapes || !shapes.length) {
      if (!/^(DIMENSION|ACAD_TABLE|ARC_DIMENSION|LARGE_RADIAL)/.test(e.type)) skip(e.type === 'UNKNOWN' ? (e.dxfName || e.className || 'UNKNOWN') : e.type);   // listed: never dropped without a word
      continue;
    }
    for (const s of shapes) {
      const flip = (det * mirror < 0) ? -1 : 1;                      // a mirror turns an arc's direction round
      const tp = (v) => ({ x: T[0] * mirror * v.x + T[2] * v.y + T[4], y: T[1] * mirror * v.x + T[3] * v.y + T[5], ...(v.b ? { b: v.b * flip } : {}) });
      const pts = s.pts.map(tp), holes = (s.holes || []).map((h) => h.map(tp));
      const bb = G.bounds(G.flatten(pts, s.closed));
      if (vp && !(bb.minX >= vp.pageRect.x0 - 1e-6 && bb.maxX <= vp.pageRect.x1 + 1e-6 && bb.minY >= vp.pageRect.y0 - 1e-6 && bb.maxY <= vp.pageRect.y1 + 1e-6)) { clipped++; continue; }
      onTaken?.(e);
      items.push({ id: items.length, handle: e.handle || '', type: e.type, layer, closed: s.closed, pts, holes, approx: !!s.approx, bbox: bb, label: e.type + (e.handle ? ' ' + e.handle : '') });
    }
  }
};

Marks made from picked objects

64-bit

The point lists a linear or an area item takes from the picked objects.

Points and bulges are copied unchanged. A closed object taken as a run gets its first point added at the end so the closing segment is measured.

  • cadgeom.js
  • lines 195 to 216
  • TakeoffCraft 0.8.1
/**
 * The marks a linear or an area item takes from `items`. A linear item takes a line, arc, polyline, spline… by its length (a closed one by
 * its perimeter); an area item takes a closed shape by its area (a circle, a closed polyline, a hatch with its holes). What does not fit is
 * counted in `skipped` ('open' for an open run offered to an area item, 'hatch' for a hatch offered to a linear item).
 */
export function marksFor(items, kind) {
  const list = [], skipped = { open: 0, hatch: 0 };
  let approx = 0;
  for (const it of items) {
    if (kind === 'area') {
      if (!it.closed) { skipped.open++; continue; }
      list.push({ pts: it.pts.map((p) => ({ ...p })), holes: it.holes.map((h) => h.map((p) => ({ ...p }))) });
    } else {
      if (it.type === 'HATCH') { skipped.hatch++; continue; }
      const pts = it.pts.map((p) => ({ ...p }));
      if (it.closed) pts.push({ x: pts[0].x, y: pts[0].y });          // the closing segment uses the last point's bulge: add the start again
      list.push({ pts, holes: [] });
    }
    if (it.approx) approx++;
  }
  return { list, skipped, approx };
}

Length and area of what is picked

64-bit

The total length or net area of the picked objects in page units.

len = Σ length; area = Σ (gross − cut-outs)

A plain 64-bit sum in list order of perimeter, run length or outline-minus-holes area from geom.js. The caller multiplies by the sheet's scale, or its square.

  • cadgeom.js
  • lines 218 to 226
  • TakeoffCraft 0.8.1
/** Lengths and areas of what is offered, in page units (the caller applies the sheet's scale). */
export function totals(items, kind) {
  let len = 0, area = 0;
  for (const it of items) {
    if (kind === 'area') { if (it.closed) area += G.areaWithHoles(it.pts, it.holes).net; }
    else if (it.type !== 'HATCH') len += it.closed ? G.perimeter(it.pts) : G.polylineLength(it.pts);
  }
  return { len, area };
}
takeoffcraft.js TakeoffCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-takeoffcraft.mjs

Picked objects in real units

64-bit

The length or area of the picked CAD objects shown above the sheet.

SF = area × k²; ft = len × k

Area is the page-unit total × ftPerUnit × ftPerUnit; length is the total × ftPerUnit. Rounded to 2 decimals or to the inch fraction for the text only.

  • takeoffcraft.js
  • lines 3120 to 3122
  • TakeoffCraft 0.8.1
const tot = geomMod.totals(sel, kind);
const marks = geomMod.marksFor(sel, kind);
sum.textContent = (kind === 'area' ? t('cad.geom.sel.area', { n: marks.list.length, area: U.fixed(tot.area * sc.ftPerUnit * sc.ftPerUnit, 2) }) : t('cad.geom.sel.linear', { n: marks.list.length, len: ftIn(tot.len * sc.ftPerUnit) }))

In detail

Method
Each object becomes a point list with bulges. The sheet transform is applied to the points; a mirror flips the sign of each bulge. Lengths and areas then come from geom.js on the page-space points, times the sheet's scale.
Number types
64-bit numbers.
Tolerances
An ellipse is a full one when |sweep − 2π| < 1e-9 radians. An object belongs to a viewport when its whole box is inside the viewport's rectangle widened by 1e-6 page units (absolute). An extrusion with z < −0.5 mirrors X.
Limits
An ellipse is cut into 1440 chords per full turn (0.25° of its parameter each, at least 24). Chords lie inside the curve, so approximate objects measure slightly short. Objects inside blocks, 3D polylines, meshes, regions and solids are not taken; they are listed.

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