Reads alternative building footprints, places each inside the buildable envelope, and writes the envelope back as a file in millimeters.
How the precision is kept
Footprints are converted from millimeters to project units in 64-bit. The position is found by a grid search (four quarter-turn rotations, five passes of a 7 × 7 grid, each a third of the last), so the placement is resolved to 1/243 of half the envelope's larger side, not to 64-bit accuracy. The envelope file rounds coordinates to 0.1 mm, the area to 0.01 m² and the angle to 0.1 degree.
Open item. Use the same foot for a feet project here as the IFC export does (it treats a feet project without a coordinate system as US survey feet; this file uses 0.3048).
Calculations under this policy
6 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 footprint area and gross floor area of each alternative, in square meters.
area (m²) = |A| ÷ 10⁶; gross = area × floors
The shoelace area in mm² is divided by the exact factor 1e6. Floors are rounded to a whole number and limited to 200; a floor height outside 1000…20000 mm skips the option.
Orientation signs from 64-bit cross products with no tolerance; the test is not an exact predicate, so edges that only just touch can be decided either way.
massing-options.js
lines 45 to 53
CivilCraft 0.8.1
/** True when no two non-neighbouring edges cross (a plain polygon). */
export function simple(pts) {
const n = pts.length, X = (a, b, c, d) => {
const o = (p, q, r) => Math.sign((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x));
return o(a, b, c) !== o(a, b, d) && o(c, d, a) !== o(c, d, b);
};
for (let i = 0; i < n; i++) for (let j = i + 2; j < n; j++) { if (i === 0 && j === n - 1) continue; if (X(pts[i], pts[(i + 1) % n], pts[j], pts[(j + 1) % n])) return false; }
return true;
}
Turning and moving a footprint
64-bit
Rotates a footprint about its centroid and moves it to a position.
x′ = t_x + (x − c_x)·cos a − (y − c_y)·sin a; y′ = t_y + (x − c_x)·sin a + (y − c_y)·cos a
The centroid is subtracted before the rotation, so the rotation works on small numbers; cos and sin are evaluated once per call.
massing-options.js
line 55
CivilCraft 0.8.1
const turn = (pts, c, ang, to) => { const co = Math.cos(ang), si = Math.sin(ang); return pts.map((p) => ({ x: to.x + (p.x - c.x) * co - (p.y - c.y) * si, y: to.y + (p.x - c.x) * si + (p.y - c.y) * co })); };
Placing an option in the envelope
64-bit
The position and rotation at which as much of the footprint as possible lies inside the buildable envelope.
k = 0.001 ÷ meters per project unit; inside = area(envelope ∩ footprint) ÷ area(footprint)
Four rotations a quarter turn apart, each searched on a 7 × 7 grid in five passes whose span shrinks by 3, so the final grid step is the starting span ÷ 243. Shares are compared with 1e-9. The inside share is clamped to at most 1. The intersection area comes from insideArea in zoning.js.
massing-options.js
lines 57 to 82
CivilCraft 0.8.1
export function fitOption(option, polygon, units = 'ft') {
const k = 0.001 / (METRES_PER[units] || 0.3048); // project units per mm
const base = option.pts.map((p) => ({ x: p.x * k, y: p.y * k }));
const c0 = centroid(base), area = Math.abs(signedArea(base));
if (!polygon || polygon.length < 3) return { ok: false, error: 'mo.err.envelope' };
const ec = centroid(polygon), edge = longestEdge(polygon), own = longestEdge(base);
let lo = { x: Infinity, y: Infinity }, hi = { x: -Infinity, y: -Infinity };
for (const p of polygon) { lo = { x: Math.min(lo.x, p.x), y: Math.min(lo.y, p.y) }; hi = { x: Math.max(hi.x, p.x), y: Math.max(hi.y, p.y) }; }
let best = null;
for (let q = 0; q < 4; q++) {
const rot = edge.ang - own.ang + (q * Math.PI) / 2;
let span = Math.max(hi.x - lo.x, hi.y - lo.y) / 2, at = { ...ec };
let cur = null;
for (let pass = 0; pass < 5; pass++, span /= 3) {
for (let i = -3; i <= 3; i++) for (let j = -3; j <= 3; j++) {
const to = { x: at.x + (i / 3) * span, y: at.y + (j / 3) * span };
const pts = turn(base, c0, rot, to), inside = insideArea(polygon, pts) / area;
const d = Math.hypot(to.x - ec.x, to.y - ec.y);
if (!cur || inside > cur.inside + 1e-9 || (Math.abs(inside - cur.inside) <= 1e-9 && d < cur.d)) cur = { pts, inside, centre: to, rot, d };
}
at = cur.centre;
}
if (!best || cur.inside > best.inside + 1e-9 || (Math.abs(cur.inside - best.inside) <= 1e-9 && cur.d < best.d)) best = cur;
}
return { ok: true, pts: best.pts, centre: best.centre, rotation: ((best.rot * 180) / Math.PI + 360) % 360, inside: Math.min(1, best.inside), outside: Math.max(0, area * (1 - best.inside)) };
}
Option to a mass
64-bit
Makes a project mass from a placed option, with its floor height in project units.
floor height = mm × 0.001 ÷ meters per project unit
One multiplication. For a feet project the divisor is 0.3048 (METRES_PER.ft), the international foot.
Writes the buildable envelope in millimeters, turned so its longest edge lies along x.
mm = meters per project unit × 1000; value = round(v × 10) ÷ 10
Coordinates are rounded to 0.1 mm with Math.round(v × 10) ÷ 10, the area to 0.01 m² and the angle to 0.1 degree before they are written. The turned polygon and the project polygon are rounded separately, so one cannot be rebuilt exactly from the other and the rounded angle.
massing-options.js
lines 89 to 104
CivilCraft 0.8.1
export function envelopeFile(parcel, zone, units = 'ft') {
const env = envelope(parcel, zone);
if (!env.poly) return { ok: false, problem: env.problem || 'fold' };
const mm = (METRES_PER[units] || 0.3048) * 1000;
const e = longestEdge(env.poly), c = e.ang, co = Math.cos(-c), si = Math.sin(-c);
const turned = env.poly.map((p) => ({ x: p.x * co - p.y * si, y: p.x * si + p.y * co }));
const minx = Math.min(...turned.map((p) => p.x)), miny = Math.min(...turned.map((p) => p.y));
const round = (v) => Math.round(v * 10) / 10;
return { ok: true, area: env.area, file: {
format: 'cadcraft-massing-envelope', version: 1, units: 'mm', parcel: clean(parcel.name || parcel.id, 80),
note: 'polygon: the buildable area with its longest edge along +x and its lowest corner at 0,0 — pass it as `envelope` to BimCraft\'s massing_describe. project: the same polygon in the lot\'s own coordinates; angle is the turn between the two.',
angle: round((c * 180) / Math.PI), areaM2: Math.round(env.area * (mm / 1000) ** 2 * 100) / 100,
polygon: turned.map((p) => ({ x: round((p.x - minx) * mm), y: round((p.y - miny) * mm) })),
project: env.poly.map((p) => ({ x: round(p.x * mm), y: round(p.y * mm) })),
} };
}
In detail
Method
Grid search for the largest share of the footprint inside the envelope; ties go to the position nearest the envelope's centroid.
Number types
64-bit numbers.
Tolerances
Shares equal within 1e-9. A footprint with an area under 1 mm² is refused. Envelope file rounded to 0.1 mm, 0.01 m², 0.1°.
Limits
The search can miss a better position between grid points or at another rotation. A feet project is converted with 0.3048 m per foot (METRES_PER.ft in units.js).
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.