Resources Numerical accuracy Calculation policy

Room outline and furniture placement

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.

furnish-mc.js ModelCraft 0.8.1 64-bit 4 calculations Reviewed

Tests tools/modelcraft-test/furnish08.mjs

Signed polygon area

64-bit

The signed area of a plan polygon.

area = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)

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.

Every policy that takes this approach