Reads a floor face as a room (outline, holes, area), finds nearby doors and windows, and builds the matrix that sets each planned piece on its footprint.
How the precision is kept
Model points are transformed in 64-bit meters and multiplied by 1000 to plan millimeters for the shared planner. The area is a shoelace sum over those coordinates. Placement matrices divide the planned footprint edges by the piece's own box size, so a planned size that differs from the piece stretches it.
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.
The shoelace sum in the given order, on absolute coordinates. For a room far from the origin the products are large and nearly cancel, so digits are lost in proportion to the distance from the origin; the coordinates are not shifted to a local origin first.
furnish-mc.js
line 35
ModelCraft 0.8.1
const area2 = (poly) => { let s = 0; for (let i = 0; i < poly.length; i++) { const a = poly[i], b = poly[(i + 1) % poly.length]; s += a[0] * b[1] - b[0] * a[1]; } return s / 2; };
Room from a floor face
64-bit
The outline, holes, height and area of a room from a level face that looks up.
floor if n_z > 0.999; plan = model point × 1000; area (m²) = (outer − Σ holes) ÷ 10⁶
The normal is transformed and normalized, and a face tilted up to about 2.56° still counts as level. The height z is the z of the first outline point, so a slightly tilted floor is treated as flat at that height. The area is computed in mm² and divided by 10⁶.
furnish-mc.js
lines 37 to 51
ModelCraft 0.8.1
export function roomOfFace(model, path, fid) {
const ctx = model.context(path || []);
if (!ctx) return null;
const f = ctx.def.faces.get(fid);
if (!f) return null;
const m = ctx.m || ident();
const n = norm(applyDir(m, faceNormal(ctx.def, f)));
if (!(n[2] > 0.999)) return null; // a floor: level and looking up
const loops = fPts(ctx.def, f).map((l) => l.map((p) => apply(m, p)));
const z = loops[0][0][2];
const plan = (l) => { const q = l.map((p) => [p[0] * MM, p[1] * MM]); return area2(q) < 0 ? q.reverse() : q; }; // anticlockwise, as the planner wants
const poly = plan(loops[0]), holes = loops.slice(1).map(plan);
const a = (area2(poly) - holes.reduce((s, h) => s + area2(h), 0)) / 1e6;
return { poly, holes, z, area: a, m };
}
Doors and windows near the room
64-bit
Which starter doors and windows belong to the room, with their middle, width and sill height in millimeters.
middle = box center × 1000; width = max(Δx, Δy) × 1000; near if distance to outline ≤ 400
Width is the larger plan side of the opening's axis-aligned box, so for an opening placed at an angle to the axes it is the box's side, not the opening's own width. The segment distance divides by the squared edge length, replaced by 1 when it is zero. The 400 mm and 0.05 m limits are absolute.
furnish-mc.js
lines 53 to 73
ModelCraft 0.8.1
/** The starter doors and windows in the model whose middle is within 0.4 m of the room's outline (plan), at the room's height or above. */
export function openingsNear(model, room) {
const doors = [], windows = [];
const segDist = (p, a, b) => { const dx = b[0] - a[0], dy = b[1] - a[1], L = dx * dx + dy * dy || 1, t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L)); return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t); };
const near = (p) => { const P = room.poly; let d = Infinity; for (let i = 0; i < P.length; i++) d = Math.min(d, segDist(p, P[i], P[(i + 1) % P.length])); return d <= 400; };
model.walk((d, wm) => {
for (const inst of d.insts.values()) {
const def = model.defs.get(inst.def);
if (!def || !(DOORS.has(def.starter) || WINDOWS.has(def.starter))) continue;
const bb = model.bbox(def, mmul(wm, inst.m));
if (!bb || bb.max[2] < room.z - 0.05) continue;
const mid = [((bb.min[0] + bb.max[0]) / 2) * MM, ((bb.min[1] + bb.max[1]) / 2) * MM];
if (!near(mid)) continue;
const w = Math.max(bb.max[0] - bb.min[0], bb.max[1] - bb.min[1]) * MM;
if (DOORS.has(def.starter)) doors.push({ x: mid[0], y: mid[1], w });
else windows.push({ x: mid[0], y: mid[1], w, sill: Math.max(0, (bb.min[2] - room.z) * MM) });
}
return true;
});
return { doors, windows };
}
Placement matrix for a piece
64-bit
The matrix that lays a furniture piece on the planner's footprint.
x axis = (fp₁ − fp₀) ÷ 1000 ÷ W; y axis = (fp₃ − fp₀) ÷ 1000 ÷ D; origin = fp₀ ÷ 1000 − x axis·min.x − y axis·min.y; z = floor − min.z
W and D are the piece's own box width and depth and are divided by without a guard, so a piece with a zero-width box would give infinite or undefined entries. The footprint is divided by 1000 to return to meters.
furnish-mc.js
lines 85 to 93
ModelCraft 0.8.1
/** The placement matrix that lays a starter piece (built front at y = min, x from min) on a planner footprint, in the room's plan. */
function matrixFor(fp, bb, z) {
const W = bb.max[0] - bb.min[0], D = bb.max[1] - bb.min[1];
const o = [fp[0][0] / MM, fp[0][1] / MM], ex = [(fp[1][0] - fp[0][0]) / MM, (fp[1][1] - fp[0][1]) / MM], ey = [(fp[3][0] - fp[0][0]) / MM, (fp[3][1] - fp[0][1]) / MM];
const sx = [ex[0] / W, ex[1] / W], sy = [ey[0] / D, ey[1] / D];
// column-major 4×4: x axis, y axis, z axis, then the origin shifted so the piece's own min corner lands on fp[0]
return [sx[0], sx[1], 0, 0, sy[0], sy[1], 0, 0, 0, 0, 1, 0,
o[0] - sx[0] * bb.min[0] - sy[0] * bb.min[1], o[1] - sx[1] * bb.min[0] - sy[1] * bb.min[1], z - bb.min[2], 1];
}
In detail
Method
Shoelace formula for area; point to segment distance for openings; an affine matrix from three footprint corners.
Number types
64-bit numbers; matrices are arrays of 16 numbers.
Tolerances
A face counts as a floor when the z part of its unit normal is above 0.999 (within about 2.56° of level). Openings count within 400 mm of the outline and no more than 0.05 m below the floor.
Limits
The floor height is taken from the first point of the outer loop only. The piece's box width and depth are divided by without a zero check.
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.