Resources Numerical accuracy Calculation policy

Counting blocks, shapes and texts in a CAD drawing

Finds every block reference, shape and text of a DWG or DXF drawing with its place, and counts the ones that match.

How the precision is kept

A count is the length of a list: a whole number, with no floating point in it. Floating point is used to place each object (block transforms multiplied down the nesting, array rows and columns) and to decide which sheet and viewport shows it.

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.

cadcount.js TakeoffCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-takeoffcraft-cad.mjs

Product of two transforms

64-bit

Combines two 2 × 3 affine matrices.

Six sums of two or three products in 64-bit, no compensation. Used for every block level, so rounding adds up along the nesting.

  • cadcount.js
  • lines 53 to 57
  • TakeoffCraft 0.8.1
const mul = (p, q) => [
  p[0] * q[0] + p[2] * q[1], p[1] * q[0] + p[3] * q[1],
  p[0] * q[2] + p[2] * q[3], p[1] * q[2] + p[3] * q[3],
  p[0] * q[4] + p[2] * q[5] + p[4], p[1] * q[4] + p[3] * q[5] + p[5],
];

Place of a block reference and its array cells

64-bit

The point of each copy of a block reference, and the transform its contents get.

p = M·(insert + R(rot)·(c·dx, r·dy))

The cell offset (column × spacing, row × spacing) is rotated by the reference's rotation and added to the insertion point before the outer matrix is applied. The contents' transform is translate(insert) · rotate · scale · translate(−base + offset ÷ scale), three matrix products, before it is multiplied onto the outer matrix. The reported rotation adds atan2 of the outer matrix.

  • cadcount.js
  • lines 200 to 208
  • TakeoffCraft 0.8.1
const dx = c * num(e.colSpacing, 0), dy = r * num(e.rowSpacing, 0);
const p = at(m, e.insert.x + rc * dx - rs * dy, e.insert.y + rs * dx + rc * dy);
out.refs.push({
  id: seq++, kind: 'insert', type: 'INSERT', name, key: name.toLowerCase(), anon: !dyn && String(e.block).startsWith('*') ? e.block : '', variant: dyn ? (dyn.state ?? '') : null, dynamic: !!dyn,
  layer, x: p.x, y: p.y, space: ctx.space, depth: ctx.depth, via: ctx.via, hidden: hiddenSelf, handle: e.handle || '', rot: rot + (m[1] || m[0] ? Math.atan2(m[1], m[0]) : 0),
  attribs, cell: cols * rows > 1 ? [r, c] : null,
});
// its content, one level down, at this copy's place
const local = mul(mul(translate(e.insert.x, e.insert.y), rotM(rot)), mul(scaleM(sx, sy), translate(-bp.x + dx / sx, -bp.y + dy / sy)));

Point that stands for a shape

64-bit

The one point a shape is placed and counted by: a center, a middle, or the middle of its box.

Midpoints are (a + b) ÷ 2 and box middles (min + max) ÷ 2 in 64-bit. The point only decides where the shape counts (which sheet and viewport), not a length.

  • cadcount.js
  • lines 73 to 97
  • TakeoffCraft 0.8.1
/** A point that stands for a shape (centre, middle, centre of its box); null when it has none. */
export function shapePoint(e) {
  const mid = (a, b) => (isPt(a) && isPt(b) ? { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } : null);
  const box = (vs) => {
    let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
    for (const v of vs || []) if (isPt(v)) { x0 = Math.min(x0, v.x); x1 = Math.max(x1, v.x); y0 = Math.min(y0, v.y); y1 = Math.max(y1, v.y); }
    return x0 <= x1 ? { x: (x0 + x1) / 2, y: (y0 + y1) / 2 } : null;
  };
  switch (e.type) {
    case 'LINE': return mid(e.start, e.end);
    case 'CIRCLE': case 'ARC': case 'ELLIPSE': return isPt(e.center) ? { x: e.center.x, y: e.center.y } : null;
    case 'POINT': return isPt(e.position) ? { x: e.position.x, y: e.position.y } : null;
    case 'LWPOLYLINE': case 'POLYLINE_2D': case 'POLYLINE_3D': case 'POLYLINE_PFACE': case 'POLYLINE_MESH': return box(e.vertices);
    case 'TEXT': case 'MTEXT': return isPt(e.insert) ? { x: e.insert.x, y: e.insert.y } : null;
    case 'SOLID': case 'TRACE': case '3DFACE': return box(e.points);
    case 'SPLINE': return box(e.fit?.length ? e.fit : e.control);
    case 'HATCH': { const l = (e.loops || [])[0]; return l ? box(l.polyline ? l.polyline.vertices : (l.edges || []).flatMap((d) => [d.start, d.end, d.center].filter(Boolean))) : null; }
    case 'LEADER': return box(e.vertices);
    case 'MULTILEADER': { const t = e.context?.text?.location || e.context?.block?.location; return isPt(t) ? { x: t.x, y: t.y } : null; }
    default:
      if (isDim(e.type)) return isPt(e.textMid) ? { x: e.textMid.x, y: e.textMid.y } : isPt(e.defPoint) ? { x: e.defPoint.x, y: e.defPoint.y } : null;
      for (const k of ['insert', 'position', 'center', 'start']) if (isPt(e[k])) return { x: e[k].x, y: e[k].y };
      return null;
  }
}

