Adds up a room program, finds building shapes that hold it inside a buildable envelope, and reports each option's footprint, gross area, surplus and site coverage.
How the precision is kept
Areas are 64-bit sums and the shapes are scaled to the wanted area in closed form. Whether a shape fits the envelope is decided by testing sample points on a grid of trial positions, not by an exact polygon test. Reported areas are rounded to 0.01 m² and the option's corner points to whole millimeters.
Calculations under this policy
7 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.
The net and gross floor area and the number of rooms in the program.
net = Σ count × area; gross = net ÷ efficiency
Rows are summed in order in m². The efficiency must be above 0.3 and at most 1, else the default 0.8 is used, which also guards the division. Counts are rounded to whole numbers when the program is cleaned.
Tests whether a turned rectangle lies inside the envelope.
Seventeen points (four per side and the center) are moved 1 mm toward the center and tested with an even-odd ray test; then no envelope corner may lie more than 1 mm inside the rectangle. The envelope's edges between those samples are not tested against the rectangle's edges.
program-mass.js
lines 52 to 66
BimCraft 0.8.1
function rectFits(P, cx, cy, w, d, ang) {
const c = Math.cos(ang), s = Math.sin(ang);
const pt = (u, v) => [cx + u * c - v * s, cy + u * s + v * c];
const corners = [pt(-w / 2, -d / 2), pt(w / 2, -d / 2), pt(w / 2, d / 2), pt(-w / 2, d / 2)];
const probe = [];
for (let i = 0; i < 4; i++) for (const f of [0, 0.25, 0.5, 0.75]) probe.push([corners[i][0] + (corners[(i + 1) % 4][0] - corners[i][0]) * f, corners[i][1] + (corners[(i + 1) % 4][1] - corners[i][1]) * f]);
probe.push([cx, cy]);
// pulled 1 mm inward so a rectangle that touches the envelope's edge counts as inside
const pulled = probe.map((q) => { const dx = cx - q[0], dy = cy - q[1], l = Math.hypot(dx, dy) || 1; return [q[0] + (dx / l), q[1] + (dy / l)]; });
if (!pulled.every((q) => inside(P, q))) return null;
// no envelope corner strictly inside the rectangle (a concave envelope)
const inv = (q) => { const x = q[0] - cx, y = q[1] - cy; return [x * c + y * s, -x * s + y * c]; };
for (const q of P) { const [u, v] = inv(q); if (Math.abs(u) < w / 2 - 1 && Math.abs(v) < d / 2 - 1) return null; }
return corners;
}
Rectangle of a wanted area
64-bit
The rectangle of a given proportion and area nearest the middle of the envelope.
short = √(area ÷ ratio); long = short × ratio
The sides give the wanted area to 64-bit rounding. The rectangle is lined up with the envelope's longest edge or square to it, and its center is tried on a 49 × 49 grid over the envelope's bounding box.
program-mass.js
lines 67 to 87
BimCraft 0.8.1
/** The rectangle of aspect `ratio` (long side ÷ short side) with at least `want` mm² that fits, centred as near the middle of the envelope as it can be; null when none. */
function fitRect(P, ratio, want) {
const xs = P.map((q) => q[0]), ys = P.map((q) => q[1]);
const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys);
// the longest edge sets the axis, so a rectangle lines up with the lot
let best = { l: 0, a: 0 };
for (let i = 0; i < P.length; i++) { const a = P[i], b = P[(i + 1) % P.length], l = Math.hypot(b[0] - a[0], b[1] - a[1]); if (l > best.l) best = { l, a: Math.atan2(b[1] - a[1], b[0] - a[0]) }; }
const short = Math.sqrt(want / ratio), long = short * ratio;
const mid = [(x0 + x1) / 2, (y0 + y1) / 2];
const N = 24, hit = [];
for (const ang of [best.a, best.a + Math.PI / 2]) {
for (let i = -N; i <= N; i++) for (let j = -N; j <= N; j++) {
const cx = mid[0] + ((x1 - x0) / 2) * (i / N), cy = mid[1] + ((y1 - y0) / 2) * (j / N);
const r = rectFits(P, cx, cy, long, short, ang);
if (r) hit.push({ r, d: Math.hypot(cx - mid[0], cy - mid[1]) + (ang === best.a ? 0 : 1) });
}
}
if (!hit.length) return null;
hit.sort((p, q) => p.d - q.d);
return hit[0].r;
}
L and U shapes of a wanted area
64-bit
An L-shaped or U-shaped footprint scaled to the wanted area and placed in the envelope.
k = √(wanted area ÷ template area)
The template is scaled about the mean of its corners, turned in four quarter turns from the longest envelope edge and optionally mirrored. Eight points per edge, pulled 1 mm toward the shape's mean point, must be inside the envelope.
program-mass.js
lines 111 to 137
BimCraft 0.8.1
/** The template scaled to `want` mm², whose corners and edge points are all inside the envelope with none of its corners inside the shape; the one nearest the middle, or null. */
function fitTemplate(P, key, want) {
const T0 = TEMPLATES[key], a0 = Math.abs(area(T0)), k = Math.sqrt(want / a0);
const xs = P.map((q) => q[0]), ys = P.map((q) => q[1]);
const x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys), mid = [(x0 + x1) / 2, (y0 + y1) / 2];
let axis = { l: 0, a: 0 };
for (let i = 0; i < P.length; i++) { const a = P[i], b = P[(i + 1) % P.length], l = Math.hypot(b[0] - a[0], b[1] - a[1]); if (l > axis.l) axis = { l, a: Math.atan2(b[1] - a[1], b[0] - a[0]) }; }
const cen = [T0.reduce((s, q) => s + q[0], 0) / T0.length, T0.reduce((s, q) => s + q[1], 0) / T0.length];
const N = 12, hit = [];
for (let turn = 0; turn < 4; turn++) for (const mirror of [false, true]) {
const ang = axis.a + (turn * Math.PI) / 2, c = Math.cos(ang), sn = Math.sin(ang);
for (let i = -N; i <= N; i++) for (let j = -N; j <= N; j++) {
const cx = mid[0] + ((x1 - x0) / 2) * (i / N), cy = mid[1] + ((y1 - y0) / 2) * (j / N);
const pts = T0.map(([u, v]) => { const uu = (mirror ? -(u - cen[0]) : u - cen[0]) * k, vv = (v - cen[1]) * k; return [cx + uu * c - vv * sn, cy + uu * sn + vv * c]; });
// sampled along every edge (every ~1.5 m of a 10 m template), pulled 1 mm toward the centre of the shape
const probe = [];
for (let e = 0; e < pts.length; e++) { const p = pts[e], q = pts[(e + 1) % pts.length]; for (let f = 0; f < 8; f++) probe.push([p[0] + (q[0] - p[0]) * (f / 8), p[1] + (q[1] - p[1]) * (f / 8)]); }
const gx = pts.reduce((s, q) => s + q[0], 0) / pts.length, gy = pts.reduce((s, q) => s + q[1], 0) / pts.length;
if (!probe.every((q) => { const dx = gx - q[0], dy = gy - q[1], l = Math.hypot(dx, dy) || 1; return inside(P, [q[0] + dx / l, q[1] + dy / l]); })) continue;
if (P.some((q) => inside(pts, q) && !pts.some((r) => Math.hypot(r[0] - q[0], r[1] - q[1]) < 2))) continue;
hit.push({ pts: anticlockwise(pts), d: Math.hypot(cx - mid[0], cy - mid[1]) + turn * 0.01 + (mirror ? 0.005 : 0) });
}
}
if (!hit.length) return null;
hit.sort((p, q) => p.d - q.d);
return hit[0].pts;
}
Rooms stacked on floors
64-bit
Assigns rooms to floors up to each floor's usable area.
placed when floor net + room area ≤ usable area + 1e-6
Room areas are added to each floor's running total in placement order; the 1e-6 m² allowance absorbs rounding in that sum. Each floor's total is rounded to 0.01 m² at the end.
program-mass.js
lines 89 to 102
BimCraft 0.8.1
/** Rooms to floors: rooms marked ground first, the rest largest first, each floor filled up to its usable area; a room never splits. */
function stackRooms(rows, floors, netPerFloor) {
const list = [];
for (const r of rows) for (let k = 0; k < r.count; k++) list.push({ name: r.name, area_m2: r.area_m2, ground: r.ground });
list.sort((a, b) => (b.ground ? 1 : 0) - (a.ground ? 1 : 0) || b.area_m2 - a.area_m2);
const stack = Array.from({ length: floors }, (_, i) => ({ floor: i + 1, rooms: [], net_m2: 0 }));
const over = [];
for (const rm of list) {
const slot = stack.find((f) => f.net_m2 + rm.area_m2 <= netPerFloor + 1e-6);
if (slot) { slot.rooms.push({ name: rm.name, area_m2: rm.area_m2 }); slot.net_m2 += rm.area_m2; } else over.push(rm.name);
}
for (const f of stack) f.net_m2 = Math.round(f.net_m2 * 100) / 100;
return { stack, over };
}
Areas reported for an option
64-bit
Footprint, gross area, required area, surplus and coverage of one massing option.
The footprint is computed from the unrounded corner points; the corner points kept for the option are rounded to whole millimeters with Math.round. Areas are rounded to 0.01 m² and coverage to 0.001 after the 64-bit arithmetic.
The option's points and floor height in the project's units.
x = mm × unit per mm
The whole-millimeter points are multiplied by the unit factor: the default 0.001 for meters is not exactly representable in binary, so each coordinate is the nearest 64-bit number to the decimal value.
program-mass.js
lines 178 to 181
BimCraft 0.8.1
/** A mass in the shape CivilCraft's massing.js reads (cleanMass): pts [{ x, y }] in the project's units (`unitPerMm`: 0.001 for metres, 1/304.8 for feet). */
export function civilMass(option, { unitPerMm = 0.001, use = '' } = {}) {
return { name: cleanText(option.name, 80), pts: option.pts.map((q) => ({ x: q[0] * unitPerMm, y: q[1] * unitPerMm })), floors: option.floors, floorHeight: option.floorHeight * unitPerMm, use: cleanText(use, 40) };
}
In detail
Method
Gross area = net area ÷ efficiency; rectangle sides √(area ÷ ratio) and ratio × that; templates scaled by √(wanted ÷ template area); even-odd point-in-polygon tests on sample points pulled 1 mm inward; rooms stacked largest first.
Number types
64-bit numbers; plan millimeters and square meters (1 m² = 1e6 mm²).
Tolerances
Trial positions: 49 × 49 for rectangles at two angles, 25 × 25 for L and U shapes at 8 orientations. Rectangle test: 16 edge points and the center. Template test: 8 points per edge. Envelope corner inside a rectangle counts when more than 1 mm from its edges. Floor filling allows 1e-6 m² over.
Limits
A shape can be reported as fitting when the envelope cuts into it between sample points. The footprint area is computed before the corners are rounded to whole millimeters, so it differs slightly from the area of the rounded outline that is built.
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.