/**
* Procedural-vector renderer: layout + segments -> SVG string.
*
* No external art. The "good look" comes from established cartography tricks:
* - thick walls with a soft cast shadow onto the floor (reads as 3-D)
* - Dyson-style hatching just outside the exterior walls
* - per-theme floor texture drawn in SVG (tiles / flagstones / boards)
* - architectural door symbols (leaf + swing arc)
*
* Pure + deterministic: same seed -> same decoration. Runs in Node and browser.
*/
import { Rng } from "./rng.mjs";
import { pieceInnerSVG } from "./furniture.mjs";
const THEMES = {
modern: {
outside: "#23262b", floorBase: "#d9dde2", floorSeam: "#b3bbc4",
grid: "#9aa3ad", wall: "#2b2f36", wallTop: "#3c424b",
doorFrame: "#3a3f45", doorLeaf: "#1f2329", doorFill: "#5a6470",
window: "#8fc4e2", windowFrame: "#3a3f45",
label: "#2b2f36", hatch: "#3a3f45", floorStyle: "tile"
},
fantasy: {
outside: "#2a241c", floorBase: "#bcab8f", floorSeam: "#8d7a5c",
grid: "#6f5f47", wall: "#372d20", wallTop: "#4c3f2c",
doorFrame: "#6a4a2c", doorLeaf: "#3a2614", doorFill: "#7a5532",
window: "#86b8c4", windowFrame: "#4a3a26",
label: "#322617", hatch: "#4a3a26", floorStyle: "flagstone"
}
};
const PX = (v, gs) => +(v * gs).toFixed(1);
/**
* @param {object} args
* @param {object} args.layout generateBuilding() output
* @param {object} args.walls buildWalls() output ({segments,...})
* @param {number} [args.gridSize]
* @param {string} [args.subtype] "modern" | "fantasy"
* @param {boolean} [args.showGrid]
* @param {boolean} [args.showLabels]
* @param {string|number} [args.seed]
* @returns {string} SVG markup
*/
export function renderSVG(args) {
const { layout } = args;
const gs = args.gridSize ?? 70;
const theme = THEMES[args.subtype] ? THEMES[args.subtype] : THEMES.modern;
const showGrid = args.showGrid !== false;
const showLabels = args.showLabels !== false;
const rng = new Rng(`${args.seed ?? "deco"}::render`);
const W = layout.cols * gs;
const H = layout.rows * gs;
const subtype = THEMES[args.subtype] ? args.subtype : "modern";
const region = layout.region;
const cols = layout.cols, rows = layout.rows;
const showFurniture = args.showFurniture !== false;
const segs = args.walls.segments;
const out = [];
const floorPath = buildFloorPath(region, cols, rows, gs);
out.push(``);
return out.join("");
}
/** Draw furniture placements (same geometry the locked Tiles use). */
function drawPlacements(placements, subtype) {
let out = ``;
for (const p of placements) {
const inner = pieceInnerSVG(p.kind, subtype, p.wpx, p.hpx);
const tx = (p.cx - p.wpx / 2).toFixed(1);
const ty = (p.cy - p.hpx / 2).toFixed(1);
out += `${inner}`;
}
return out + ``;
}
/** Floor path from the region grid: per-row runs of in-room cells -> rects. */
function buildFloorPath(region, cols, rows, gs) {
let d = "";
for (let y = 0; y < rows; y += 1) {
let x = 0;
while (x < cols) {
if (region[y][x] < 0) { x += 1; continue; }
let run = 0;
while (x + run < cols && region[y][x + run] >= 0) run += 1;
d += `M${PX(x, gs)},${PX(y, gs)}h${PX(run, gs)}v${PX(1, gs)}h${PX(-run, gs)}Z`;
x += run;
}
}
return d;
}
function regAt(region, x, y, cols, rows) {
if (x < 0 || y < 0 || x >= cols || y >= rows) return -1;
return region[y][x];
}
// --- floor textures --------------------------------------------------------
function floorTexture(theme, layout, gs, rng) {
if (theme.floorStyle === "flagstone") return flagstoneTexture(theme, layout, gs, rng);
return tileTexture(theme, layout, gs);
}
function tileTexture(theme, layout, gs) {
// Clean square-tile seams across the footprint.
const fp = layout.footprint;
const lines = [];
lines.push(``);
for (let x = fp.x; x <= fp.x + fp.w; x += 1) {
lines.push(``);
}
for (let y = fp.y; y <= fp.y + fp.h; y += 1) {
lines.push(``);
}
lines.push(``);
return lines.join("");
}
function flagstoneTexture(theme, layout, gs, rng) {
// Irregular flagstones: per in-room cell, a jittered quad. (Clipped to floor.)
const { region, cols, rows } = layout;
const j = gs * 0.14;
const out = [``];
for (let y = 0; y < rows; y += 1) {
for (let x = 0; x < cols; x += 1) {
if (region[y][x] < 0) continue;
const p = (cx, cy) => `${PX(cx, gs) + (rng.float() - 0.5) * j},${PX(cy, gs) + (rng.float() - 0.5) * j}`;
out.push(``);
}
}
out.push(``);
return out.join("");
}
function gridOverlay(layout, gs, color) {
const fp = layout.footprint;
const out = [``];
for (let x = fp.x; x <= fp.x + fp.w; x += 1) {
out.push(``);
}
for (let y = fp.y; y <= fp.y + fp.h; y += 1) {
out.push(``);
}
out.push(``);
return out.join("");
}
// --- exterior hatching -----------------------------------------------------
function exteriorHatching(segs, region, cols, rows, gs, rng) {
const out = [];
const step = 0.4; // along-wall spacing in cells
const len = gs * 0.34; // tick length in px
for (const s of segs) {
if (s.kind !== "wall") continue;
const isV = s.x1 === s.x2;
// Detect exterior + outward direction by sampling either side at the midpoint.
let nx = 0, ny = 0, exterior = false;
if (isV) {
const X = s.x1, midY = Math.floor((s.y1 + s.y2) / 2);
const left = regAt(region, X - 1, midY, cols, rows);
const right = regAt(region, X, midY, cols, rows);
if (left === -1 && right >= 0) { nx = -1; exterior = true; }
else if (right === -1 && left >= 0) { nx = 1; exterior = true; }
} else {
const Y = s.y1, midX = Math.floor((s.x1 + s.x2) / 2);
const top = regAt(region, midX, Y - 1, cols, rows);
const bottom = regAt(region, midX, Y, cols, rows);
if (top === -1 && bottom >= 0) { ny = -1; exterior = true; }
else if (bottom === -1 && top >= 0) { ny = 1; exterior = true; }
}
if (!exterior) continue;
const length = isV ? (s.y2 - s.y1) : (s.x2 - s.x1);
for (let t = step / 2; t < length; t += step) {
const bx = isV ? s.x1 : s.x1 + t;
const by = isV ? s.y1 + t : s.y1;
const px = PX(bx, gs), py = PX(by, gs);
// slight slant for a hand-drawn feel
const slant = (rng.float() - 0.5) * 0.5;
const ex = px + nx * len + (isV ? 0 : slant * len);
const ey = py + ny * len + (isV ? slant * len : 0);
out.push(``);
}
}
return out.join("");
}
// --- door symbol (era-aware artwork) ---------------------------------------
function doorSymbol(s, gs, theme, subtype) {
const x1 = PX(s.x1, gs), y1 = PX(s.y1, gs);
const x2 = PX(s.x2, gs), y2 = PX(s.y2, gs);
const L = Math.hypot(x2 - x1, y2 - y1) || 1;
const ux = (x2 - x1) / L, uy = (y2 - y1) / L; // along the wall
const ppx = -uy, ppy = ux; // wall perpendicular
const isV = s.orient === "v";
const insideA = s.inside === "a";
let nx = 0, ny = 0;
if (isV) nx = insideA ? -1 : 1; else ny = insideA ? -1 : 1;
const wallTh = gs * 0.17;
const out = [``];
// Jamb posts (door frame) straddling the wall at both ends.
const jamb = wallTh * 1.25;
for (const [ex, ey] of [[x1, y1], [x2, y2]]) {
out.push(
``
);
}
// Hinge at end 1; leaf swings ~75° toward the inside.
const hx = x1, hy = y1;
const ang = (75 * Math.PI) / 180;
const sign = (ux * ny - uy * nx) >= 0 ? 1 : -1;
const cos = Math.cos(ang * sign), sin = Math.sin(ang * sign);
const leafLen = L * 0.94;
const dx = ux * cos - uy * sin, dy = ux * sin + uy * cos; // leaf direction
const lx = hx + dx * leafLen, ly = hy + dy * leafLen;
const ox = hx + ux * leafLen, oy = hy + uy * leafLen; // closed position
// Swing arc.
out.push(
``
);
// Leaf as a filled panel (planked wood / metal slab).
const pdx = -dy, pdy = dx; // across the leaf thickness
const th = gs * 0.13;
const pt = (bx, by, sx, sy) => `${(bx + pdx * sx + dx * sy).toFixed(1)},${(by + pdy * sx + dy * sy).toFixed(1)}`;
out.push(
``
);
if (subtype === "fantasy") {
for (const f of [0.33, 0.66]) {
out.push(``);
}
} else {
out.push(``);
}
// Handle + hinge dot.
const handle = pt(hx, hy, 0, leafLen * 0.82).split(",");
out.push(``);
out.push(``);
if (s.locked) {
const mx = (hx + ox) / 2, my = (hy + oy) / 2;
out.push(
`` +
``
);
}
out.push(``);
return out.join("");
}
// --- window symbol (era-aware artwork) -------------------------------------
function windowSymbol(s, gs, theme, sill, subtype) {
const x1 = PX(s.x1, gs), y1 = PX(s.y1, gs), x2 = PX(s.x2, gs), y2 = PX(s.y2, gs);
const L = Math.hypot(x2 - x1, y2 - y1) || 1;
const ux = (x2 - x1) / L, uy = (y2 - y1) / L;
const px = -uy, py = ux; // perpendicular (sill width)
const out = [``];
// Frame band, then glass band.
out.push(``);
out.push(``);
// Highlight streak.
out.push(``);
const bar = (f) => {
const bx = x1 + (x2 - x1) * f, by = y1 + (y2 - y1) * f;
out.push(``);
};
if (subtype === "fantasy") {
bar(0.33); bar(0.66); // leaded panes
} else {
bar(0.5); // single mullion
}
out.push(``);
return out.join("");
}
function escapeXml(str) {
return String(str).replace(/[<>&'"]/g, (c) => (
{ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ }[c]
));
}