Places a room's furniture by rule: against the walls or in the middle, clear of doors, windows and each other.
How the precision is kept
Positions are tried on a grid: every 100 mm along a wall, every 150 mm from the room's middle. Fit and overlap are tested in 64-bit on the trial rectangle, with 1 mm and 2 mm of slack. The position kept is then rounded to the whole millimeter and the rotation to 0.001°, so what is placed is the tested rectangle to within half a millimeter.
Calculations under this policy
9 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 four plan corners of a piece, or of the zone kept clear round it.
corner = (x + u·cos r − v·sin r, y + u·sin r + v·cos r), u ∈ ±(w ÷ 2 + side), v ∈ [−front, d]
The rotation is degrees × π ÷ 180 with Math.PI, then one sine and one cosine for all four corners. For a quarter turn the cosine is about 6e-17 instead of 0.
furnish.js
lines 58 to 64
BimCraft 0.8.1
/** The four corners of a rectangle centred at (x, y), `w` across and `d` deep, turned `rot`; extra margins front/back/sides widen the box (used as the reserved zone). */
function corners(x, y, w, d, rot, { front = 0, side = 0 } = {}) {
const c = Math.cos(rad(rot)), s = Math.sin(rad(rot));
// local frame: u across (x), v from front (0) to back (d): the box spans v in [-front, d]
const u0 = -w / 2 - side, u1 = w / 2 + side, v0 = -front, v1 = d;
return [[u0, v0], [u1, v0], [u1, v1], [u0, v1]].map(([u, v]) => [x + u * c - v * s, y + u * s + v * c]);
}
Do two pieces overlap
64-bit
Whether two rectangles overlap by more than a hair.
separate when, on any edge normal of either, a_max ≤ b_min + 1 mm or b_max ≤ a_min + 1 mm
The separating-axis test on the four edge normals. The slack is 1 mm, absolute: two pieces that overlap by 1 mm or less count as not overlapping. Each normal is divided by its edge length, which is not guarded against a zero-size rectangle.
furnish.js
lines 67 to 71
BimCraft 0.8.1
/** Do two convex rectangles overlap by more than a hair (separating axis)? */
function overlaps(a, b, eps = 1) {
for (const ax of [...axes(a), ...axes(b)]) { const [a0, a1] = range(a, ax), [b0, b1] = range(b, ax); if (a1 <= b0 + eps || b1 <= a0 + eps) return false; }
return true;
}
Is a point in the room
64-bit
Whether a point lies inside the room's outline.
even-odd count of edges crossed by a ray toward +x
A plain ray-crossing test with no tolerance of its own. An edge is used only when its ends are on opposite sides of the ray, so the division by the difference in y is never by zero.
furnish.js
lines 72 to 79
BimCraft 0.8.1
function inside(poly, p) {
let c = false;
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
const a = poly[i], b = poly[j];
if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]) c = !c;
}
return c;
}
Does a piece fit in the room
64-bit
Whether a rectangle lies inside the room and outside its holes.
Eight points are tested: the corners and the middle of each side, each pulled 2 mm toward the rectangle's middle first, because a piece against a wall has points exactly on the outline. A part of the room's outline that cuts into the rectangle between two test points is not seen.
furnish.js
lines 81 to 90
BimCraft 0.8.1
/** Does the rectangle sit inside the room (corners and edge midpoints in, none in a hole)? */
function fits(room, rect) {
// a piece stands against the wall: its back is ON the boundary, where "inside" is a coin toss, so the points are pulled 2 mm toward the middle
const mx = rect.reduce((s, q) => s + q[0], 0) / 4, my = rect.reduce((s, q) => s + q[1], 0) / 4;
const pull = (q) => { const dx = mx - q[0], dy = my - q[1], l = Math.hypot(dx, dy) || 1; return [q[0] + (dx / l) * 2, q[1] + (dy / l) * 2]; };
const pts = [...rect, ...rect.map((q, i) => [(q[0] + rect[(i + 1) % 4][0]) / 2, (q[1] + rect[(i + 1) % 4][1]) / 2])].map(pull);
if (!pts.every((p) => inside(room.poly, p))) return false;
for (const h of room.holes || []) if (pts.some((p) => inside(h, p))) return false;
return true;
}
Which way the outline runs
64-bit
The signed area of the room's outline, used to turn it counter-clockwise.
area = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)
The shoelace sum on the coordinates as given. Only its sign is used.
furnish.js
line 91
BimCraft 0.8.1
const area = (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; };
Zone kept clear at a door
64-bit
The rectangle round a door where nothing is placed.
width = door width + 300 mm; depth = one door width on each side of the wall
The wall is the first one within 400 mm of the door's point; its angle comes from atan2 of the wall's unit direction. The margins are fixed whole numbers of millimeters.
furnish.js
lines 110 to 115
BimCraft 0.8.1
const doorRects = doors.map((d) => {
const e = edges.find((x) => distToSeg([d.x, d.y], x.a, x.b) < 400) || edges[0];
const ang = (Math.atan2(e.dir[1], e.dir[0]) * 180) / Math.PI;
// box centred on the door, aligned with the wall: v from -(w) to +(w)
return corners(d.x, d.y, d.w + 300, d.w, ang, { front: d.w });
});
A piece against a wall
64-bit
The first free place for a piece along a wall, with its back on the wall.
t = w ÷ 2 + side, then every 100 mm; (x, y) = wall start + direction × t + inward normal × depth; kept: round(x), round(y), rotation to 0.001°
The trial position is exact to 64-bit and is what the fit tests use. The position kept is rounded to the whole millimeter and the rotation to 0.001°, so on a wall that is not along an axis the placed back can be up to about half a millimeter off the wall.
furnish.js
lines 150 to 159
BimCraft 0.8.1
const rot = (Math.atan2(e.n[0], -e.n[1]) * 180) / Math.PI;
if (e.l < p.w + 2 * (p.side || 0) - 1) continue;
// slide along the wall from one end; the first free spot wins (deterministic)
const from = p.w / 2 + (p.side || 0), to = e.l - p.w / 2 - (p.side || 0);
for (let t = from; t <= to + 0.5; t += step) {
const wx = e.a[0] + e.dir[0] * t, wy = e.a[1] + e.dir[1] * t;
const x = wx + e.n[0] * p.d, y = wy + e.n[1] * p.d; // origin = the front of the piece; the back stands on the wall
const rect = corners(x, y, p.w, p.d, rot);
const zr = corners(x, y, p.w, p.d, rot, { front: p.clear, side: p.side || 0 });
if (free(rect, p, zr)) { done = { family, x: Math.round(x), y: Math.round(y), rot: Math.round(rot * 1000) / 1000, w: p.w, d: p.d, rect, zone: zr, why: 'against the wall ' + Math.round(e.l) + ' mm long' }; break; }
A piece in the middle of the room
64-bit
The first free place for a free-standing piece, starting at the middle of the room.
middle of the outline's box, then offsets of k × 150 mm (k = 1…20) along ±x and ±y, at the longest wall's angle and at 90° to it
The middle is that of the bounding box, not the centroid. Offsets are whole multiples of 150 mm. The position kept is rounded to the whole millimeter.
furnish.js
lines 166 to 176
BimCraft 0.8.1
const mid = [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
const along = edges[0] ? (Math.atan2(edges[0].dir[1], edges[0].dir[0]) * 180) / Math.PI : 0;
const spots = [[0, 0]];
for (let k = 1; k <= 20; k++) for (const [ux, uy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) spots.push([ux * k * 150, uy * k * 150]);
for (const rot of [along, along + 90]) {
for (const [dx, dy] of spots) {
const cx = mid[0] + dx, cy = mid[1] + dy;
const ox = cx + Math.sin(rad(rot)) * (p.d / 2), oy = cy - Math.cos(rad(rot)) * (p.d / 2); // the front-centre of the box
const rect = corners(ox, oy, p.w, p.d, rot);
const zr = corners(ox, oy, p.w, p.d, rot, { front: p.clear / 2, side: p.clear / 2 });
if (free(rect, p, zr)) { done = { family, x: Math.round(cx), y: Math.round(cy), rot: Math.round(rot * 1000) / 1000, w: p.w, d: p.d, rect, zone: zr, why: 'in the middle of the room' }; break; }
Footprint of a placed piece
64-bit
The four plan corners of a piece from its stored position.
Built from the stored, rounded position and rotation, so it can differ from the rectangle that was tested by up to half a millimeter. A free-standing piece is stored by its center and shifted half its depth to find its front.
furnish.js
lines 93 to 97
BimCraft 0.8.1
/** A placement's four plan corners (the box of the piece itself, no clearance). */
export function footprint(p) {
const free = PIECES[p.family]?.at === 'free';
return free ? corners(p.x + Math.sin(rad(p.rot)) * (p.d / 2), p.y - Math.cos(rad(p.rot)) * (p.d / 2), p.w, p.d, p.rot) : corners(p.x, p.y, p.w, p.d, p.rot);
}
In detail
Method
Rectangles are tested for overlap by the separating-axis test and for being inside the room by an even-odd ray test on their corners and edge middles. Walls are tried longest first and the first free spot wins, so the same room gives the same layout.
Number types
64-bit numbers in millimeters and degrees.
Tolerances
Two rectangles overlap only when they overlap by more than 1 mm. Test points are pulled 2 mm toward the rectangle's middle before the inside test. Walls shorter than 300 mm are not used. A door or window belongs to a wall within 400 mm.
Limits
A rule-based layout on a search grid (the header says so): a piece that fits only between grid positions is not found. Sizes are the starter families' fixed footprints.
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.