/** * Room-aware furniture placement. * * Produces grid-aligned, non-overlapping placements that sit inside rooms, * prefer walls for wall-pieces, and keep doorways clear. The SAME placements * feed both the live preview (vector) and the locked Tiles placed in the scene, * so they always match. Pure / Node-testable. Output px uses the given gridSize. * * Placement shape: * { kind, subtype, rotation(0|90), cx, cy (centre px), wpx, hpx (native px) } */ import { Rng } from "./rng.mjs"; import { PIECES, piecesFor } from "./furniture.mjs"; /** Cells adjacent to a door — kept clear so furniture never blocks a doorway. */ function buildKeepClear(walls) { const set = new Set(); for (const s of walls.segments ?? []) { if (s.kind !== "door") continue; if (s.orient === "v") { set.add(`${s.x1 - 1},${s.y1}`); set.add(`${s.x1},${s.y1}`); } else { set.add(`${s.x1},${s.y1 - 1}`); set.add(`${s.x1},${s.y1}`); } } return set; } export function computePlacements(layout, walls, gs, subtype, seed) { const { region, cols, rows, rooms } = layout; const rng = new Rng(`${seed}::furnlayout`); const occupied = Array.from({ length: rows }, () => new Array(cols).fill(false)); const keepClear = buildKeepClear(walls); const placements = []; const inRoom = (id, x, y) => x >= 0 && y >= 0 && x < cols && y < rows && region[y][x] === id; for (const room of rooms) { if (!room.label || room.type === "stairs" || room.type === "elevator") continue; const kinds = piecesFor(room.label); for (const kind of kinds) { const def = PIECES[kind]; if (!def) continue; const [pw, ph] = def.footprint; const orients = pw === ph ? [[pw, ph, 0]] : [[pw, ph, 0], [ph, pw, 90]]; const cands = []; for (const [ow, oh, rot] of orients) { for (let ty = room.y; ty <= room.y + room.h - oh; ty += 1) { for (let tx = room.x; tx <= room.x + room.w - ow; tx += 1) { let ok = true; for (let dy = 0; dy < oh && ok; dy += 1) { for (let dx = 0; dx < ow && ok; dx += 1) { const cx = tx + dx, cy = ty + dy; if (!inRoom(room.id, cx, cy) || occupied[cy][cx] || keepClear.has(`${cx},${cy}`)) ok = false; } } if (!ok) continue; let wallScore = 0; for (let dx = 0; dx < ow; dx += 1) { if (!inRoom(room.id, tx + dx, ty - 1)) wallScore += 1; if (!inRoom(room.id, tx + dx, ty + oh)) wallScore += 1; } for (let dy = 0; dy < oh; dy += 1) { if (!inRoom(room.id, tx - 1, ty + dy)) wallScore += 1; if (!inRoom(room.id, tx + ow, ty + dy)) wallScore += 1; } cands.push({ tx, ty, ow, oh, rot, wallScore }); } } } if (!cands.length) continue; // Wall pieces want maximum wall contact; free pieces prefer open centre. const scores = cands.map((c) => c.wallScore); const target = def.wall ? Math.max(...scores) : Math.min(...scores); const pool = cands.filter((c) => c.wallScore === target); const pick = pool[Math.floor(rng.float() * pool.length)]; for (let dy = 0; dy < pick.oh; dy += 1) { for (let dx = 0; dx < pick.ow; dx += 1) occupied[pick.ty + dy][pick.tx + dx] = true; } placements.push({ kind, subtype, rotation: pick.rot, wpx: pw * gs, hpx: ph * gs, cx: (pick.tx + pick.ow / 2) * gs, cy: (pick.ty + pick.oh / 2) * gs }); } } return placements; }