Files
RingBRP/tools/bellhouse-plans.mjs
T
slaguru666andClaude Opus 5 d89acbac93 STANDING ORDER: section map draws the pit galleries, key in order
The section now shows 13, 14, 15 and 18 on a third level with the
incline from the plant floor, instead of footnoting them as not drawn,
and the key runs 6-18 in number order in two columns. Sheet 6 moves the
incline to the plant's north end to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:28:32 +01:00

769 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* STANDING ORDER — the Bellhouse blueprints: eight floor plans of the Seat, one per
* Foundry scene.
*
* Drawn from declared rooms rather than generated, for culland-hall.svg's reason: a room,
* a door and a label are load-bearing, and a generator cannot be trusted with any of them.
* Every plan is 2800x2000 at 100px to the square (5 ft), so the Foundry grid lands on the
* drawn one. Labels are what a 1962 drawing office would have written, plus the pencil
* amendments of fifty years below; nothing on a plan tells the players a GM secret.
*
* The sheets follow the section drawing (SO-H04, bellhouse-section.svg): locations 6-9 on
* Level 1, 10-12 on Level 2, and 13-15 in the pit galleries the section does not draw.
* North is to the left on every sheet, as on the section (looking west): the lobby is the
* north end, the sick bay and the stair down the south. Every sheet names where its
* corridors lead, so the eight read as one building.
*
* node tools/bellhouse-plans.mjs write art/maps/bh-*.svg
* node tools/bellhouse-plans.mjs --render and render each to .webp (rsvg-convert, cwebp)
* node tools/bellhouse-plans.mjs --check hold every sheet to the rules below, write nothing
*
* WHAT --check HOLDS. Every door and archway sits on a wall two different spaces share (or
* on an outside wall it is declared to leave by). Every door swings into a space, clear of
* furniture and lettering. Every piece of furniture sits wholly inside one room. No two
* rooms overlap unless one is inside the other. Every room can be walked to from the
* sheet's way in. It does not judge whether a room is sensible; that is a person's job.
*/
import { writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import path from "node:path";
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
const OUT = path.join(ROOT, "art", "maps");
const S = 100, W = 28, H = 20, SHEETS = 13;
const esc = s => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const p = n => +(n * S).toFixed(1);
/* ---------------------------------------------------------------- primitives */
/* Approximate lettering widths, in squares per character, for the checker. */
const CHAR = { lbl: 0.2, sub: 0.126, pen: 0.13, tb: 0.12 };
function sheet() {
const g = [];
const geo = { rooms: [], doors: [], items: [], labels: [], exits: [] };
const item = (x, y, w, h, kind) => geo.items.push({ x, y, w, h, kind });
const label = (x, y, t, cls, size) => {
const cw = CHAR[cls] ?? 0.12;
const k = size ? size / { lbl: 30, sub: 21, pen: 27, tb: 20 }[cls] : 1;
geo.labels.push({ x, y: y - 0.24 * k, w: t.length * cw * k, h: 0.3 * k, text: t, kind: cls });
};
const api = {
g, geo,
room(x, y, w, h, name, { sub, n, cls = "rm", lx = 0.3, ly = 0.75, entry = false } = {}) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="${cls}"/>`);
geo.rooms.push({ x, y, w, h, name: name ?? `(${x},${y})`, entry });
if (name) { g.push(`<text x="${p(x + lx)}" y="${p(y + ly)}" class="lbl">${esc(name)}</text>`); label(x + lx, y + ly, name, "lbl"); }
if (sub) { g.push(`<text x="${p(x + lx)}" y="${p(y + ly + 0.38)}" class="sub">${esc(sub)}</text>`); label(x + lx, y + ly + 0.38, sub, "sub"); }
if (n != null) {
g.push(`<circle cx="${p(x + w - 0.45)}" cy="${p(y + 0.45)}" r="30" class="numc"/>`
+ `<text x="${p(x + w - 0.45)}" y="${p(y + 0.45) + 13}" class="num">${n}</text>`);
item(x + w - 0.75, y + 0.15, 0.6, 0.6, "number");
}
},
/* A door on a wall: `v` for a vertical wall at x, else horizontal at y. A gap in the
wall and a quarter-circle swing, the way a drawing office draws one. `flip` swings it
to the left of a vertical wall, or above a horizontal one. */
door(x, y, v, len = 1, flip = false, { out = false } = {}) {
const L = p(len), X = p(x), Y = p(y), d = flip ? -1 : 1;
if (v) {
g.push(`<line x1="${X}" y1="${Y}" x2="${X}" y2="${Y + L}" class="gap"/>`,
`<path d="M${X} ${Y}L${X + d * L} ${Y}A${L} ${L} 0 0 ${d > 0 ? 1 : 0} ${X} ${Y + L}" class="swing"/>`);
} else {
g.push(`<line x1="${X}" y1="${Y}" x2="${X + L}" y2="${Y}" class="gap"/>`,
`<path d="M${X} ${Y}L${X} ${Y + d * L}A${L} ${L} 0 0 ${d > 0 ? 0 : 1} ${X + L} ${Y}" class="swing"/>`);
}
geo.doors.push({ x, y, v, len, d, out, swing: true });
},
/* An open archway: just the gap. */
arch(x, y, v, len = 2, cls = "gap") {
const X = p(x), Y = p(y), L = p(len);
g.push(v ? `<line x1="${X}" y1="${Y}" x2="${X}" y2="${Y + L}" class="${cls}"/>`
: `<line x1="${X}" y1="${Y}" x2="${X + L}" y2="${Y}" class="${cls}"/>`);
geo.doors.push({ x, y, v, len, d: 0, out: false, swing: false });
},
box(x, y, w, h, text, size = 20) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="fur"/>`);
if (text) g.push(`<text x="${p(x + w / 2)}" y="${p(y + h / 2) + size * 0.35}" class="fl" font-size="${size}">${esc(text)}</text>`);
item(x, y, w, h, text || "box");
},
chair(x, y) { g.push(`<rect x="${p(x)}" y="${p(y)}" width="34" height="34" rx="6" class="fur"/>`); item(x, y, 0.34, 0.34, "chair"); },
bed(x, y, vert = true, w = 0.9, l = 2) {
const [bw, bh] = vert ? [w, l] : [l, w];
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(bw)}" height="${p(bh)}" class="fur"/>`);
g.push(vert ? `<line x1="${p(x)}" y1="${p(y + 0.45)}" x2="${p(x + bw)}" y2="${p(y + 0.45)}" class="fine"/>`
: `<line x1="${p(x + 0.45)}" y1="${p(y)}" x2="${p(x + 0.45)}" y2="${p(y + bh)}" class="fine"/>`);
item(x, y, bw, bh, "bed");
},
shelf(x, y, w, h, kind = "shelf") {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="fur"/>`);
const lines = [];
if (w >= h) for (let i = x + 0.25; i < x + w - 0.01; i += 0.25) lines.push(`M${p(i)} ${p(y)}V${p(y + h)}`);
else for (let j = y + 0.25; j < y + h - 0.01; j += 0.25) lines.push(`M${p(x)} ${p(j)}H${p(x + w)}`);
g.push(`<path d="${lines.join("")}" class="fine"/>`);
item(x, y, w, h, kind);
},
reels(x, y, text = "TAPE") {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="200" height="120" class="fur"/>`,
`<circle cx="${p(x + 0.5)}" cy="${p(y + 0.5)}" r="32" class="fine"/>`,
`<circle cx="${p(x + 1.5)}" cy="${p(y + 0.5)}" r="32" class="fine"/>`,
`<text x="${p(x + 1)}" y="${p(y + 1.12)}" class="fl" font-size="15">${esc(text)}</text>`);
item(x, y, 2, 1.2, "reels");
},
safe(x, y) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="80" height="80" class="fur"/>`,
`<path d="M${p(x)} ${p(y)}l80 80M${p(x) + 80} ${p(y)}l-80 80" class="fine"/>`,
`<text x="${p(x + 0.4)}" y="${p(y + 1.1)}" class="fl" font-size="16">SAFE</text>`);
item(x, y, 0.8, 1.15, "safe");
},
speaker(x, y) {
g.push(`<circle cx="${p(x)}" cy="${p(y)}" r="16" class="fur"/>`,
`<path d="M${p(x) + 24} ${p(y) - 16}a24 24 0 0 1 0 32M${p(x) + 34} ${p(y) - 26}a38 38 0 0 1 0 52" class="fine"/>`);
item(x - 0.16, y - 0.26, 0.9, 0.52, "speaker");
},
round(x, y, r, text) {
g.push(`<circle cx="${p(x)}" cy="${p(y)}" r="${p(r)}" class="fur"/>`);
if (text) g.push(`<text x="${p(x)}" y="${p(y) + 7}" class="fl" font-size="18">${esc(text)}</text>`);
item(x - r, y - r, 2 * r, 2 * r, text || "round");
},
peg(x, y) { g.push(`<circle cx="${p(x)}" cy="${p(y)}" r="7" fill="#e8f1fb"/>`); item(x - 0.07, y - 0.07, 0.14, 0.14, "peg"); },
/* A flight of stairs is a room of its own: you walk through it. */
stairs(x, y, w, h, text, { vert = false, entry = false } = {}) {
g.push(`<rect x="${p(x)}" y="${p(y)}" width="${p(w)}" height="${p(h)}" class="rm2"/>`);
const d = [];
if (vert) for (let i = x + 0.3; i < x + w - 0.01; i += 0.3) d.push(`M${p(i)} ${p(y)}V${p(y + h)}`);
else for (let j = y + 0.3; j < y + h - 0.01; j += 0.3) d.push(`M${p(x)} ${p(j)}H${p(x + w)}`);
g.push(`<path d="${d.join("")}" class="fine"/>`);
const tw = text.length * 0.12 + 0.2;
g.push(`<rect x="${p(x + w / 2 - tw / 2)}" y="${p(y + h / 2) - 18}" width="${p(tw)}" height="30" fill="#11417d"/>`,
`<text x="${p(x + w / 2)}" y="${p(y + h / 2) + 5}" class="fl" font-size="19">${esc(text)}</text>`);
geo.rooms.push({ x, y, w, h, name: text, entry, stairs: true });
},
blast(x, y, h) {
g.push(`<rect x="${p(x) - 22}" y="${p(y)}" width="44" height="${p(h)}" class="blast"/>`);
const d = [];
for (let j = y; j < y + h - 0.01; j += 0.2) d.push(`M${p(x) - 22} ${p(j) + 20}l44 -20`);
g.push(`<path d="${d.join("")}" class="fine"/>`);
geo.doors.push({ x, y, v: true, len: h, d: 0, out: true, swing: false });
},
/* The drawing office's own lettering. */
text(x, y, t, cls = "sub") {
g.push(`<text x="${p(x)}" y="${p(y)}" class="${cls}">${esc(t)}</text>`);
label(x, y, t, cls);
},
/* Pencil: fifty years of amendments in a different hand. */
pencil(x, y, t, rot = -2) {
g.push(`<text x="${p(x)}" y="${p(y)}" class="pen" transform="rotate(${rot} ${p(x)} ${p(y)})">${esc(t)}</text>`);
label(x, y, t, "pen");
},
/* Where a corridor leaves the sheet: an arrow to the edge and the sheet it reaches. */
exit(x, y, dir, t, { tx, ty } = {}) {
const [dx, dy] = { l: [-1, 0], r: [1, 0], u: [0, -1], d: [0, 1] }[dir];
const x2 = dx < 0 ? 0.15 : dx > 0 ? W - 0.15 : x, y2 = dy < 0 ? 0.15 : dy > 0 ? H - 0.15 : y;
g.push(`<line x1="${p(x)}" y1="${p(y)}" x2="${p(x2)}" y2="${p(y2)}" class="arw" marker-end="url(#ah)"/>`);
const lx = tx ?? (dx > 0 ? x2 - t.length * CHAR.sub - 0.1 : dx < 0 ? 0.2 : x + 0.2);
const ly = ty ?? (dy === 0 ? y - 0.2 : dy < 0 ? 0.45 : H - 0.3);
g.push(`<text x="${p(lx)}" y="${p(ly)}" class="sub exit">${esc(t)}</text>`);
if (t) label(lx, ly, t, "sub");
geo.exits.push({ x, y, text: t });
},
raw(s) { g.push(s); }
};
return api;
}
const STYLE = `
.rm { fill:#154a8a; stroke:#e8f1fb; stroke-width:7; }
.rm2 { fill:#11417d; stroke:#e8f1fb; stroke-width:5; }
.rock { fill:#0e3668; stroke:#9fbde0; stroke-width:4; stroke-dasharray:18 10; }
.gap { stroke:#154a8a; stroke-width:12; }
.gap2 { stroke:#11417d; stroke-width:12; }
.rgap { stroke:#0e3668; stroke-width:12; }
.swing { fill:none; stroke:#e8f1fb; stroke-width:3; }
.fur { fill:#1b579c; stroke:#cfe0f3; stroke-width:3; }
.fine { fill:none; stroke:#cfe0f3; stroke-width:2; }
.blast { fill:#0a2f5c; stroke:#e8f1fb; stroke-width:4; }
.arw { stroke:#e8f1fb; stroke-width:4; }
.grid { stroke:#2a5d9c; stroke-width:1; }
.numc { fill:#e8f1fb; }
.num { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:36px;
fill:#123f78; text-anchor:middle; }
.lbl { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:30px;
fill:#f4f8fd; letter-spacing:2px; }
.sub { font-family:'Courier New',Courier,monospace; font-size:21px; fill:#b9d0ea; }
.exit { font-weight:700; fill:#e8f1fb; }
.fl { font-family:'Courier New',Courier,monospace; font-size:18px; fill:#e8f1fb;
text-anchor:middle; }
.ttl { font-family:'Courier New',Courier,monospace; font-weight:700; font-size:34px;
fill:#f4f8fd; letter-spacing:3px; }
.tb { font-family:'Courier New',Courier,monospace; font-size:20px; fill:#dbe8f6; }
.pen { font-family:'Bradley Hand','Noteworthy','Marker Felt',cursive; font-size:27px;
fill:#ffe98a; }
`;
const TITLE_BLOCK = { x: 19, y: 16.5, w: 8.7, h: 3.3 };
const LEGEND = { w: 6.4, h: 3.8 };
function build(plan) {
const s = sheet();
plan.draw(s);
const legend = plan.legend ?? [0.3, 16.1];
return { s, legend };
}
function render(plan) {
const { s, legend } = build(plan);
const { no, title, level } = plan;
const grid = [];
for (let i = 1; i < W; i++) grid.push(`M${i * S} 0V${H * S}`);
for (let j = 1; j < H; j++) grid.push(`M0 ${j * S}H${W * S}`);
// Title block, bottom right, as a drawing office would box it.
const [tx, ty, tw, th] = [TITLE_BLOCK.x, TITLE_BLOCK.y, TITLE_BLOCK.w, TITLE_BLOCK.h].map(p);
const block = `
<rect x="${tx}" y="${ty}" width="${tw}" height="${th}" fill="#0f3a70" stroke="#e8f1fb" stroke-width="5"/>
<line x1="${tx}" y1="${ty + 110}" x2="${tx + tw}" y2="${ty + 110}" stroke="#e8f1fb" stroke-width="3"/>
<line x1="${tx}" y1="${ty + 250}" x2="${tx + tw}" y2="${ty + 250}" stroke="#e8f1fb" stroke-width="3"/>
<text x="${tx + 24}" y="${ty + 44}" class="tb">REGIONAL SEAT OF GOVERNMENT 12 · BELLHOUSE</text>
<text x="${tx + 24}" y="${ty + 88}" class="tb">SHEET ${no} OF ${SHEETS} · ${esc(level)}</text>
<text x="${tx + 24}" y="${ty + 165}" class="ttl">${esc(title)}</text>
<text x="${tx + 24}" y="${ty + 216}" class="tb">SCALE: ONE SQUARE = 5 FT · SECTION: SO-H04</text>
<text x="${tx + 24}" y="${ty + 300}" class="tb">DRG RSG12/${String(no).padStart(2, "0")} · ISSUED 1962 · COPY NOT TO LEAVE THE SEAT</text>`;
// Legend, bottom left.
const [lx, ly] = legend.map(p);
const key = `
<rect x="${lx}" y="${ly}" width="${p(LEGEND.w)}" height="${p(LEGEND.h)}" fill="#0f3a70" stroke="#e8f1fb" stroke-width="3"/>
<text x="${lx + 24}" y="${ly + 44}" class="tb" font-weight="700">KEY</text>
<path d="M${lx + 30} ${ly + 80}L${lx + 30} ${ly + 150}A70 70 0 0 0 ${lx + 100} ${ly + 80}" class="swing"/>
<line x1="${lx + 30}" y1="${ly + 80}" x2="${lx + 100}" y2="${ly + 80}" stroke="#e8f1fb" stroke-width="7"/>
<text x="${lx + 130}" y="${ly + 120}" class="tb">DOOR</text>
<circle cx="${lx + 360}" cy="${ly + 110}" r="26" class="numc"/>
<text x="${lx + 360}" y="${ly + 122}" class="num" font-size="30">7</text>
<text x="${lx + 400}" y="${ly + 120}" class="tb">LOCATION No.</text>
<text x="${lx + 30}" y="${ly + 200}" class="pen" font-size="25">pencil</text>
<text x="${lx + 130}" y="${ly + 200}" class="tb">= AMENDED AT THE SEAT, 1974–2026</text>
<rect x="${lx + 30}" y="${ly + 232}" width="70" height="44" class="rock"/>
<text x="${lx + 130}" y="${ly + 262}" class="tb">= UNLINED MINE WORKINGS</text>
<text x="${lx + 30}" y="${ly + 330}" class="tb">1 SQUARE = 5 FT · NORTH ←</text>`;
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * S}" height="${H * S}" viewBox="0 0 ${W * S} ${H * S}">
<!-- STANDING ORDER — ${esc(title)}. Generated by tools/bellhouse-plans.mjs; edit that, not this. -->
<defs>
<style>${STYLE}</style>
<marker id="ah" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M0 0L10 5L0 10z" fill="#e8f1fb"/>
</marker>
</defs>
<rect width="${W * S}" height="${H * S}" fill="#123f78"/>
<path d="${grid.join("")}" class="grid"/>
${s.g.join("\n ")}
${key}
${block}
</svg>
`;
}
/* ---------------------------------------------------------------- the sheets */
const MAIN = "MAIN CORRIDOR", LOWER = "LOWER CORRIDOR";
export const PLANS = [
/* Level 1, the adit end. The crossing lands in the blast lock; the adit beyond the blast
door has been rubble since 1975. */
{ key: "bh-lobby", no: 1, title: "LOBBY & ENQUIRIES", level: "LEVEL 1 · ENTRY", draw: s => {
s.room(1, 6, 4, 6, "BLAST LOCK", { sub: "outer lock", entry: true });
s.blast(1, 8, 2);
s.raw(`<circle cx="${p(3.1)}" cy="${p(9.6)}" r="${p(1)}" fill="none" stroke="#ffe98a" stroke-width="4" stroke-dasharray="14 10"/>`);
s.pencil(1.5, 11.5, "air goes wrong here", -3);
s.text(0.2, 7.6, "ADIT");
s.room(5, 6, 4, 6, "DECONTAM.", { sub: "showers · lockers" });
for (const y of [8.4, 9.6, 10.8]) s.round(6.7, y, 0.3);
s.shelf(7.9, 7.4, 0.8, 4.2);
s.room(9, 3, 10, 12, "LOBBY", { n: 6, sub: "public waiting" });
s.box(14.2, 5, 3.8, 0.8, "ENQUIRIES COUNTER");
s.box(17.2, 5.8, 0.8, 2.4);
for (const y of [8.4, 10.4, 12.4]) {
s.box(10, y, 2.9, 0.4);
for (const x of [10.2, 11.2, 12.2]) s.chair(x, y + 0.55);
}
s.box(14, 14.4, 3.6, 0.35, "NOTICEBOARD", 14);
s.pencil(13.6, 13.9, "housing list — see Mr Ogden", -1);
s.room(19, 3, 4.5, 4.5, "ENQUIRIES", { sub: "Mr Ogden" });
s.box(20.2, 4.9, 2, 1, "DESK"); s.chair(21, 6.1); s.shelf(22.7, 3.9, 0.6, 2.4);
s.round(22.9, 7, 0.25, "☎");
s.room(19, 7.5, 4.5, 4, "INTERVIEW", { sub: "does not lock" });
s.box(20.2, 9.3, 2.2, 1.1, "TABLE");
s.chair(20.4, 8.85); s.chair(21.7, 8.85); s.chair(20.4, 10.5); s.chair(21.7, 10.5);
s.room(23.5, 3, 4, 4.5, "ARMOURY");
s.shelf(23.8, 5, 3.4, 0.6); s.text(23.8, 6.1, "arms rack");
s.pencil(23.8, 7.1, "key: Controller", -2);
s.room(23.5, 7.5, 4, 4, "WARDENS'", { sub: "post" });
s.shelf(24, 10.2, 2.4, 0.5); s.text(24, 11.15, "truncheons");
s.room(19, 11.5, 8.5, 3, MAIN, { cls: "rm2", ly: 1.9, lx: 0.4, entry: true });
s.exit(27, 12.4, "r", "CONTROL, REGISTRY, SICK BAY · SHEETS 2–4 →", { tx: 19.2, ty: 15.05 });
s.door(5, 8.6, true, 1); s.door(9, 8.6, true, 1);
s.door(19, 4.3, true, 1, true); s.door(19, 8.9, true, 1, true);
s.door(26.6, 7.5, false, 0.8, true); s.door(23.5, 8.5, true, 0.8, true); s.door(24.4, 11.5, false, 1);
s.arch(19, 12, true, 2, "gap2");
}},
/* Level 1. Locations 7 and the Controller's floor. */
{ key: "bh-control", no: 2, title: "CONTROL ROOM", level: "LEVEL 1 · GOVERNMENT", draw: s => {
s.room(0.5, 1, 27, 3, MAIN, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(1, 3.4, "l", "← LOBBY · SHEET 1", { ty: 0.6 });
s.exit(27, 3.4, "r", "REGISTRY · SHEET 3 →", { ty: 0.6 });
s.room(1, 4, 13, 9, "CONTROL ROOM", { n: 7, sub: "Controller's floor" });
s.box(1.6, 5.6, 11.8, 0.9, "CONSOLE · DIALS · TANNOY DESK");
for (let x = 2.4; x < 13; x += 1.4) s.chair(x, 6.65);
s.box(3.5, 8.1, 6, 2.5, "SITUATION MAP · REGION 12", 20);
for (const x of [3.9, 6.3, 8.7]) { s.chair(x, 7.5); s.chair(x, 10.8); }
s.round(12.3, 11.5, 0.55, "SCOPE");
s.pencil(9, 12.7, "SEALED 1974 — by order", -2);
s.room(14, 4, 6, 5, "CONTROLLER", { sub: "E. Sowerby" });
s.box(15.2, 5.8, 2.4, 1.1, "DESK"); s.chair(16.2, 7.1);
s.shelf(19.1, 4.3, 0.7, 3.6); s.text(17.3, 8.6, "files · Centre");
s.room(14, 9, 6, 6, "STUDIO", { sub: "broadcast · 1962" });
s.reels(15.6, 10.7, "TAPE DECK");
s.safe(18.9, 9.25);
s.box(15.2, 12.6, 2.6, 0.9, "MIXER"); s.chair(16.3, 13.7);
s.round(19.2, 13.7, 0.3, "MIC");
s.pencil(14.3, 14.75, "new tape rec'd from Centre 2019", -1);
s.room(20, 4, 7.5, 5, "SECRETARIAT", { sub: "minutes · rotas" });
for (const x of [20.6, 22.8, 25]) { s.box(x, 6.1, 1.6, 0.8); s.chair(x + 0.6, 7.1); }
s.shelf(20.4, 8.2, 3, 0.55);
s.room(20, 9, 7.5, 6, "COMMS", { sub: "teleprinters · switchboard" });
s.box(21, 10.9, 1.3, 1, "TP"); s.box(22.7, 10.9, 1.3, 1, "TP");
s.box(20.6, 12.7, 5, 0.9, "SWITCHBOARD"); s.chair(21.6, 13.9); s.chair(23.6, 13.9);
s.pencil(24.4, 11.3, "no line out", 4);
s.door(2.2, 4, false, 1, true); s.door(14.8, 4, false, 1, true); s.door(25.8, 4, false, 1, true);
s.door(14, 11.6, true, 1, true); s.door(14.4, 9, false, 0.9, true); s.door(26.1, 9, false, 1, true);
}},
/* Level 1. Location 8. The public side of the counter is the corridor side. */
{ key: "bh-registry", no: 3, title: "REGISTRY", level: "LEVEL 1 · GOVERNMENT", draw: s => {
s.room(0.5, 1, 27, 3, MAIN, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(1, 3.4, "l", "← CONTROL ROOM · SHEET 2", { ty: 0.6 });
s.exit(27, 3.4, "r", "SICK BAY · SHEET 4 →", { ty: 0.6 });
s.room(1, 4, 9, 7, "PUBLIC COUNTER", { n: 8 });
s.box(1, 7.4, 7.4, 0.7, "COUNTER");
s.text(8.5, 7.9, "flap");
for (const x of [1.6, 3, 4.4]) s.chair(x, 5.9);
s.box(5.6, 5.9, 2, 0.45, "TAKE A NUMBER", 13);
s.pencil(1.4, 10.6, "complaints: allow 11 weeks", -2);
s.room(10, 4, 12, 11, "RECORDS");
for (let x = 11; x < 21.5; x += 1.5) s.shelf(x, 5.6, 0.6, 7.6);
s.text(10.4, 14.5, "BIRTHS · DEATHS · HOUSING · RATIONS · 1974–");
s.room(22, 4, 5.5, 5, "REGISTRAR", { sub: "A. Pennyfeather" });
s.box(23.2, 5.8, 2.2, 1, "DESK"); s.chair(24.1, 7.1);
s.room(22, 9, 5.5, 6, "STRONGROOM", { sub: "standing orders" });
s.safe(26.3, 10.1); s.shelf(22.5, 11.4, 3.5, 0.7); s.shelf(22.5, 13.4, 3.5, 0.7);
s.room(1, 11, 9, 4, "TYPING POOL");
for (const x of [1.4, 4.1, 6.8]) { s.box(x, 12.3, 1.8, 0.8); s.chair(x + 0.7, 13.4); }
s.door(3.2, 4, false, 1, true); s.door(24.2, 4, false, 1, true);
s.door(10, 9.3, true, 1, true); s.door(22, 8, true, 0.9, true); s.door(26.3, 9, false, 0.9, true);
s.door(6.2, 11, false, 1, true);
}},
/* Level 1, the far end. Location 9, and the stair down to Level 2. */
{ key: "bh-sickbay", no: 4, title: "SICK BAY", level: "LEVEL 1 · WELFARE", draw: s => {
s.room(0.5, 1, 24, 3, MAIN, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(1, 3.4, "l", "← REGISTRY · SHEET 3", { ty: 0.6 });
s.stairs(24.5, 1, 3, 3, "DOWN");
s.text(19.6, 0.6, "DOWN TO LEVEL 2 · SHEET 5");
s.arch(24.5, 1.5, true, 2, "gap2");
s.room(1, 4, 6, 5, "RECEPTION", { n: 9 });
s.box(1.6, 5.7, 3, 0.7, "DESK");
for (const x of [1.6, 2.6, 3.6, 4.6]) s.chair(x, 7.2);
s.room(7, 4, 12, 6, "GENERAL WARD", { sub: "eighteen beds" });
for (let x = 7.6; x < 17; x += 1.1) { s.bed(x, 5.3); s.bed(x, 7.9, true, 0.9, 1.9); }
s.room(19, 4, 4, 3, "SIDE ROOM", { sub: "Mrs Marlow" });
s.bed(21.9, 4.2, true, 0.9, 1.8); s.chair(20.9, 5.4); s.shelf(19.3, 6.3, 1.4, 0.5);
s.room(23, 4, 4.5, 3, "DISPENSARY");
s.shelf(23.3, 5.1, 3.9, 0.55);
s.room(19, 7, 8.5, 3, "MEDICAL OFFICER", { sub: "Dr Larkhall" });
s.box(23.2, 8.2, 2, 1, "DESK"); s.shelf(26.6, 7.3, 0.6, 2.4); s.chair(23.9, 9.3);
s.room(7, 10, 7, 5, "DELIVERY", { sub: "maternity" });
s.bed(10, 11.8, false, 1, 2.2); s.round(12.9, 12.4, 0.35, "COT");
s.pencil(7.4, 14.6, "Cotter — due Thurs", -2);
s.room(14, 10, 6, 5, "THEATRE");
s.box(15.8, 11.6, 2.4, 1, "TABLE"); s.round(18.9, 11.2, 0.4); s.shelf(14.3, 14, 3, 0.6);
s.room(1, 9, 6, 6, "ISOLATION");
s.room(1, 11, 3, 4, null); s.room(4, 11, 3, 4, null);
s.bed(1.8, 12.6); s.bed(4.8, 12.6);
s.pencil(4.4, 10.6, "unused since '78", -1);
s.door(3.5, 4, false, 1, true); s.door(23.4, 4, false, 0.9, true);
s.door(7, 6, true, 1, true); s.door(19, 5, true, 1, true); s.door(19, 8.2, true, 1, true);
s.door(10, 10, false, 1); s.door(16.2, 10, false, 1);
s.door(3.2, 9, false, 1, true); s.door(2.5, 11, false, 0.8); s.door(5.2, 11, false, 0.8);
s.door(21.2, 7, false, 0.8, true); s.door(25.8, 7, false, 0.8, true);
}},
/* Level 2. Location 10, directly below the sick bay. */
{ key: "bh-creche", no: 5, title: "CRECHE & SCHOOL", level: "LEVEL 2 · WELFARE", draw: s => {
s.room(0.5, 1, 24, 3, LOWER, { cls: "rm2", ly: 1.3, lx: 9, entry: true });
s.stairs(24.5, 1, 3, 3, "UP");
s.text(20.6, 0.6, "UP TO LEVEL 1 · SHEET 4");
s.arch(24.5, 1.5, true, 2, "gap2");
s.exit(1, 3.4, "l", "← PIPE ROOM & PLANT · SHEET 6", { ty: 0.6 });
s.room(1, 4, 5, 4, "CLOAKS");
for (let y = 5.3; y < 7.8; y += 0.5) s.peg(1.3, y);
s.room(1, 8, 5, 5, "STAFF", { sub: "K. Marlow" });
s.bed(1.5, 10.2); s.box(3.4, 9.6, 1.8, 0.8, "DESK");
s.room(6, 4, 10, 7, "SCHOOLROOM", { n: 10 });
s.box(7, 5.2, 5, 0.25); s.text(7, 5.9, "BLACKBOARD");
s.box(12.8, 5.6, 2, 0.9, "TEACHER");
for (let x = 7; x < 15; x += 2) for (let y = 6.9; y < 10; y += 1.2) s.box(x, y, 1.3, 0.6);
s.pencil(12.4, 10.7, "atlas: 1971 edition", -2);
s.room(16, 4, 11.5, 7, "PLAYROOM");
s.raw(`<rect x="${p(16.2)}" y="${p(4.2)}" width="${p(11.1)}" height="16" fill="#e8f1fb" opacity=".5"/>`);
s.text(16.3, 5.4, "pinned drawings along this wall");
s.box(18, 6.6, 4, 2.6, "RUG", 20);
for (const [x, y] of [[23.5, 7], [25.4, 6.4], [24.6, 8.8], [17.2, 9.9]]) s.round(x, y, 0.35);
s.box(25.6, 9.6, 1.4, 1.1, "TOYS", 16);
s.room(6, 11, 14, 5, "DORMITORY", { sub: "under-twelves" });
for (let x = 6.6; x < 19.5; x += 0.95) { s.bed(x, 12.4, true, 0.7, 1.3); s.bed(x, 14.4, true, 0.7, 1.3); }
s.speaker(13.2, 11.4);
s.pencil(14.2, 12.1, "speaker fitted 2019 — 03:00 nightly", -1);
s.room(20, 11, 7.5, 5, "WASHROOM");
for (let x = 22; x < 27; x += 1.2) s.round(x, 12.9, 0.3);
s.box(21.4, 14.3, 5, 0.8, "BATHS");
s.door(3.8, 4, false, 1, true); s.door(6, 5, true, 1); s.door(16, 6.4, true, 1);
s.door(9.6, 11, false, 1); s.door(4, 8, false, 1); s.door(6, 11.6, true, 1, true);
s.door(20, 12.6, true, 1);
}},
/* Level 2. Locations 11 and 12, in the section's order: the pipe room under the Registry,
the plant under the Control Room. The pneumatic pipe comes down from the moor into the
pipe room; the incline goes down from the plant floor's north end to the north end of
the coal roads. */
{ key: "bh-plant", no: 6, title: "PIPE ROOM & PLANT", level: "LEVEL 2 · SERVICES", draw: s => {
s.room(0.5, 1, 27, 3, LOWER, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(27, 3.4, "r", "CRECHE · SHEET 5 →", { ty: 0.6 });
s.exit(1, 3.4, "l", "← DORMITORIES & CANTEEN · SHEETS 9–10", { ty: 0.6 });
s.room(0.5, 4, 19.5, 7, "PLANT FLOOR", { n: 12 });
s.box(15.6, 5.6, 3.4, 2.6, "COAL BOILER");
s.box(12.2, 5.6, 2.4, 2, "GEN 1"); s.box(9.2, 5.6, 2.4, 2, "GEN 2");
s.box(1.4, 5.4, 6.8, 0.6, "DUCTING");
s.box(5.4, 7.2, 2.4, 1.6, "PUMPS"); s.box(2.2, 7.2, 2.4, 1.6, "FILTERS");
s.pencil(9.2, 9.3, "coal up from No.2 road", -2);
s.room(20, 4, 7.5, 6, "PIPE ROOM", { n: 11, sub: "Centre's in-tray" });
s.round(23.8, 6.9, 0.55, "PIPE");
s.text(20.3, 8.2, "4\" pneumatic from moor ↑");
s.box(25.4, 6.3, 1.6, 1.4, "IN-TRAY", 15);
s.shelf(20.4, 9.1, 4, 0.5);
s.pencil(23.2, 5.7, "every 90 days", -2);
s.room(20, 10, 7.5, 5, "STORES", { sub: "issue counter" });
s.box(20.4, 11.9, 4.4, 0.6, "COUNTER");
s.shelf(20.4, 13.6, 6.2, 0.6); s.shelf(26.5, 10.5, 0.7, 2.6);
s.stairs(0.5, 11, 3, 4, "INCLINE");
s.text(0.4, 15.6, "↓ PIT GALLERIES · SHEET 7");
s.room(3.5, 11, 5, 4, "COAL HOPPER");
s.box(4.3, 12.4, 3.4, 2, "HOPPER");
s.room(8.5, 11, 11.5, 4.5, "TRANSMITTER", { sub: "feed to mast" });
s.shelf(9.5, 12.9, 5, 0.9); s.text(9.5, 14.3, "valve racks");
s.box(15.4, 12.3, 2.2, 1.4, "PANEL");
s.raw(`<line x1="${p(19.2)}" y1="${p(13.4)}" x2="${p(19.2)}" y2="${p(11.4)}" class="arw" marker-end="url(#ah)"/>`);
s.text(15.6, 15.2, "MAST CABLE ↑ MOOR");
s.door(3.4, 4, false, 1, true); s.door(14.6, 4, false, 1, true); s.door(24.6, 4, false, 1, true);
s.door(20, 8.6, true, 1, true); s.door(23.4, 10, false, 1);
s.arch(1, 11, false, 2); s.arch(4.5, 11, false, 3);
s.door(17.9, 11, false, 1);
}},
/* Pit galleries. Location 14 and the Buffer Depot, and location 15. Unlined coal roads;
only the Sump and the depot were ever built out. */
{ key: "bh-sump", no: 7, title: "COAL ROADS & SUMP", level: "PIT GALLERIES", draw: s => {
s.stairs(0.5, 1.5, 3, 3, "INCLINE", { entry: true });
s.text(0.3, 1.2, "↑ PLANT FLOOR · SHEET 6");
s.room(3.5, 1.5, 24, 3, "No.2 COAL ROAD", { cls: "rock", n: 14, sub: "growing beds under lamps", lx: 0.2 });
s.arch(3.5, 2, true, 2, "rgap");
for (let x = 8.8; x < 26; x += 2.2) { s.shelf(x, 1.8, 1.9, 0.55, "beds"); s.shelf(x, 3.65, 1.9, 0.55, "beds"); }
s.exit(27, 3, "r", "VIGIL HALL · SHEET 8 →", { ty: 1.2 });
s.room(9, 4.5, 2, 3, null, { cls: "rock" }); s.room(17, 4.5, 2, 3, null, { cls: "rock" });
s.room(11, 4.5, 6, 3, "BUFFER DEPOT", { sub: "tins · blankets · oil" });
s.shelf(11.4, 6.4, 5.2, 0.5);
s.room(0.5, 7.5, 18.5, 3, "No.3 COAL ROAD", { cls: "rock", sub: "trout tanks" });
s.exit(1, 10, "l", "← THE ESTATE · SHEET 11", { tx: 0.3, ty: 11 });
for (let x = 6; x < 16.5; x += 2.8) s.box(x, 8.1, 2.3, 1.8, "TANK", 16);
s.room(19, 6.5, 8.5, 5, "HEN RUN", { cls: "rock" });
for (let x = 20; x < 27; x += 1.3) s.box(x, 9.9, 0.9, 0.9);
s.pencil(20.3, 8.6, "eggs → Welfare, Tues/Fri", -2);
s.stairs(13, 10.5, 3, 4, "SUMP STAIR");
s.room(1, 11.5, 12, 2, "THE SUMP", { n: 15, ly: 1.3 });
for (const [i, x] of [1, 4, 7, 10].entries()) {
s.room(x, 13.5, 3, 4.5, `CELL ${i + 1}`, { lx: 0.2, ly: 0.9 });
s.bed(x + 0.5, 15.4);
s.door(x + 1.9, 13.5, false, 0.9);
}
s.box(8.8, 11.8, 2, 0.8, "WARDEN");
s.pencil(1.3, 18.7, "locks: 1962 pattern — oil them", -1);
s.arch(13.5, 10.5, false, 2, "rgap"); s.arch(13, 12, true, 1);
s.arch(9, 4.5, false, 2, "rgap"); s.arch(9, 7.5, false, 2, "rgap");
s.arch(17, 4.5, false, 2, "rgap"); s.arch(17, 7.5, false, 2, "rgap");
s.door(11, 5.2, true, 1, true); s.door(19, 8.5, true, 1);
}, legend: [17.5, 12.1] },
/* Pit galleries. Location 13: a hall lined out of a worked seam, for the whole Seat.
999 people standing need about 5,000 sq ft; this is 7,800. */
{ key: "bh-vigil", no: 8, title: "VIGIL HALL", level: "PIT GALLERIES", draw: s => {
s.room(0.5, 7, 2, 4, null, { cls: "rock", entry: true });
s.text(0.2, 6.6, "← SHEET 7");
s.room(2.5, 1, 24.5, 14, "VIGIL HALL", { n: 13, ly: 13.6 });
s.box(8, 1.4, 12, 1.9, "PLATFORM", 22);
s.box(9, 1.05, 10, 0.22); s.text(20.3, 1.45, "ROLL OF HONOUR");
s.box(13.6, 3.5, 0.8, 0.5); s.text(14.6, 3.95, "lectern");
for (let y = 5; y < 10.5; y += 0.8) for (let x = 4.5; x < 23.8; x += 0.55) {
if (x > 13.5 && x < 14.6) continue;
s.chair(x, y);
}
for (const y of [4.6, 11.4]) s.speaker(2.9, y);
for (const x of [18.5, 20.3, 22.1]) s.box(x, 12.2, 1.4, 1.4, "STACK", 14);
s.pencil(6, 12.4, "Thursday: chairs out — all 999 stand", -2);
s.text(15.6, 14.4, "standing room");
s.door(2.5, 7.6, true, 1.8);
s.door(27, 7.5, true, 1, true, { out: true });
s.text(23.2, 7.2, "fire exit → galleries");
}},
/* Level 2, north of the plant, under the lobby. Location 16: the 1962 dormitories, built
for the whole intake and now for the single and the young. Double bunks, 60 berths a hall. */
{ key: "bh-dorms", no: 9, title: "DORMITORIES", level: "LEVEL 2 · QUARTERS", draw: s => {
s.room(0.5, 1, 27, 3, LOWER, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(27, 3.4, "r", "PLANT & PIPE ROOM · SHEET 6 →", { ty: 0.6 });
s.exit(1, 3.4, "l", "← CANTEEN & QUARTERS · SHEET 10", { ty: 0.6 });
["A", "B", "C", "D"].forEach((h, i) => {
const x = 0.5 + i * 5.5;
s.room(x, 4, 5.5, 11, `HALL ${h} · 60`, { ly: 10.75, n: i === 0 ? 16 : undefined });
for (const y of [5.1, 7.4, 8.7, 11, 12.3]) for (let k = 0; k < 6; k++) s.bed(x + 0.2 + k * 0.72, y, true, 0.6, 1.3);
s.door(x + 4.45, 4, false, 0.9, true);
});
s.pencil(6.3, 10.65, "bunks by age — see Registry", -1);
s.room(22.5, 4, 5, 5.5, "ABLUTIONS");
for (let x = 23; x < 27; x += 0.9) s.round(x + 0.3, 5.5, 0.28);
for (let x = 22.8; x < 27; x += 1.1) s.box(x, 7.9, 0.9, 1.3, "WC", 13);
s.pencil(22.8, 6.9, "hot water Tues/Fri", -2);
s.room(22.5, 9.5, 5, 5.5, "LAUNDRY");
s.box(22.9, 10.9, 1.8, 1.2, "COPPERS", 14); s.box(25.3, 10.9, 1.8, 1.2, "MANGLE", 14);
s.shelf(23, 13.4, 4.2, 0.6, "drying racks"); s.text(23, 14.7, "drying racks");
s.door(25.2, 4, false, 1, true); s.door(25.3, 9.5, false, 0.9);
s.door(22.5, 12.2, true, 0.8);
}},
/* Level 2, the north end. Location 17: the canteen feeds the Seat in four sittings, and the
officers' cabins hold the Controller, the doctor and the oldest of the 1974 intake. */
{ key: "bh-canteen", no: 10, title: "CANTEEN & QUARTERS", level: "LEVEL 2 · QUARTERS", draw: s => {
s.room(0.5, 1, 27, 3, LOWER, { cls: "rm2", ly: 1.3, lx: 11, entry: true });
s.exit(27, 3.4, "r", "DORMITORIES · SHEET 9 →", { ty: 0.6 });
s.room(0.5, 4, 13.5, 11, "CANTEEN", { n: 17 });
for (const y of [5.6, 7.5, 9.4, 11.3, 13.2]) for (const x of [1.2, 7.2]) {
s.box(x, y, 5.2, 0.6);
for (let k = 0; k < 11; k++) { s.chair(x + 0.12 + k * 0.46, y - 0.4); s.chair(x + 0.12 + k * 0.46, y + 0.66); }
}
s.pencil(1, 14.8, "four sittings — 6, 7, 8 and 9 o'clock", -1);
s.room(14, 4, 6, 7, "KITCHEN");
s.box(14.2, 5.5, 0.6, 5, "", 12); s.text(14.95, 10.6, "servery");
s.box(16.4, 5.2, 3.2, 1, "RANGES"); s.box(16.4, 6.8, 3.2, 1.2, "COPPERS");
s.box(15.8, 8.8, 3.6, 0.8, "PREP");
s.arch(14, 5.5, true, 5);
s.room(14, 11, 6, 4, "BAKERY");
s.box(14.6, 12.6, 2.2, 1.4, "OVEN"); s.shelf(18.6, 12.4, 1, 2.2, "flour");
s.room(20, 4, 3.25, 2.2, null); s.room(20, 6.2, 3.25, 2.2, null); s.room(20, 8.4, 3.25, 2.2, null);
s.room(20, 10.6, 3.25, 2.2, null); s.room(20, 12.8, 3.25, 2.2, null);
s.room(23.25, 4, 1, 11, null, { cls: "rm2" });
s.room(24.25, 4, 3.25, 2.2, null); s.room(24.25, 6.2, 3.25, 2.2, null); s.room(24.25, 8.4, 3.25, 2.2, null);
s.room(24.25, 10.6, 3.25, 2.2, null); s.room(24.25, 12.8, 3.25, 2.2, null);
[4, 6.2, 8.4, 10.6, 12.8].forEach((y, i) => {
s.bed(20.3, y + 0.2, true, 0.9, 1.8); s.box(21.6, y + 0.2, 0.7, 0.6);
s.door(23.25, y + 1.1, true, 0.8, true); s.text(21.3, y + 1.95, `C${2 * i + 1}`);
s.bed(26.3, y + 0.2, true, 0.9, 1.8); s.box(25.3, y + 0.2, 0.7, 0.6);
s.door(24.25, y + 1.1, true, 0.8); s.text(25.2, y + 1.95, `C${2 * i + 2}`);
});
s.text(20.1, 15.5, "OFFICERS' CABINS · 2 BERTHS EACH");
s.pencil(20.2, 16.1, "C1 — Controller", -1);
s.arch(23.25, 4, false, 1, "gap2");
s.door(2, 4, false, 1, true); s.door(11, 4, false, 1, true); s.door(18.4, 4, false, 1, true);
s.door(17.2, 11, false, 1);
}, legend: [0.3, 16.1] },
...[11, 12, 13].map(no => estate(no))
];
/* The Estate: location 18. Three coal roads west of No.3 road, lined by the residents with
family dwellings, ten feet by ten, from 1979 on. Each sheet is one stretch of the same
three roads; the middle road carries you from sheet to sheet. Houses are numbered as on
a street, odd on the north side of a road and even on the south, running on across the
three sheets. */
function estate(no) {
const part = { 11: "SOUTH", 12: "MIDDLE", 13: "NORTH" }[no];
const offset = { 11: 0, 12: 13, 13: 26 }[no];
return { key: `bh-estate-${part.toLowerCase()}`, no, title: `THE ESTATE · ${part}`, level: "PIT GALLERIES", draw: s => {
const bands = [0.4, 5.6, 10.8];
const roads = ["No.4 ROAD", "No.5 ROAD", "No.6 ROAD"];
const crossX = 13;
bands.forEach((y0, b) => {
s.room(0.5, y0 + 2, 27, 1, roads[b], { cls: "rock", ly: 0.75, lx: b === 1 && no < 13 ? 0.8 : 0.2, entry: b === 1, n: b === 0 && no === 11 ? 18 : undefined });
for (let x = 1; x < 27; x += 2) {
for (const side of ["top", "bottom"]) {
if (x === crossX && ((side === "bottom" && b < 2) || (side === "top" && b > 0))) continue;
const y = side === "top" ? y0 : y0 + 3;
const k = offset + (25 - x) / 2 + 1; // numbered from the No.3 road end, walking in
const tag = `${4 + b}/${String(side === "top" ? 2 * k - 1 : 2 * k).padStart(2, "0")}`;
const facility = (side === "top" && x === 25 && b === 0) ? "PRIVY"
: (side === "bottom" && x === 1 && b === 2) ? "PRIVY"
: (side === "top" && x === 25 && b === 2) ? "TAP" : null;
s.room(x, y, 2, 2, null);
if (side === "top") {
s.door(x + 1.1, y + 2, false, 0.8, true);
if (facility) s.box(x + 0.2, y + 0.2, 0.8, 0.8, facility, 12);
else { s.bed(x + 0.15, y + 0.15, true, 0.6, 1.3); s.box(x + 1.1, y + 0.2, 0.7, 0.5); s.text(x + 0.15, y + 1.85, tag); }
} else {
s.door(x + 1.1, y, false, 0.8);
if (facility) s.box(x + 0.2, y + 1, 0.8, 0.8, facility, 12);
else { s.bed(x + 0.15, y + 0.55, true, 0.6, 1.3); s.box(x + 1.1, y + 1.2, 0.7, 0.5); s.text(x + 0.15, y + 0.4, tag); }
}
}
}
if (b < 2) {
s.room(crossX, y0 + 3, 2, bands[b + 1] + 2 - (y0 + 3), null, { cls: "rock" });
s.arch(crossX, y0 + 3, false, 2, "rgap"); s.arch(crossX, bands[b + 1] + 2, false, 2, "rgap");
}
});
const mid = bands[1] + 2.5, line = bands[1] + 2.75;
if (no === 11) s.exit(27, mid, "r", "No.3 ROAD · SHEET 7 →", { ty: line, tx: 24.2 });
else s.exit(27, mid, "r", `SHEET ${no - 1} →`, { ty: line, tx: 25.5 });
if (no < 13) s.exit(0.9, mid, "l", `← SHEET ${no + 1}`, { ty: line, tx: 3.4 });
else s.pencil(3.5, bands[2] + 2.8, "west face: worked out 1958", -1);
if (no === 11) s.pencil(16, bands[0] + 2.85, "lined by residents from 1979 · see housing list", -1);
}};
}
/* ---------------------------------------------------------------- the check */
const EPS = 0.02;
const inside = (a, b, t = EPS) => a.x >= b.x - t && a.y >= b.y - t && a.x + a.w <= b.x + b.w + t && a.y + a.h <= b.y + b.h + t;
const overlap = (a, b, t = EPS) => a.x < b.x + b.w - t && b.x < a.x + a.w - t && a.y < b.y + b.h - t && b.y < a.y + a.h - t;
const holds = (r, x, y) => x > r.x && x < r.x + r.w && y > r.y && y < r.y + r.h;
const area = r => r.w * r.h;
const smallest = (rooms, x, y) => rooms.filter(r => holds(r, x, y)).sort((a, b) => area(a) - area(b))[0] ?? null;
const onEdge = (r, d) => d.v
? (Math.abs(r.x - d.x) < EPS || Math.abs(r.x + r.w - d.x) < EPS) && d.y >= r.y - EPS && d.y + d.len <= r.y + r.h + EPS
: (Math.abs(r.y - d.y) < EPS || Math.abs(r.y + r.h - d.y) < EPS) && d.x >= r.x - EPS && d.x + d.len <= r.x + r.w + EPS;
export function checkPlan(plan) {
const { s, legend } = build(plan);
const { rooms, doors, items, labels } = s.geo;
const out = [];
const where = d => `${d.swing ? "door" : "opening"} at (${d.x},${d.y})`;
const furniture = items.filter(i => i.kind !== "number");
const reserved = [
{ ...TITLE_BLOCK, name: "title block" },
{ x: legend[0], y: legend[1], ...LEGEND, name: "key" }
];
// Rooms: none crosses another; none sits on the key or title block.
for (let i = 0; i < rooms.length; i++) {
for (let j = i + 1; j < rooms.length; j++) {
const a = rooms[i], b = rooms[j];
if (overlap(a, b) && !inside(a, b) && !inside(b, a)) out.push(`rooms ${a.name} and ${b.name} overlap`);
}
for (const z of reserved) if (overlap(rooms[i], z)) out.push(`room ${rooms[i].name} runs under the ${z.name}`);
}
// Doors: on a shared wall, between two different spaces, swinging into one of them clear.
const edges = [];
for (const d of doors) {
const m = d.len / 2;
const [ax, ay, bx, by] = d.v ? [d.x - 0.05, d.y + m, d.x + 0.05, d.y + m] : [d.x + m, d.y - 0.05, d.x + m, d.y + 0.05];
const A = smallest(rooms, ax, ay), B = smallest(rooms, bx, by);
if (d.out) {
if (!A === !B) out.push(`${where(d)} is marked as leaving the building but has space on ${A && B ? "both sides" : "neither side"}`);
if (!(A && onEdge(A, d)) && !(B && onEdge(B, d))) out.push(`${where(d)} is not on a wall`);
continue;
}
if (!A || !B) { out.push(`${where(d)} opens onto solid rock (${A?.name ?? "nothing"} | ${B?.name ?? "nothing"})`); continue; }
if (A === B) { out.push(`${where(d)} is not on a wall — ${A.name} on both sides`); continue; }
if (!onEdge(A, d) && !onEdge(B, d)) out.push(`${where(d)} between ${A.name} and ${B.name} is not on either room's wall`);
edges.push([A, B]);
if (!d.swing) continue;
const into = d.d < 0 ? A : B;
const sw = d.v ? { x: d.d < 0 ? d.x - d.len : d.x, y: d.y, w: d.len, h: d.len }
: { x: d.x, y: d.d < 0 ? d.y - d.len : d.y, w: d.len, h: d.len };
if (!inside(sw, into)) out.push(`${where(d)} swings out of ${into.name}`);
for (const i of furniture) if (overlap(sw, i)) out.push(`${where(d)} swings into ${i.kind} at (${i.x},${i.y}) in ${into.name}`);
for (const l of labels) if (overlap(sw, l)) out.push(`${where(d)} swings across the lettering "${l.text}"`);
for (const e of doors) if (e !== d && e.swing) {
const sw2 = e.v ? { x: e.d < 0 ? e.x - e.len : e.x, y: e.y, w: e.len, h: e.len } : { x: e.x, y: e.d < 0 ? e.y - e.len : e.y, w: e.len, h: e.len };
if (overlap(sw, sw2, 0.05) && (d.x < e.x || (d.x === e.x && d.y < e.y))) out.push(`${where(d)} and the door at (${e.x},${e.y}) swing into each other`);
}
}
// Furniture: wholly in one room, clear of other furniture and of lettering.
for (const i of items) {
const r = smallest(rooms, i.x + i.w / 2, i.y + i.h / 2);
if (!r) { out.push(`${i.kind} at (${i.x},${i.y}) is outside every room`); continue; }
if (!inside(i, r)) out.push(`${i.kind} at (${i.x},${i.y}) crosses the wall of ${r.name}`);
const inner = rooms.find(q => q !== r && inside(q, r) && overlap(i, q));
if (inner) out.push(`${i.kind} at (${i.x},${i.y}) in ${r.name} overlaps the room ${inner.name}`);
}
for (let a = 0; a < items.length; a++) for (let b = a + 1; b < items.length; b++) {
const i = items[a], j = items[b];
if (overlap(i, j)) out.push(`${i.kind} at (${i.x},${i.y}) overlaps ${j.kind} at (${j.x},${j.y})`);
}
for (const l of labels) {
for (const i of furniture) if (overlap(l, i, 0.05)) out.push(`the lettering "${l.text}" runs over ${i.kind} at (${i.x},${i.y})`);
for (const z of reserved) if (overlap(l, z)) out.push(`the lettering "${l.text}" runs under the ${z.name}`);
if (l.x + l.w > W || l.x < 0) out.push(`the lettering "${l.text}" runs off the sheet`);
if (!smallest(rooms, l.x + 0.05, l.y + l.h / 2)) {
for (const q of rooms) if (overlap(l, q)) out.push(`the lettering "${l.text}" starts outside and runs into ${q.name}`);
}
const r = smallest(rooms, l.x + 0.05, l.y + l.h / 2);
if (r && l.kind !== "pen" && l.x + l.w > r.x + r.w + EPS) out.push(`the lettering "${l.text}" runs out of ${r.name}`);
}
// Every room is reachable from the way in.
const start = rooms.filter(r => r.entry);
if (!start.length) out.push("the sheet has no way in");
const seen = new Set(start);
const parent = r => rooms.filter(q => q !== r && inside(r, q)).sort((a, b) => area(a) - area(b))[0];
let grew = true;
while (grew) {
grew = false;
for (const [a, b] of edges) {
if (seen.has(a) !== seen.has(b)) { seen.add(a); seen.add(b); grew = true; }
}
}
for (const r of rooms) if (!seen.has(r)) out.push(`${r.name} cannot be reached from the way in`);
return [...new Set(out)];
}
if (import.meta.url === `file://${process.argv[1]}`) {
const argv = process.argv.slice(2);
if (argv.includes("--check")) {
let bad = 0;
for (const pl of PLANS) {
const problems = checkPlan(pl);
bad += problems.length;
console.log(` sheet ${pl.no} ${pl.key}: ${problems.length ? problems.length + " problem(s)" : "OK"}`);
problems.forEach(x => console.log(` - ${x}`));
}
if (PLANS.length !== SHEETS) { console.log(` ${PLANS.length} sheets drawn, ${SHEETS} numbered`); bad++; }
process.exit(bad ? 1 : 0);
}
const doRender = argv.includes("--render");
for (const pl of PLANS) {
const svgPath = path.join(OUT, `${pl.key}.svg`);
writeFileSync(svgPath, render(pl));
if (doRender) {
const png = path.join(OUT, `${pl.key}.png`);
execFileSync("rsvg-convert", ["-w", "2800", "-h", "2000", svgPath, "-o", png]);
execFileSync("cwebp", ["-quiet", "-q", "88", png, "-o", path.join(OUT, `${pl.key}.webp`)]);
execFileSync("rm", [png]);
}
console.log(` ${pl.key}${doRender ? " (+webp)" : ""}`);
}
}