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