Files
citygen/src/gen/furnish.ts
T

477 lines
23 KiB
TypeScript

// Расстановка мебели по правилам для каждого типа комнаты.
// Работает в сетке здания: предмет занимает клетки, «спинкой» встаёт к стене,
// не перекрывает двери (зона открывания) и высокими предметами не закрывает окна.
import type { Building, Prop, Vec } from '../types';
import type { DoorL, FloorG, Grid, WindowL } from './interior';
import { C } from './interior';
import * as K from './catalog';
import type { Def } from './catalog';
import { Rng } from '../util/rng';
import { distToSegment } from '../util/geom';
import { CAR_COLORS } from './props';
export interface PropL {
prop: Prop;
pos: Vec;
rot: number;
room: number;
}
interface Ctx {
g: Grid;
F: FloorG;
doors: DoorL[];
windows: WindowL[];
building: Building;
rng: Rng;
}
type Dir = 'N' | 'E' | 'S' | 'W';
const ROT: Record<Dir, number> = { N: 0, S: Math.PI, W: -Math.PI / 2, E: Math.PI / 2 };
export function furnishFloor(ctx: Ctx): PropL[] {
const { g, F, rng } = ctx;
const n = g.gw * g.gh;
const occ = new Uint8Array(n); // 0 свободно, 1 занято, 2 держать свободным
const win = new Uint8Array(n);
const out: PropL[] = [];
const cellsOf = new Map<number, number[]>();
for (let i = 0; i < n; i++) {
const r = F.labels[i];
if (r < 0 || !g.mask[i]) continue;
if (!cellsOf.has(r)) cellsOf.set(r, []);
cellsOf.get(r)!.push(i);
}
const cc = (i: number): Vec => [((i % g.gw) + 0.5) * C, (((i / g.gw) | 0) + 0.5) * C];
// зоны дверей и окон
const around = (a: Vec, b: Vec, r: number, fn: (i: number, p: Vec) => void) => {
const x0 = Math.max(0, Math.floor((Math.min(a[0], b[0]) - r) / C)), x1 = Math.min(g.gw - 1, Math.floor((Math.max(a[0], b[0]) + r) / C));
const y0 = Math.max(0, Math.floor((Math.min(a[1], b[1]) - r) / C)), y1 = Math.min(g.gh - 1, Math.floor((Math.max(a[1], b[1]) + r) / C));
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) { const i = y * g.gw + x; if (F.labels[i] >= 0) fn(i, cc(i)); }
};
for (const d of ctx.doors) {
const m: Vec = [(d.a[0] + d.b[0]) / 2, (d.a[1] + d.b[1]) / 2];
const half = Math.hypot(d.b[0] - d.a[0], d.b[1] - d.a[1]) / 2;
around(d.a, d.b, half + 0.7, (i, p) => {
if (distToSegment(p, d.a, d.b) < 0.95 || Math.hypot(p[0] - m[0], p[1] - m[1]) < half + 0.6) occ[i] = 2;
});
}
for (const w of ctx.windows) around(w.a, w.b, 0.6, (i, p) => { if (distToSegment(p, w.a, w.b) < 0.6) win[i] = 1; });
// лестницы — вне мебели
for (const s of F.stairs) for (let y = s.block.y0; y < s.block.y1; y++) for (let x = s.block.x0; x < s.block.x1; x++) occ[y * g.gw + x] = 1;
const lab = (x: number, y: number) => (x < 0 || y < 0 || x >= g.gw || y >= g.gh ? -1 : F.labels[y * g.gw + x]);
const free = (x: number, y: number, room: number, tall?: boolean) => {
if (x < 0 || y < 0 || x >= g.gw || y >= g.gh) return false;
const i = y * g.gw + x;
return F.labels[i] === room && g.mask[i] === 1 && occ[i] === 0 && !(tall && win[i]);
};
const emit = (def: Def, pos: Vec, rot: number, room: number, cells: number[], frontCells: number[], color?: string) => {
for (const i of cells) occ[i] = 1;
for (const i of frontCells) if (occ[i] === 0) occ[i] = 2;
out.push({
pos, rot, room,
prop: {
id: '', kind: def.kind, pos: [0, 0], rot: 0, w: def.w, d: def.d, shapes: def.shapes,
blocksMove: def.blocksMove ?? true, blocksSight: !!def.tall, tags: def.tags, color,
},
});
return true;
};
/** Поставить предмет спинкой к стене. */
const atWall = (room: number, def: Def, opt: { corner?: boolean; window?: boolean; tries?: number } = {}): boolean => {
const cells = cellsOf.get(room);
if (!cells) return false;
const A = Math.max(1, Math.ceil(def.w / C - 1e-6)), D = Math.max(1, Math.ceil(def.d / C - 1e-6));
const FD = Math.ceil((def.front ?? 0) / C - 1e-6);
let best: { s: number; dir: Dir; x0: number; y0: number; x1: number; y1: number } | null = null;
for (const i of cells) {
const cx = i % g.gw, cy = (i / g.gw) | 0;
for (const dir of ['N', 'E', 'S', 'W'] as Dir[]) {
let x0: number, y0: number, x1: number, y1: number;
if (dir === 'N') { x0 = cx; y0 = cy; x1 = cx + A; y1 = cy + D; }
else if (dir === 'S') { x0 = cx; y0 = cy - D + 1; x1 = cx + A; y1 = cy + 1; }
else if (dir === 'W') { x0 = cx; y0 = cy; x1 = cx + D; y1 = cy + A; }
else { x0 = cx - D + 1; y0 = cy; x1 = cx + 1; y1 = cy + A; }
let ok = true;
for (let y = y0; y < y1 && ok; y++) for (let x = x0; x < x1 && ok; x++) if (!free(x, y, room, def.tall)) ok = false;
if (!ok) continue;
// сзади должна быть стена по всей длине
let wallOk = true;
if (dir === 'N') for (let x = x0; x < x1; x++) wallOk &&= lab(x, y0 - 1) !== room;
if (dir === 'S') for (let x = x0; x < x1; x++) wallOk &&= lab(x, y1) !== room;
if (dir === 'W') for (let y = y0; y < y1; y++) wallOk &&= lab(x0 - 1, y) !== room;
if (dir === 'E') for (let y = y0; y < y1; y++) wallOk &&= lab(x1, y) !== room;
if (!wallOk) continue;
// перед предметом — проход
let frontOk = true;
for (let k = 1; k <= FD && frontOk; k++) {
if (dir === 'N') for (let x = x0; x < x1; x++) frontOk &&= lab(x, y1 - 1 + k) === room && occ[(y1 - 1 + k) * g.gw + x] !== 1;
if (dir === 'S') for (let x = x0; x < x1; x++) frontOk &&= lab(x, y0 - k) === room && occ[(y0 - k) * g.gw + x] !== 1;
if (dir === 'W') for (let y = y0; y < y1; y++) frontOk &&= lab(x1 - 1 + k, y) === room && occ[y * g.gw + x1 - 1 + k] !== 1;
if (dir === 'E') for (let y = y0; y < y1; y++) frontOk &&= lab(x0 - k, y) === room && occ[y * g.gw + x0 - k] !== 1;
}
if (!frontOk) continue;
let s = rng.next();
if (opt.corner) {
const sideA = dir === 'N' || dir === 'S' ? lab(x0 - 1, dir === 'N' ? y0 : y1 - 1) !== room : lab(dir === 'W' ? x0 : x1 - 1, y0 - 1) !== room;
const sideB = dir === 'N' || dir === 'S' ? lab(x1, dir === 'N' ? y0 : y1 - 1) !== room : lab(dir === 'W' ? x0 : x1 - 1, y1) !== room;
s += (sideA ? 2 : 0) + (sideB ? 2 : 0);
}
if (opt.window) {
let wn = 0;
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) wn += win[y * g.gw + x];
s += wn ? 3 : 0;
}
if (!best || s > best.s) best = { s, dir, x0, y0, x1, y1 };
}
}
if (!best) return false;
const { dir, x0, y0, x1, y1 } = best;
const inset = 0.07 + def.d / 2;
let pos: Vec;
if (dir === 'N') pos = [((x0 + x1) / 2) * C, y0 * C + inset];
else if (dir === 'S') pos = [((x0 + x1) / 2) * C, y1 * C - inset];
else if (dir === 'W') pos = [x0 * C + inset, ((y0 + y1) / 2) * C];
else pos = [x1 * C - inset, ((y0 + y1) / 2) * C];
const used: number[] = [];
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) used.push(y * g.gw + x);
const front: number[] = [];
for (let k = 1; k <= FD; k++) {
if (dir === 'N') for (let x = x0; x < x1; x++) front.push((y1 - 1 + k) * g.gw + x);
if (dir === 'S') for (let x = x0; x < x1; x++) front.push((y0 - k) * g.gw + x);
if (dir === 'W') for (let y = y0; y < y1; y++) front.push(y * g.gw + x1 - 1 + k);
if (dir === 'E') for (let y = y0; y < y1; y++) front.push(y * g.gw + x0 - k);
}
return emit(def, pos, ROT[dir], room, used, front.filter((i) => i >= 0 && i < n));
};
/** Поставить предмет отдельно (стол, кластер), с отступом margin клеток, ближе к центру комнаты. */
const inMiddle = (room: number, def: Def, margin = 1, center?: Vec, color?: string): boolean => {
const cells = cellsOf.get(room);
if (!cells) return false;
const c = center ?? roomPole(room);
let best: { s: number; x0: number; y0: number; x1: number; y1: number; rot: boolean } | null = null;
for (const rotd of [false, true]) {
const A = Math.max(1, Math.ceil((rotd ? def.d : def.w) / C - 1e-6)), D = Math.max(1, Math.ceil((rotd ? def.w : def.d) / C - 1e-6));
for (const i of cells) {
const x0 = i % g.gw, y0 = (i / g.gw) | 0;
let ok = true;
for (let y = y0 - margin; y < y0 + D + margin && ok; y++)
for (let x = x0 - margin; x < x0 + A + margin && ok; x++) {
const inner = x >= x0 && x < x0 + A && y >= y0 && y < y0 + D;
if (inner) ok = free(x, y, room);
else ok = lab(x, y) === room && occ[y * g.gw + x] !== 1;
}
if (!ok) continue;
const s = -Math.hypot((x0 + A / 2) * C - c[0], (y0 + D / 2) * C - c[1]) + rng.next() * 0.3;
if (!best || s > best.s) best = { s, x0, y0, x1: x0 + A, y1: y0 + D, rot: rotd };
}
}
if (!best) return false;
const used: number[] = [];
for (let y = best.y0; y < best.y1; y++) for (let x = best.x0; x < best.x1; x++) used.push(y * g.gw + x);
return emit(def, [((best.x0 + best.x1) / 2) * C, ((best.y0 + best.y1) / 2) * C], best.rot ? Math.PI / 2 : rng.chance(0.5) ? 0 : Math.PI, room, used, [], color);
};
/** Сетка одинаковых предметов (столики, парты, станки). */
const lattice = (room: number, make: () => Def, gapX: number, gapY: number, margin: number, alongLong = true, max = 400, faceRot?: number): number => {
const cells = cellsOf.get(room);
if (!cells) return 0;
const bb = roomBox(room);
const longX = bb[2] - bb[0] >= bb[3] - bb[1];
const def0 = make();
const rotd = alongLong ? !longX : longX;
const A = Math.ceil((rotd ? def0.d : def0.w) / C - 1e-6), D = Math.ceil((rotd ? def0.w : def0.d) / C - 1e-6);
const sx = A + Math.round(gapX / C), sy = D + Math.round(gapY / C);
let placed = 0;
for (let y = bb[1] + margin; y + D <= bb[3] - margin && placed < max; y += sy)
for (let x = bb[0] + margin; x + A <= bb[2] - margin && placed < max; x += sx) {
let ok = true;
for (let yy = y - 1; yy < y + D + 1 && ok; yy++)
for (let xx = x - 1; xx < x + A + 1 && ok; xx++) {
const inner = xx >= x && xx < x + A && yy >= y && yy < y + D;
ok = inner ? free(xx, yy, room) : lab(xx, yy) === room && occ[yy * g.gw + xx] !== 1;
}
if (!ok) continue;
const used: number[] = [];
for (let yy = y; yy < y + D; yy++) for (let xx = x; xx < x + A; xx++) used.push(yy * g.gw + xx);
emit(make(), [(x + A / 2) * C, (y + D / 2) * C], faceRot ?? (rotd ? Math.PI / 2 : 0), room, used, []);
placed++;
}
return placed;
};
/** Ряды стеллажей вдоль длинной стороны комнаты с проходами. */
const rows = (room: number, make: (L: number) => Def, depth: number, aisle: number, margin: number, maxLen = 12) => {
const bb = roomBox(room);
const longX = bb[2] - bb[0] >= bb[3] - bb[1];
const D = Math.ceil(depth / C), step = D + Math.round(aisle / C);
const [a0, a1, b0, b1] = longX ? [bb[1], bb[3], bb[0], bb[2]] : [bb[0], bb[2], bb[1], bb[3]];
for (let a = a0 + margin; a + D <= a1 - margin; a += step) {
let runStart = -1;
const flush = (end: number) => {
let s = runStart;
while (end - s >= 4) {
const len = Math.min(end - s, Math.round(maxLen / C));
const L = len * C - 0.1;
const used: number[] = [];
for (let k = s; k < s + len; k++) for (let dd = a; dd < a + D; dd++) used.push(longX ? dd * g.gw + k : k * g.gw + dd);
const pos: Vec = longX ? [((s + s + len) / 2) * C, (a + D / 2) * C] : [(a + D / 2) * C, ((s + s + len) / 2) * C];
emit(make(L), pos, longX ? 0 : Math.PI / 2, room, used, []);
s += len + 3;
}
runStart = -1;
};
for (let k = b0 + margin; k <= b1 - margin; k++) {
let ok = k < b1 - margin;
for (let dd = a - 1; dd < a + D + 1 && ok; dd++) {
const x = longX ? k : dd, y = longX ? dd : k;
const inner = dd >= a && dd < a + D;
ok = inner ? free(x, y, room, true) : lab(x, y) === room && occ[y * g.gw + x] !== 1;
}
if (ok && runStart < 0) runStart = k;
if (!ok && runStart >= 0) flush(k);
}
}
};
const roomBox = (room: number): [number, number, number, number] => {
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const i of cellsOf.get(room) ?? []) {
const x = i % g.gw, y = (i / g.gw) | 0;
x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x + 1); y1 = Math.max(y1, y + 1);
}
return [x0, y0, x1, y1];
};
const roomPole = (room: number): Vec => {
const bb = roomBox(room);
return [((bb[0] + bb[2]) / 2) * C, ((bb[1] + bb[3]) / 2) * C];
};
const areaOf = (room: number) => (cellsOf.get(room)?.length ?? 0) * C * C;
const shortSide = (room: number) => { const bb = roomBox(room); return Math.min(bb[2] - bb[0], bb[3] - bb[1]) * C; };
const use = ctx.building.use ?? '';
for (const [room, cells] of cellsOf) {
const R = F.rooms[room];
if (R.dead || !cells.length) continue;
const a = areaOf(room);
switch (R.kind) {
case 'bedroom': {
if (a >= 10 && shortSide(room) >= 2.8) {
if (!atWall(room, K.bedWithNightstands())) atWall(room, K.bed(true));
} else if (!atWall(room, K.bed(a >= 9), { corner: true })) atWall(room, K.bed(false), { corner: true });
atWall(room, K.wardrobe(a > 12 ? 1.8 : 1.2), { corner: true });
if (a > 11) atWall(room, K.desk(1.2), { window: true });
if (a > 14 && rng.chance(0.5)) atWall(room, K.armchair(), { corner: true });
if (rng.chance(0.3)) atWall(room, K.plant(), { corner: true });
break;
}
case 'living': {
const studio = a < 30 && !F.rooms.some((r) => r.unit === R.unit && r.kind === 'bedroom' && !r.dead);
if (studio) {
atWall(room, K.bed(true), { corner: true });
if (!F.rooms.some((r) => r.unit === R.unit && r.kind === 'kitchen' && !r.dead)) atWall(room, K.kitchenLine(1.8));
}
if (!atWall(room, K.sofaSet())) atWall(room, K.sofa(1.8));
atWall(room, K.tvStand());
if (a > 16) atWall(room, K.armchair(), { corner: true });
atWall(room, K.bookshelf(rng.pick([0.8, 1.0, 1.6])));
if (a > 20 && !studio) inMiddle(room, K.diningTable(a > 26 ? 6 : 4), 1);
atWall(room, K.plant(), { corner: true });
if (rng.chance(0.5)) atWall(room, K.plant(), { corner: true });
break;
}
case 'kitchen': {
let placed = false;
for (const L of [3.6, 3.0, 2.4, 1.8, 1.2]) if (atWall(room, K.kitchenLine(L))) { placed = true; break; }
if (!placed) inMiddle(room, K.kitchenLine(1.2), 0);
atWall(room, K.fridge(), { corner: true });
if (a >= 8) inMiddle(room, K.diningTable(4), 1);
break;
}
case 'bath': {
if (a >= 4.2 && shortSide(room) >= 1.7) { if (!atWall(room, K.bathtub(), { corner: true })) atWall(room, K.shower(), { corner: true }); }
else atWall(room, K.shower(), { corner: true });
atWall(room, K.toilet());
atWall(room, K.basin());
if (a > 6) atWall(room, K.washer());
break;
}
case 'wc': {
const n = ctx.building.kind === 'house' || a < 5 ? 1 : Math.min(4, Math.floor(a / 2.5));
for (let k = 0; k < n; k++) atWall(room, K.toilet(), { corner: k === 0 });
atWall(room, K.basin());
if (n > 1) atWall(room, K.basin());
break;
}
case 'hall': {
atWall(room, K.coatRack());
if (a > 5) atWall(room, K.shoeCabinet());
if (a > 9) atWall(room, K.plant(), { corner: true });
if (ctx.building.kind === 'civic' && a > 20) { atWall(room, K.counter(2.4)); atWall(room, K.sofa(2.0)); }
break;
}
case 'landing':
if (F.level === 0) atWall(room, K.mailboxes());
break;
case 'lobby': {
atWall(room, K.mailboxes());
atWall(room, K.plant(), { corner: true });
break;
}
case 'corridor': {
if (rng.chance(0.4)) atWall(room, K.plant(), { corner: true });
break;
}
case 'utility': {
atWall(room, K.washer());
atWall(room, K.boiler(), { corner: true });
atWall(room, K.shelf(1.0));
break;
}
case 'storage':
case 'backroom': {
for (let k = 0; k < 8; k++) if (!atWall(room, K.shelf(rng.pick([1.0, 1.2, 1.8])))) break;
for (let k = 0; k < (a > 15 ? 3 : 1); k++) inMiddle(room, K.crate(), 1);
break;
}
case 'sales': {
if (['clothes', 'shoes', 'boutique', 'fashion'].includes(use)) {
lattice(room, K.clothesRack, 1.0, 1.2, 2);
atWall(room, K.counter(1.6));
for (let k = 0; k < 6; k++) if (!atWall(room, K.wallShelf(2.0))) break;
} else if (['bakery', 'cafe', 'butcher', 'pastry', 'deli', 'confectionery'].includes(use) || a < 25) {
if (!atWall(room, K.counter(Math.min(3.0, Math.max(1.2, shortSide(room) - 1))))) inMiddle(room, K.counter(1.2), 1);
for (let k = 0; k < 3; k++) if (!atWall(room, K.wallShelf(1.6))) break;
} else {
// супермаркет / обычный магазин: кассы у входа, стеллажи рядами, холодильники у стен
const ent = ctx.doors.find((d) => d.r2 === -1 && d.r1 === room);
const nCheck = a > 400 ? 4 : a > 120 ? 2 : 1;
for (let k = 0; k < nCheck; k++) inMiddle(room, K.checkout(), 1, ent ? [(ent.a[0] + ent.b[0]) / 2, (ent.a[1] + ent.b[1]) / 2] : undefined);
if (use === 'supermarket' || a > 150) for (let k = 0; k < 4; k++) if (!atWall(room, K.fridgeDisplay(2.4))) break;
for (let k = 0; k < 10; k++) if (!atWall(room, K.wallShelf(rng.pick([1.8, 2.4])))) break;
rows(room, K.gondola, 1.0, 1.6, 2, 10);
}
if (a > 20) atWall(room, K.plant(), { corner: true });
break;
}
case 'dining': {
atWall(room, K.counter(Math.min(3.0, Math.max(1.4, shortSide(room) * 0.6))));
if (lattice(room, a > 30 && rng.chance(0.5) ? K.cafeTable : () => K.diningTable(4), 0.8, 0.9, 1) === 0) inMiddle(room, K.cafeTable(), 1);
atWall(room, K.plant(), { corner: true });
break;
}
case 'prokitchen': {
for (const L of [2.4, 1.8, 1.2]) if (atWall(room, K.prepTable(L))) break;
atWall(room, K.proStove());
atWall(room, K.fridge(), { corner: true });
atWall(room, K.fridge(), { corner: true });
if (a > 14) inMiddle(room, K.prepTable(1.6), 1);
atWall(room, K.shelf(1.2));
break;
}
case 'office': {
atWall(room, K.desk(1.4), { window: true });
if (a > 12) atWall(room, K.desk(1.4), { window: true });
atWall(room, K.filing(), { corner: true });
atWall(room, K.bookshelf(1.0));
if (rng.chance(0.6)) atWall(room, K.plant(), { corner: true });
break;
}
case 'openoffice': {
lattice(room, K.officeCluster, 1.4, 1.2, 2);
for (let k = 0; k < 3; k++) atWall(room, K.filing(), { corner: true });
atWall(room, K.plant(), { corner: true });
atWall(room, K.plant(), { corner: true });
break;
}
case 'meeting': {
const bb = roomBox(room);
const L = Math.max(1.6, Math.min(6, (Math.max(bb[2] - bb[0], bb[3] - bb[1]) * C) - 2.2));
if (!inMiddle(room, K.meetingTable(L), 1)) inMiddle(room, K.meetingTable(1.6), 1);
atWall(room, K.board(2.0));
break;
}
case 'staff': {
inMiddle(room, K.diningTable(4), 1);
atWall(room, K.kitchenLine(1.8));
atWall(room, K.sofa(1.8));
break;
}
case 'classroom': {
atWall(room, K.board(3.0));
inMiddle(room, K.teacherDesk(), 1);
lattice(room, K.studentDesk, 0.7, 0.5, 2);
atWall(room, K.bookshelf(1.6));
break;
}
case 'ward': {
for (let k = 0; k < Math.max(1, Math.min(4, Math.floor(a / 8))); k++) if (!atWall(room, K.hospitalBed())) break;
atWall(room, K.nightstand());
atWall(room, K.armchair(), { corner: true });
break;
}
case 'guestroom': {
if (!atWall(room, K.bedWithNightstands())) if (!atWall(room, K.bed(true))) atWall(room, K.bed(false));
atWall(room, K.desk(1.2), { window: true });
atWall(room, K.wardrobe(1.0), { corner: true });
atWall(room, K.armchair(), { corner: true });
break;
}
case 'workshop': {
for (let k = 0; k < 3; k++) atWall(room, K.workbench(rng.pick([1.6, 2.0])));
for (let k = 0; k < 4; k++) atWall(room, K.shelf(1.2));
inMiddle(room, K.machine(), 1);
break;
}
case 'hall_ind': {
const kindB = ctx.building.kind;
if (kindB === 'warehouse' && !['sports_hall', 'sports_centre'].includes(use)) {
rows(room, K.palletRack, 1.1, 3.2, 3, 16);
lattice(room, K.pallet, 2.0, 2.0, 2, true, 6);
} else if (kindB === 'farm') {
lattice(room, K.pallet, 2.5, 2.5, 2, true, 8);
for (let k = 0; k < 4; k++) atWall(room, K.shelf(1.8));
} else if (['sports_hall', 'sports_centre'].includes(use)) {
atWall(room, K.bookshelf(2.0));
} else {
lattice(room, K.machine, 2.4, 2.6, 3, true, 40);
for (let k = 0; k < 4; k++) atWall(room, K.workbench(2.0));
for (let k = 0; k < 6; k++) atWall(room, K.shelf(1.8));
}
break;
}
case 'nave': {
// алтарь у дальней от входа стены, скамьи рядами поперёк
const ent = ctx.doors.find((d) => d.r2 === -1);
const bb = roomBox(room);
const longX = bb[2] - bb[0] >= bb[3] - bb[1];
const cMid: Vec = [((bb[0] + bb[2]) / 2) * C, ((bb[1] + bb[3]) / 2) * C];
let far: Vec = cMid;
if (ent) {
const em: Vec = [(ent.a[0] + ent.b[0]) / 2, (ent.a[1] + ent.b[1]) / 2];
far = longX ? [em[0] < cMid[0] ? (bb[2] - 3) * C : (bb[0] + 3) * C, cMid[1]] : [cMid[0], em[1] < cMid[1] ? (bb[3] - 3) * C : (bb[1] + 3) * C];
}
inMiddle(room, K.altar(), 1, far);
const width = (longX ? bb[3] - bb[1] : bb[2] - bb[0]) * C;
const L = Math.max(1.5, Math.min(5, (width - 2.4) / 2));
lattice(room, () => K.pew(L), 1.6, 0.5, 2, false, 60, longX ? Math.PI / 2 : 0);
break;
}
case 'garage': {
if (a > 14 && shortSide(room) >= 2.6) inMiddle(room, K.carDef(), 0, undefined, rng.pick(CAR_COLORS));
atWall(room, K.workbench(1.6));
atWall(room, K.shelf(1.2));
break;
}
default:
break;
}
}
return out;
}