Finds the run of cabinets a countertop covers, pushes the front edges out by the overhang, and writes the countertop's shape, area, perimeter and front edge length.
How the precision is kept
Cabinet footprints are built from their stored width and depth, turned and moved to the plan in 64-bit millimeters. The footprints are joined by the room-loop code with a 0.5 mm snap. Area, perimeter and front edge go into the file through the 6-decimal rounding of the writer.
Calculations under this policy
4 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.
Each counter-ready cabinet's footprint, as a rectangle or an L, in the level's plan.
(X, Y) = (pₓ + x·cos r − y·sin r, p_y + x·sin r + y·cos r), then the level's inverse matrix
Width, depth and height are read from the element's stored values and multiplied by the file unit. One cosine and sine of the placement angle per cabinet; the result is moved into the level's axes with the inverse of its world matrix.
res-counter.js
lines 32 to 61
BimCraft 0.8.1
// ------------------------------------------------------------------------------------------------------------ cabinets
export function counterCabinets(w, storey) {
const out = [];
const st = w.api.GetLine(w.mid, storey, false);
if (!st?.ObjectPlacement) return out;
const Mi = inv4(w.storeyWorld(storey));
const so = w.storeyOf();
for (const p of w.placements()) {
if (so.get(p.id) !== storey) continue;
const src = w.readPset(p.id, 'CadCraft_SourceAttributes') || {};
if (String(src['CC-COUNTER-READY'] || '').toLowerCase() !== 'yes') continue;
const eq = p.equipment || {};
const wid = Number(eq.Width) * w.unitMm, dep = Number(eq.Depth) * w.unitMm, top = Number(eq.Height) * w.unitMm;
if (!(wid > 0 && dep > 0 && top > 0)) continue;
const r = p.rot * Math.PI / 180, c = Math.cos(r), s = Math.sin(r);
const toPlan = (x, y) => { const X = p.x + x * c - y * s, Y = p.y + x * s + y * c; const q = apply4(Mi, X, Y, 0); return [q[0], q[1]]; };
const leg = /^L\s+([\d.]+)/.exec(String(src['CC-FOOTPRINT'] || ''));
let local, fronts;
if (leg) {
const L = Number(leg[1]), h = wid / 2;
local = [[-h, -dep], [-h + L, -dep], [-h + L, -L], [h, -L], [h, 0], [-h, 0]];
fronts = [[1, 2], [2, 3]]; // the two faces towards the room; the ends meet the next cabinets
} else {
local = [[-wid / 2, -dep], [wid / 2, -dep], [wid / 2, 0], [-wid / 2, 0]];
fronts = [[0, 1]];
}
out.push({ id: p.id, name: p.name, top, local, poly: local.map(([x, y]) => toPlan(x, y)), fronts, toPlan });
}
return out;
}
Countertop runs
64-bit
The outline, area and perimeter of each joined run of cabinets with the overhang added.
front edge points += overhang × outward normal; run = union of the footprints
Each front edge's two points are moved along the edge's unit normal; a point shared by two fronts is moved along both. The union is made by planLoops with a 0.5 mm snap, and the area and perimeter are those of the joined outline.
res-counter.js
lines 62 to 81
BimCraft 0.8.1
/** Each cabinet's footprint with its front edges pushed out by `overhang` (in its own axes), then the union of them all. */
export function counterRuns(cabinets, { overhang = COUNTER_DEFAULTS.overhang } = {}) {
const polys = cabinets.map((cb) => {
const q = cb.local.map((p) => p.slice());
// push each front edge out: move its two points by the overhang along the edge's outward normal (an L's inner corner,
// on both fronts, moves along both: where the two pushed edges meet, as the edges are square)
for (const [i, j] of cb.fronts) {
const a = cb.local[i], b = cb.local[j], L = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
const n = [(b[1] - a[1]) / L, -(b[0] - a[0]) / L]; // right of a→b = outward for an anticlockwise footprint
for (const k of [i, j]) { q[k][0] += n[0] * overhang; q[k][1] += n[1] * overhang; }
}
return q.map(([x, y]) => cb.toPlan(x, y));
});
if (!polys.length) return [];
const L = planLoops(polys, { snap: 0.5 });
return L.outlines.map((o) => {
const inside = cabinets.filter((cb, i) => polys[i].some((p) => pointIn(p, o.poly)) || pointIn(centroidOf(polys[i]), o.poly));
return { poly: o.poly, holes: [], area: Math.abs(o.area), perimeter: o.perimeter, cabinets: inside.map((cb) => cb.id), top: Math.max(...inside.map((cb) => cb.top), 0) };
});
}
Point in a run
64-bit
Tests whether a point is inside a run's outline or on its edge.
even-odd ray test, or within 1 mm of an edge
The ray test divides by the y difference of an edge only when its ends lie on opposite sides of the point's y. A point less than 1 mm from an edge, inside the edge's box grown by 1 mm, also counts as inside.
res-counter.js
lines 83 to 92
BimCraft 0.8.1
function pointIn(pt, poly) {
let inside = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const [xi, yi] = poly[i], [xj, yj] = poly[j];
if ((yi > pt[1]) !== (yj > pt[1]) && pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi) inside = !inside;
}
// on the boundary counts as inside (a footprint corner on the run's outline)
if (!inside) for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { const a = poly[j], b = poly[i], d = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1; if (Math.abs((b[0] - a[0]) * (pt[1] - a[1]) - (b[1] - a[1]) * (pt[0] - a[0])) / d < 1 && Math.min(a[0], b[0]) - 1 <= pt[0] && pt[0] <= Math.max(a[0], b[0]) + 1 && Math.min(a[1], b[1]) - 1 <= pt[1] && pt[1] <= Math.max(a[1], b[1]) + 1) return true; }
return inside;
}
Countertop quantities
64-bit
The area, perimeter and front edge length stored for a countertop.
Area = round(mm² ÷ 10⁶, 6) m²; FrontEdge = Σ lengths of the cabinets' front edges
Area is stored in m² rounded to 6 decimals (1 mm²); perimeter and front edge in file units rounded to 6 decimals. The front edge sums Math.hypot lengths of the cabinets' own front edges, without the overhang.
res-counter.js
lines 131 to 134
BimCraft 0.8.1
let front = 0;
const cabs = counterCabinets(w, c.storey).filter((cb) => run.cabinets.includes(cb.id));
for (const cb of cabs) for (const [i, j] of cb.fronts) front += Math.hypot(cb.local[j][0] - cb.local[i][0], cb.local[j][1] - cb.local[i][1]);
w.upsertPset(c.id, 'CadCraft_CountertopQuantities', [['Area', 'IFCAREAMEASURE', round(run.area / 1e6, 6)], ['Perimeter', 'IFCLENGTHMEASURE', round(w.u(run.perimeter))], ['FrontEdge', 'IFCLENGTHMEASURE', round(w.u(front))]]);
In detail
Method
Rectangle or L footprint per cabinet; front edges moved along their outward normal; union of the footprints (planLoops in model-rooms.js); front edge = sum of the cabinets' front edge lengths.
Number types
64-bit numbers in plan millimeters. The default overhang and thickness are 1 and 1.5 times 25.4, which is exact.
Tolerances
Union snap 0.5 mm. A point within 1 mm of the outline counts as inside. The change key rounds numbers to 0.1 mm.
Limits
The front edge length is measured on the cabinets, before the overhang is added, so at an L-shaped corner it is not the length of the countertop's own front edge.
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.