Count of block references

Exact

How many references of the named blocks there are, in total, by variant and by layer.

total = number of matching references

The total is found.length and the tallies are whole-number increments in a Map. No floating-point value takes part in the count.

  • cadcount.js
  • lines 303 to 318
  • TakeoffCraft 0.8.1
/**
 * Count block references. query: `{ names: ['SHELF-18X42', …] (case does not matter), variant: undefined (all) | 'Open' | '' (state not
 * known), nested: 'all' (default) | 'top' | 'inside', layers: [names] | null, hidden: 'skip' (default) | 'include' }`.
 * → `{ total, found: [ref], byVariant: [[state, n]…], byLayer: [[layer, n]…], hiddenSkipped }`
 */
export function countBlocks(ix, refs, query = {}) {
  const names = new Set((query.names || (query.name ? [query.name] : [])).map(fold));
  const nested = query.nested || 'all';
  const layers = query.layers ? new Set(query.layers) : null;
  const q = { ...query, layers };
  const { found, hiddenSkipped } = select(ix, refs, q, (r) => r.kind === 'insert' && names.has(r.key)
    && (nested === 'all' || (nested === 'top' ? r.depth === 0 : r.depth > 0))
    && (query.variant === undefined || query.variant === null || (r.dynamic && (r.variant ?? '') === query.variant)));
  const tally = (f) => { const m = new Map(); for (const r of found) { const k = f(r); m.set(k, (m.get(k) || 0) + 1); } return [...m].sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0]))); };
  return { total: found.length, found, byVariant: tally((r) => (r.dynamic ? r.variant ?? '' : null)).filter(([k]) => k !== null), byLayer: tally((r) => r.layer), hiddenSkipped };
}
cadset.js TakeoffCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-takeoffcraft-cad.mjs

Which sheet shows an object, and where

64-bit

The page point of a drawing object on a sheet, through the first viewport that contains it.

The point goes through the viewport matrix and is compared with the viewport's rectangle with no tolerance; a point on the edge is inside. The first viewport in the list that contains it takes it, so an object in two overlapping viewports is counted once.

  • cadset.js
  • lines 215 to 230
  • TakeoffCraft 0.8.1
/** Where a drawing object (in its own space) sits on this sheet, as a page point {x, y}; null when the sheet does not show it. */
export function placeRef(sheet, ref, ix) {
  const m = sheet.toPage;
  if (sheet.kind === 'model') return ref.space === 'model' ? { x: m[0] * ref.x + m[4], y: m[3] * ref.y + m[5] } : null;
  if (ref.space === sheet.key) return { x: m[0] * ref.x + m[4], y: m[3] * ref.y + m[5] };
  if (ref.space !== 'model') return null;
  const handle = ix?.layers.get(ref.layer)?.handle;
  for (const v of sheet.viewports) {
    const vm = vpMatrix(v);
    const px = vm[0] * ref.x + vm[2] * ref.y + vm[4], py = vm[1] * ref.x + vm[3] * ref.y + vm[5];
    if (px < v.rect.x0 || px > v.rect.x1 || py < v.rect.y0 || py > v.rect.y1) continue;
    if (handle && v.frozen && v.frozen.has(handle)) return { hidden: true };   // frozen in this viewport only
    return { x: sheet.toPage[0] * px + sheet.toPage[4], y: sheet.toPage[3] * py + sheet.toPage[5] };
  }
  return null;
}

In detail

Method
The drawing is walked once. Each reference's point is the product of the transforms above it applied to its insertion point; arrays are expanded cell by cell. A sheet shows a model object when its point falls in one of the sheet's viewports.
Number types
Integer counts. 64-bit numbers for the 2 × 3 matrices and points.
Tolerances
The viewport test has none: a point exactly on a viewport's edge is inside. A scale of 0 is read as 1.
Limits
Nesting stops at 20 levels, references at 1,500,000 and one array at 1,000,000 cells; each stop is reported. Each level of nesting adds at least four matrix products, so placement rounding grows with depth.

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