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>
769 lines
42 KiB
JavaScript
769 lines
42 KiB
JavaScript
/**
|
||
* 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
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)" : ""}`);
|
||
}
|
||
}
|