Stores the site boundary with its area and perimeter, places the building on the site, adds or splits levels, and turns a site-study mass into walls and floors.
How the precision is kept
Outline points are moved between a level's plan and the site's axes with 4×4 matrix products in 64-bit millimeters. Area and perimeter are computed from those points and stored through the writer's 6-decimal rounding. The outline is also kept as text rounded to 3 decimals of the file unit, and that text is what the app reads the outline back from.
Open item. Keep the site outline text at full precision so the outline read back matches the points the area and perimeter were computed from.
Calculations under this policy
11 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 shoelace sum over absolute site coordinates in point order, with no compensation. For an outline far from the site origin the products are large and nearly cancel.
model-site.js
line 39
BimCraft 0.8.1
const area = (p) => { let s = 0; for (let i = 0; i < p.length; i++) { const a = p[i], b = p[(i + 1) % p.length]; s += a[0] * b[1] - a[1] * b[0]; } return s / 2; };
Site area and perimeter
64-bit
The GrossArea and GrossPerimeter stored for the site.
A = |shoelace|; P = Σ √(Δx² + Δy²)
Both are computed from the unrounded outline in site millimeters; the perimeter adds Math.hypot edge lengths in order. They are stored as a length rounded to 6 decimals of the file unit and an area in m² rounded to 6 decimals.
model-site.js
lines 120 to 121
BimCraft 0.8.1
const A = Math.abs(area(pts)), P = pts.reduce((sum, a, i) => { const b = pts[(i + 1) % pts.length]; return sum + Math.hypot(b[0] - a[0], b[1] - a[1]); }, 0);
w.upsertQto(sid, 'Qto_SiteBaseQuantities', [['GrossPerimeter', 'length', P], ['GrossArea', 'area', A]]);
Outline saved as text
64-bit
The site outline written to the CadCraft_Site Points property.
x,y;x,y;… each rounded to 3 decimals of the file unit
Each coordinate is rounded to 3 decimals of the file's length unit before it is written. readSite parses this text back, so the outline the app shows and edits later is the rounded one: 0.001 mm in a millimeter file, 1 mm in a meter file.
Where the building sits on the site and how far it is turned.
M = site⁻¹ × building; at = (M₁₂, M₁₃); angle = atan2(M₁, M₀) × 180 ÷ π
One inverse and one product of 4×4 world matrices in millimeters. The angle uses Math.atan2 of the first column, so it does not depend on the column being exactly unit length.
Writes the building's new position and turn relative to the site.
The building's height on the site is read from the current relative matrix and kept. The new x, y and angle are stored through the placement writer: coordinates to 6 decimals of the file unit, direction cosines to 12 decimals.
z = base + e; height = next e − e, at least 1000 mm
Each requested elevation is rounded to 0.001 mm, duplicates are removed and the list is sorted. A level is skipped when an existing one is within 1 mm of it. The last level takes the given height, else the height of the model's top level, else 3000 mm.
model-site.js
lines 152 to 173
BimCraft 0.8.1
export function addLevels({ elevations, height }) {
const want = [...new Set(elevations.map((e) => Math.round(e * 1000) / 1000))].sort((a, b) => a - b);
const guids = [];
return op((w) => {
if (!want.length) refuse('model.err.lv.none');
let levels = readLevels(w);
const base = levels.length ? levels[0].elevation : 0;
const made = new Map(), out = [];
want.forEach((e, i) => {
const z = base + e;
if (readLevels(w).some((l) => Math.abs(l.elevation - z) < 1)) return;
levels = readLevels(w);
const next = want[i + 1] !== undefined ? want[i + 1] - e : (height > 0 ? height : (levels[levels.length - 1]?.height || 3000));
if (!guids[i]) guids[i] = val(w.guid());
const r = addLevel(w, { name: levelName(levels, levels.length + 1), elevation: z, height: Math.max(1000, next), guid: guids[i] });
made.set(r.guid, r.id); out.push(r.id);
});
if (!out.length) refuse('model.err.lv.none');
regenAll(w);
return { id: out[0], ids: out, made };
});
}
Height split into equal levels
64-bit
Divides a building height into a number of equal levels.
h = T ÷ n; zᵢ = base + i × h
Each elevation is base + i × h, one multiplication and one addition, so rounding does not build up from level to level. The count is truncated with Math.floor and must be 1 to 200 with at least 1000 mm per level.
model-site.js
lines 175 to 193
BimCraft 0.8.1
export function splitLevels({ total, count }) {
const T = Number(total), n = Math.floor(Number(count));
const guids = [];
return op((w) => {
if (!(T >= 1000 * n) || !(n >= 1) || n > 200) refuse('model.err.lv.split');
const levels = readLevels(w), order = levels.map((l) => l.guid); // in their order now: moving one never reorders the rest
const base = levels.length ? levels[0].elevation : 0, h = T / n;
const made = new Map(), out = [];
for (let i = 0; i < n; i++) {
const z = base + i * h;
if (order[i]) { setLevel(w, order[i], { elevation: z, height: h, shiftAbove: false }); out.push(w.byGuid(order[i])); continue; }
if (!guids[i]) guids[i] = val(w.guid());
const r = addLevel(w, { name: levelName(readLevels(w), i + 1), elevation: z, height: h, guid: guids[i] });
made.set(r.guid, r.id); out.push(r.id);
}
regenAll(w);
return { id: out[0], ids: out, made };
});
}
The outline of a mass in world millimeters, read from its extruded profile.
p = world matrix × (profile point × unit)
Profile coordinates are multiplied by the file unit and transformed by the placement's world matrix with z = 0. A last point within 1e-6 mm of the first is dropped as the closing point.
model-site-mass.js
lines 29 to 44
BimCraft 0.8.1
/** The footprint of a proxy: its extruded profile's outline, world mm. */
function footprintOf(w, id) {
const line = w.api.GetLine(w.mid, id, true);
const reps = line.Representation?.Representations || [];
for (const r of reps) for (const it of r.Items || []) {
const curve = it.SweptArea?.OuterCurve;
if (!curve?.Points) continue;
const local = curve.Points.map((p) => (p.Coordinates || []).map((c) => Number(val(c)) * w.unitMm)).filter((q) => q.length >= 2);
const lp = line.ObjectPlacement?.value ?? line.ObjectPlacement?.expressID;
const M = lp ? scaleT(w.api.GetWorldTransformMatrix(w.mid, lp), w.unitMm) : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
let pts = local.map((q) => apply4(M, q[0], q[1], 0)).map((q) => [q[0], q[1]]);
if (pts.length > 1 && Math.hypot(pts[0][0] - pts[pts.length - 1][0], pts[0][1] - pts[pts.length - 1][1]) < 1e-6) pts = pts.slice(0, -1); // a closed polyline repeats its first point
return pts;
}
return null;
}
A point inside the footprint
64-bit
Finds a point inside a possibly concave outline, used as the seed for the floor.
x = aₓ + (y − a_y) ÷ (b_y − a_y) × (bₓ − aₓ); seed = middle of the widest span
Scan lines are tried at seven fixed fractions of the outline's height and the widest span between crossings wins. The division by b_y − a_y is safe because an edge is used only when its ends lie on opposite sides of the scan line.
model-site-mass.js
lines 71 to 82
BimCraft 0.8.1
/** A point inside a (possibly concave) polygon: the middle of the widest stretch of the scan line through its centre. */
export function insidePoint(P) {
const ys = P.map((p) => p[1]), y0 = Math.min(...ys), y1 = Math.max(...ys);
let best = null;
for (const f of [0.5, 0.4, 0.6, 0.3, 0.7, 0.2, 0.8]) {
const y = y0 + (y1 - y0) * f, xs = [];
for (let i = 0; i < P.length; i++) { const a = P[i], b = P[(i + 1) % P.length]; if ((a[1] > y) !== (b[1] > y)) xs.push(a[0] + ((y - a[1]) / (b[1] - a[1])) * (b[0] - a[0])); }
xs.sort((p, q) => p - q);
for (let k = 0; k + 1 < xs.length; k += 2) if (!best || xs[k + 1] - xs[k] > best.w) best = { w: xs[k + 1] - xs[k], p: [(xs[k] + xs[k + 1]) / 2, y] };
}
return best ? best.p : [P[0][0], P[0][1]];
}
Walls round the footprint
64-bit
Makes a wall along every edge of the mass footprint on each of its levels.
The footprint is moved into each level's plan with the inverse of the level's world matrix. Edges shorter than 1 mm get no wall. The top level's walls take the mass's floor height, the others follow the level above.
Shoelace area, summed edge lengths, rigid transforms by matrix product and inverse, equal division of a height into levels.
Number types
64-bit numbers; the saved outline text holds decimal numbers with 3 decimals of the file's length unit.
Tolerances
A level within 1 mm of an existing elevation is not added. Typed elevations are rounded to 0.001 mm. A mass edge shorter than 1 mm makes no wall. A mass footprint under 1 mm² is refused. A closing point within 1e-6 mm of the first is dropped. A site outline under 10000 mm² is refused (polyProblem in model-floors.js).
Limits
In a file whose unit is the meter the outline read back from the Points text is on a 1 mm grid, while the stored area and perimeter were computed from the unrounded points.
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.