/**
 * Lumbridge HQ — the reference office pack, and the file you copy.
 *
 * Pure data against `src/interiors/types.ts`. One level, fifteen rooms, three
 * hundred props and seventy-six seats, on a 34 x 18 m floor plate: a
 * lobby, an open desk floor, three meeting rooms at three different sizes, a
 * strip of focus booths, a kitchen, a lounge, a quiet room, a workshop, a server
 * room, a store, and the circulation that ties them together.
 *
 * **This file is the dev kit.** It is not here because Lumbridge needs an office
 * in a repo; it is here because a stranger who clones this should be able to
 * open one file, recognise a floor plan, change six numbers and have their own
 * office. So the dimensions are real ones — 2.8 m ceilings, 0.9 m doors, 1.6 m
 * desks, 1.8 m corridors — and the reasoning is on the page next to the numbers
 * rather than in a design document nobody reads. `src/offices/README.md` is the
 * companion: it explains the format, this explains the choices.
 *
 * The engine still knows nothing. A room called "Alcatraz" is a polygon with a
 * label, `colorKey: "focus"` is an opaque string, and seat `eng-04` is a spot on
 * the floor with a name. Nothing here tells the renderer that anybody works in
 * this building — see ARCHITECTURE.md §3.3, and the note on `Presence` in
 * `interiors/types.ts`, which is the same rule one level in.
 *
 * ### Two audiences, and why this pack barely uses the second one
 *
 * A handful of items below carry `audience: "private"`, which means a public
 * build — `createOfficeScene(office, { depth: "public" })`, the office an
 * anonymous visitor gets — does not construct them. There are seven of them: two
 * zones and five objects in the server room. Everything else in this file is
 * built at both depths, and that is the honest answer for this pack rather than
 * a gap in it. The difference between the public building and the signed-in one
 * is **the people**, and the people are not in this file at all.
 *
 * That is the whole reason a pack can be published. `Presence` binds by seat id
 * and arrives from somewhere else, so the geometry and the occupancy are two
 * separate acts and only one of them is happening here. Read the note on
 * `Audience` in `interiors/types.ts` before reaching for the field, especially
 * the part that says it is a UI tier and not a security boundary: **this file is
 * bundled into the static build**, so a self-hoster who marks their real floor
 * plan private has published their real floor plan with an extra step. The
 * building here is fabricated. If yours is not, the thing that keeps a stranger
 * out is your API, not this field.
 *
 * ### The coordinate frame
 *
 * Metres, `1 unit = 1 m`, the floor on the XZ plane with +Y up. The origin is
 * the **north-west corner** of the slab, +X runs east and +Z runs south, so in
 * plan view — looking down, +X to the right — +Z goes *down the page* and the
 * glazed façade along `z = 0` is at the top.
 *
 * Calling one edge "north" is a convenience for reading this file and nothing
 * more. An `Office` has no orientation on the earth and no sun; it is not a
 * city. If you rotate the whole building in your head, the only thing that
 * changes is the vocabulary in these comments.
 *
 * ### Which way things face
 *
 * `Yaw` is `object.rotation.y`: zero points at −Z, and the angle increases
 * counter-clockwise seen from above. An asset's **front is at −Z and its working
 * side at +Z** (`src/assets/office/common.ts`), which combine into one rule that
 * covers every prop in this file:
 *
 * > A prop takes the yaw of *the wall it backs onto*, and a seat takes the yaw
 * > of *the direction the occupant looks*.
 *
 * So a shelf against the north wall is `NORTH`, a locker against the east wall
 * is `EAST`, and someone at a desk that also reads `NORTH` is looking north at
 * it. Both halves fall out of the same convention, which is why a `DeskBank`
 * can hand one `rotation` to a desk, its chair and the seat between them.
 */

import type {
  AssetId,
  Audience,
  DeskBank,
  Level,
  Office,
  Opening,
  Outline,
  Point2,
  Prop,
  Room,
  Seat,
  Viewpoint,
  Wall,
  Yaw,
  Zone,
} from "../interiors/types.ts";
import { LUMBRIDGE_HQ_SITE } from "./sites.ts";

// ---- The floor plate ------------------------------------------------------

/**
 * The building, in a handful of numbers. Everything else in this file is
 * measured off these, so a self-hoster with a different slab starts here.
 *
 * **The plate is in two pieces and they are different kinds of thing.**
 *
 * `BLOCK_E` at 34 m is the east edge of the original block — 34 x 18 m, 612 m²,
 * a plausible single floor of a mid-rise: deep enough for a row of rooms behind
 * a corridor, shallow enough that the desks still get daylight. Go much deeper
 * and the middle of the plate becomes a place nobody wants to sit, which is a
 * real constraint and not a rendering one. Every room, wall and seat authored
 * before the wing existed is measured against this line, which is why it keeps a
 * name of its own rather than being folded into `WIDTH`.
 *
 * `WIDTH` at 48 m is the whole plate, block plus wing. The 14 m east of the
 * block is one room — see `COMMONS` — and it is deliberately not laid out on the
 * block's grid. A second row of cellular offices would have added area without
 * adding anything you could feel; a double-height volume you walk into changes
 * what the building is like to be in, which is the thing that was actually
 * wanted.
 */
const BLOCK_E = 34.0;
const WIDTH = 48.0;
const DEPTH = 18.0;

/** Floor to underside of ceiling. 2.8 m is an ordinary commercial storey. */
const CEILING = 2.8;

/**
 * The gap between one storey's ceiling and the next one's floor.
 *
 * In a real building this is 1.4 m of structure, ducts, trays and raised floor,
 * and you never see it. Here it is **ten times that**, and the exaggeration is
 * deliberate and is the whole reason it has a name of its own.
 *
 * The brief was to be able to see the space between the floors and eventually to
 * run things through it — cable trays, fibre, the drops that land on the ceiling
 * of the floor below and the floor of the one above. At 1.4 m there is nowhere
 * to put any of that and nothing to look at; at 14 m the interstitial is a
 * storey in its own right, the two slabs read as two separate places, and the
 * commons becomes a genuine shaft rather than a tall room.
 *
 * It is frankly not architecture. A 16.8 m floor-to-floor is a hangar, the stair
 * in the east wing is no longer a stair anybody could climb, and the gallery
 * looks down from the height of a five-storey building. That is an accepted
 * cost of a deliberately unreal building, and **this is the one number to change
 * if it reads as too much** — everything else in the file is measured off it.
 */
const PLENUM = 14.0;

/**
 * Floor to floor. Not the same as `CEILING` and the difference is not slack: it
 * is the room plus the interstitial above it.
 *
 * `Plan` adds this to every coordinate on level 2 exactly once, so both levels
 * are authored in their own floor's frame and neither has to know about the
 * other.
 */
const STOREY = CEILING + PLENUM;

/**
 * The commons is open to both storeys, so its walls are two rooms tall.
 *
 * Stated as the sum rather than as `7.0` because it *is* the sum: change the
 * storey height and the void has to follow, and a hard-coded number here would
 * be the one thing that quietly did not.
 */
const COMMONS_HEIGHT = STOREY + CEILING;

/**
 * The booths get a lower lid than the room they stand in. That is what makes a
 * booth feel like a booth rather than a cupboard with the top cut off, and it
 * is the reason `Room.ceiling` is a per-room override at all.
 */
const BOOTH_CEILING = 2.3;

/**
 * Exterior walls are thicker than partitions and say so; the level's default
 * (0.12 m) covers the thirty-odd internal ones. A wall that does not name a
 * thickness inherits the level's, which is the whole point of the defaults —
 * the interesting wall should be the one that stands out in the source.
 */
const EXT_THICKNESS = 0.25;
const INT_THICKNESS = 0.12;

/** Half-thicknesses, used constantly to sit a prop against a wall face. */
const EXT_FACE = EXT_THICKNESS / 2;
const INT_FACE = INT_THICKNESS / 2;

/**
 * How far the centre of a wall-hung panel sits off the wall's face.
 *
 * `tera:screen.wall-display` is 0.12 m deep and `tera:whiteboard` 0.10 m, and a
 * prop's origin is the centre of its footprint — so half the depth plus a
 * millimetre of reveal is what makes a display hang *on* a wall rather than
 * half inside it. Worth naming rather than sprinkling 0.07 through the file,
 * because the moment somebody registers a thicker panel over `overrides:` these
 * are the two numbers that have to move with it.
 */
const DISPLAY_OFFSET = 0.07;
const BOARD_OFFSET = 0.06;

// The lines the plan is built on. Rooms and walls are authored against the same
// numbers on purpose: a wall is centred on its line and straddles the boundary
// between the two floor slabs that meet there, so if the slab edge and the wall
// line disagree by a centimetre you get a seam you will never find again.
const SPINE_N = 10.4; // open floor / lobby / kitchen give way to the corridor
const SPINE_S = 12.2; // corridor gives way to the south rooms
const LOBBY_E = 7.2; // lobby / open desk floor
const SOCIAL_W = 25.6; // open desk floor / lounge and kitchen
const KITCHEN_N = 6.0; // lounge / kitchen, an open boundary with no wall on it
const BOOTH_N = 8.6; // front of the focus booths

// South-room divisions, west to east. These are the only widths in the file
// that were chosen by what has to fit inside rather than by a grid.
const X_ALCATRAZ = 0.0;
const X_BERNAL = 7.2;
const X_BOLINAS = 12.4;
const X_QUIET = 16.0;
const X_WORKSHOP = 19.6;
const X_SERVER = 26.4;
const X_STORE = 30.2;

// Booth divisions. Two metres each, sharing party walls, backing onto the
// corridor wall — three booths out of eleven metres of otherwise dead frontage.
const X_BOOTH_1 = 8.4;
const X_BOOTH_2 = 10.4;
const X_BOOTH_3 = 12.4;
const X_BOOTH_E = 14.4;

// -- The east wing ----------------------------------------------------------
//
// Three lines, and the wing needs no more because it is one room. The stair is
// the only enclosure in it, tucked into the south-east corner where it takes
// daylight from nothing.
const STAIR_W = 42.8; // stair core / commons floor
const STAIR_N = 12.6; // stair core / commons floor
/**
 * How far level 2's gallery reaches out over the commons.
 *
 * 4.2 m of balcony, which is enough to stand two people and a rail at without
 * being a floor — past about six the void stops reading as a void and starts
 * reading as a corridor with a hole in it, and the whole point of the wing is
 * the volume you can see all of at once.
 */
const GALLERY_E = BLOCK_E + 4.2;

// ---- Yaw ------------------------------------------------------------------

/**
 * The four right angles, named for the direction a prop's *front* points.
 *
 * Reading them takes the one rule from the header: a thing standing against a
 * wall points its front at that wall, so `NORTH` is both "the shelf on the north
 * wall" and "the person looking north". There is no sign flip anywhere between
 * here and `object.rotation.y`.
 */
const NORTH: Yaw = 0;
const EAST: Yaw = -Math.PI / 2;
const SOUTH: Yaw = Math.PI;
const WEST: Yaw = Math.PI / 2;

// ---- Assets ---------------------------------------------------------------

/**
 * Every asset this pack places, named once.
 *
 * They are all `tera:` ids from `src/assets/office/`. Bind them to constants
 * rather than repeating the strings, because the day you swap every workstation
 * in the building for `acme:desk.standing` should be a one-line day — and if it
 * is more than one line, you wanted the `overrides:` mechanism instead, which
 * reskins `tera:desk.workstation` itself and needs no edit here at all. The
 * README's "Registering your own assets" section is the longer version.
 */
const DESK: AssetId = "tera:desk.workstation";
const PEDESTAL: AssetId = "tera:desk.pedestal";
const PARTITION: AssetId = "tera:desk.partition";
const TASK_CHAIR: AssetId = "tera:seat.task-chair";
const LOUNGE_CHAIR: AssetId = "tera:seat.lounge";
const MEETING_TABLE: AssetId = "tera:table.meeting";
const SIDE_TABLE: AssetId = "tera:table.side";
const SHELF: AssetId = "tera:storage.shelf";
const LOCKER: AssetId = "tera:storage.locker";
const MONITOR: AssetId = "tera:screen.monitor";
const DISPLAY: AssetId = "tera:screen.wall-display";
const PLANT: AssetId = "tera:plant.potted";
const TREE: AssetId = "tera:plant.tall";
const PENDANT: AssetId = "tera:light.pendant";
const TROFFER: AssetId = "tera:light.troffer";
const RUG: AssetId = "tera:rug";
const WHITEBOARD: AssetId = "tera:whiteboard";

/**
 * Surface ids, which are looser than they look.
 *
 * `MaterialRegistry.resolve` throws away the namespace and reads the **first
 * dot-segment** as a role, through a small alias table, and falls back rather
 * than throwing. So `tera:wood.plank`, `acme:wood.reclaimed` and a bare `wood`
 * are all the `woodFloor` role, and a typo renders in the fallback material
 * instead of failing the build. Naming them here keeps the whole palette of the
 * building in one screen, which is what you actually want when you decide the
 * meeting rooms should have carpet after all.
 */
const WOOD = "tera:wood.plank";
const CARPET = "tera:carpet.loop";
const CARPET_ACCENT = "tera:carpetAccent.broadloom";
const CONCRETE = "tera:concrete.polished";
const TERRAZZO = "tera:tile.terrazzo";
const RAISED_FLOOR = "tera:floorSlab.raised";
const PAINT = "tera:paint.matt";
const ACCENT_PAINT = "tera:plasterAccent.deep";
const GLASS = "tera:glass.curtain";
const FELT = "tera:felt.acoustic";
const CEILING_TILE = "tera:ceiling.tile";

// ---- Authoring helpers ----------------------------------------------------

/*
 * Five functions, and they all run at module load and return plain objects.
 *
 * The exported `Office` is still exactly what `interiors/types.ts` demands —
 * JSON-serialisable data with no functions in it — and would survive
 * `JSON.parse(JSON.stringify(…))` unchanged. These exist because the
 * alternative is four hundred literals in which every seventh one has a typo:
 * a floor of ceiling lights is a grid, a bench of monitor arms is a line, and
 * the winding rule for an outline is the sort of thing a human gets right
 * fourteen times and wrong on the fifteenth.
 *
 * A pack shipped as `.json` spells all of this out. That is fine; it is a build
 * product of a file like this one.
 */

/**
 * An axis-aligned room outline, wound counter-clockwise **in plan view**.
 *
 * Worth being precise about, since it is the one place the frame bites: plan
 * view has +Z going down the page, so a polygon that looks clockwise on paper
 * has a *negative* shoelace area over (x, z). Going NW → SW → SE → NE, as this
 * does, is the counter-clockwise one. `Plan` silently re-winds anything that
 * arrives the other way round, so getting it wrong costs nothing — but getting
 * it right means the outlines in `plan.problems` are the ones with real
 * problems.
 */
function rect(x0: number, z0: number, x1: number, z1: number): Outline {
  return [
    { x: x0, z: z0 },
    { x: x0, z: z1 },
    { x: x1, z: z1 },
    { x: x1, z: z0 },
  ];
}

/**
 * A regular grid of points, columns first. `rows: 1` gives a line, which is how
 * most of the calls below use it.
 */
function grid(
  x0: number,
  z0: number,
  columns: number,
  rows: number,
  dx: number,
  dz: number,
): Point2[] {
  const points: Point2[] = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < columns; c++) {
      points.push({ x: x0 + c * dx, z: z0 + r * dz });
    }
  }
  return points;
}

/**
 * One asset at each of `points`, with ids `${prefix}-01`, `-02`, …
 *
 * Same numbering scheme as `DeskBank` expansion, deliberately: two digits from
 * one, in the order the points were generated, so a prop id is something you can
 * work out on paper instead of by running the code.
 */
function scatter(
  prefix: string,
  kind: AssetId,
  points: Point2[],
  opts: { rotation?: Yaw; elevation?: number; colorKey?: string; audience?: Audience } = {},
): Prop[] {
  return points.map((position, i) => ({
    id: `${prefix}-${String(i + 1).padStart(2, "0")}`,
    kind,
    position,
    rotation: opts.rotation ?? NORTH,
    elevation: opts.elevation,
    colorKey: opts.colorKey,
    // A run of identical props is a run of identical props at both depths.
    // Nothing in the format stops a pack marking one rack of four private, but
    // half a row is a stranger thing to look at than no row.
    audience: opts.audience,
  }));
}

/**
 * A doorway. 0.9 m x 2.1 m is a standard single leaf; the wide ones in this
 * building say so at the call site.
 *
 * An opening with `sill: 0` that clears 1.1 m of head is what `Plan` treats as
 * walkable, so this is also the thing that punches the gap in the collision
 * segments. That is the whole reason doors are openings and not props.
 */
function doorway(start: number, width = 0.9, head = 2.1): Opening {
  return { kind: "door", start, width, sill: 0, head };
}

/**
 * A window. The sill is what keeps it out of the collider — an opening a walker
 * could step through is a door whatever you call it, so a glazed hole at
 * ankle height would quietly become a hole in the wall you can walk through.
 */
function pane(start: number, width: number, sill = 0.9, head = 2.2): Opening {
  return { kind: "window", start, width, sill, head };
}

/** A cased opening with no leaf in it — the way you get from A to B indoors. */
function archway(start: number, width: number, head = 2.4): Opening {
  return { kind: "arch", start, width, sill: 0, head };
}

// ---- Rooms ----------------------------------------------------------------

/**
 * The fifteen slabs.
 *
 * A `Room` is a floor finish with a name and **implies no walls** — the walls
 * are the separate list below, and the two are not derived from each other.
 * That means an open plan is simply rooms with nothing standing between them:
 * the lounge, the kitchen and the desk floor share three edges and only one of
 * those edges carries a wall.
 *
 * `ceiling: null` means no ceiling at all, and nearly every room here declares
 * it. The reason is the establishing viewpoint: an office you look down into
 * cannot have lids on the rooms you are trying to look into. The booths, the
 * server room and the store keep theirs, because those are the three rooms you
 * are never meant to see inside, and a ceiling is a cheap way of saying so.
 *
 * Order matters in one respect: `Plan.roomAt` resolves later rooms first, so a
 * room laid on top of another wins the lookup. Nothing here overlaps — the open
 * floor is notched around the booths rather than passing underneath them, since
 * two coplanar slabs at y = 0 is a z-fight waiting for the wrong GPU.
 */
const ROOMS: Room[] = [
  // -- North band: the daylit half ------------------------------------------
  {
    id: "lobby",
    name: "Reception",
    outline: rect(0, 0, LOBBY_E, SPINE_N),
    floor: WOOD,
    ceiling: null,
  },
  {
    id: "open-floor",
    name: "The Floor",
    // Notched around the booth block on its southern edge. Eight points rather
    // than four, and the only non-rectangular room in the building.
    outline: [
      { x: LOBBY_E, z: 0 },
      { x: LOBBY_E, z: SPINE_N },
      { x: X_BOOTH_1, z: SPINE_N },
      { x: X_BOOTH_1, z: BOOTH_N },
      { x: X_BOOTH_E, z: BOOTH_N },
      { x: X_BOOTH_E, z: SPINE_N },
      { x: SOCIAL_W, z: SPINE_N },
      { x: SOCIAL_W, z: 0 },
    ],
    floor: CARPET,
    ceiling: null,
  },
  {
    id: "lounge",
    name: "The Lounge",
    outline: rect(SOCIAL_W, 0, BLOCK_E, KITCHEN_N),
    floor: CARPET_ACCENT,
    ceiling: null,
  },
  {
    id: "kitchen",
    name: "The Kitchen",
    outline: rect(SOCIAL_W, KITCHEN_N, BLOCK_E, SPINE_N),
    floor: TERRAZZO,
    ceiling: null,
  },

  // -- The booths -----------------------------------------------------------
  // Three rooms of four square metres. They are rooms and not props because
  // they have walls, a door and a lid, which is the whole difference.
  {
    id: "booth-1",
    name: "Booth 1",
    outline: rect(X_BOOTH_1, BOOTH_N, X_BOOTH_2, SPINE_N),
    floor: CARPET_ACCENT,
    ceiling: { height: BOOTH_CEILING, surface: CEILING_TILE },
  },
  {
    id: "booth-2",
    name: "Booth 2",
    outline: rect(X_BOOTH_2, BOOTH_N, X_BOOTH_3, SPINE_N),
    floor: CARPET_ACCENT,
    ceiling: { height: BOOTH_CEILING, surface: CEILING_TILE },
  },
  {
    id: "booth-3",
    name: "Booth 3",
    outline: rect(X_BOOTH_3, BOOTH_N, X_BOOTH_E, SPINE_N),
    floor: CARPET_ACCENT,
    ceiling: { height: BOOTH_CEILING, surface: CEILING_TILE },
  },

  // -- Circulation ----------------------------------------------------------
  {
    id: "corridor",
    name: "Circulation",
    // 1.8 m clear, running the full 34 m. A corridor is a room like any other;
    // giving it an id is what lets a walk-mode controller, a wayfinding overlay
    // or a cleaning schedule refer to it without inventing a second concept.
    outline: rect(0, SPINE_N, BLOCK_E, SPINE_S),
    floor: CONCRETE,
    ceiling: null,
  },

  // -- South band: the enclosed half ----------------------------------------
  {
    id: "alcatraz",
    name: "Alcatraz",
    outline: rect(X_ALCATRAZ, SPINE_S, X_BERNAL, DEPTH),
    floor: CARPET,
    ceiling: null,
  },
  {
    id: "bernal",
    name: "Bernal",
    outline: rect(X_BERNAL, SPINE_S, X_BOLINAS, DEPTH),
    floor: CARPET,
    ceiling: null,
  },
  {
    id: "bolinas",
    name: "Bolinas",
    outline: rect(X_BOLINAS, SPINE_S, X_QUIET, DEPTH),
    floor: CARPET,
    ceiling: null,
  },
  {
    id: "lands-end",
    name: "Lands End",
    outline: rect(X_QUIET, SPINE_S, X_WORKSHOP, DEPTH),
    floor: CARPET_ACCENT,
    ceiling: null,
  },
  {
    id: "workshop",
    name: "Dogpatch",
    outline: rect(X_WORKSHOP, SPINE_S, X_SERVER, DEPTH),
    floor: CONCRETE,
    ceiling: null,
  },
  {
    id: "server-room",
    name: "Farallon",
    // A raised floor and a lid, because this is the one room in the building
    // whose finishes are doing a job rather than making an impression.
    outline: rect(X_SERVER, SPINE_S, X_STORE, DEPTH),
    floor: RAISED_FLOOR,
    ceiling: { height: 2.6, surface: CEILING_TILE },
  },
  {
    id: "store",
    name: "Facilities",
    outline: rect(X_STORE, SPINE_S, BLOCK_E, DEPTH),
    floor: CONCRETE,
    ceiling: { height: 2.6, surface: CEILING_TILE },
  },

  // -- The east wing --------------------------------------------------------
  /**
   * One room, fourteen metres by eighteen, open to both storeys.
   *
   * `ceiling: null` is doing something different here from everywhere else in
   * this file. Elsewhere it means "take the lid off so the establishing shot can
   * see in" — a rendering convenience for a room that has a ceiling in reality.
   * Here there genuinely is no ceiling at 2.8 m, because level 2's slab stops at
   * `BLOCK_E` and this volume runs all the way to the roof. The walls around it
   * carry `COMMONS_HEIGHT` to say so.
   *
   * It is deliberately *not* subdivided. Fourteen metres would take three more
   * meeting rooms and a run of desks, and that would have been more floor area
   * doing exactly what the block already does. A room you can see the whole of,
   * with a tree in it and a balcony over one edge, is the thing the building did
   * not have.
   */
  {
    id: "commons",
    name: "The Commons",
    // Notched around the stair rather than passing underneath it, for the same
    // reason the open floor is notched around the booths: two coplanar slabs at
    // y = 0 is a z-fight waiting for the wrong GPU. Wound the way `rect` winds —
    // down the west side, east along the south, back up — which is
    // counter-clockwise in plan view with +Z down the page.
    outline: [
      { x: BLOCK_E, z: 0 },
      { x: BLOCK_E, z: DEPTH },
      { x: STAIR_W, z: DEPTH },
      { x: STAIR_W, z: STAIR_N },
      { x: WIDTH, z: STAIR_N },
      { x: WIDTH, z: 0 },
    ],
    floor: TERRAZZO,
    ceiling: null,
  },
  {
    id: "stair-core",
    name: "The Stair",
    // The one enclosure in the wing, in the corner that gets no daylight from
    // the two glazed sides. A stair is the right thing to put where nobody
    // wants to sit.
    outline: rect(STAIR_W, STAIR_N, WIDTH, DEPTH),
    floor: CONCRETE,
    ceiling: null,
  },
];

// ---- Walls ----------------------------------------------------------------

/**
 * Twenty-eight segments, and every hole in the building.
 *
 * A wall belongs to no room; it stands where it is put, from `from` to `to`,
 * centred on that line. `Opening.start` is measured **from the `from` end**, so
 * the direction a wall is written in is the direction its openings are measured
 * in — which is why every wall below is written west-to-east or north-to-south,
 * with no exceptions. Reversing one and forgetting is how a door ends up at the
 * wrong end of a room.
 */
const WALLS: Wall[] = [
  // -- The envelope ---------------------------------------------------------
  {
    id: "ext-north",
    // The daylight side. Six 4 m ribbon windows on 1.2 m piers: enough glass
    // that the desk floor reads as a place with a view, enough wall that the
    // building still has corners.
    from: { x: 0, z: 0 },
    to: { x: BLOCK_E, z: 0 },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [
      pane(1.2, 4.0, 0.75, 2.35),
      pane(6.4, 4.0, 0.75, 2.35),
      pane(11.6, 4.0, 0.75, 2.35),
      pane(16.8, 4.0, 0.75, 2.35),
      pane(22.0, 4.0, 0.75, 2.35),
      pane(27.2, 4.0, 0.75, 2.35),
    ],
  },
  {
    id: "commons-west-glazed",
    // Full-height glazing as a *wall with a glass surface*, not as one enormous
    // opening. This is the distinction the format turns on: an opening is a hole
    // and a hole is something you can see and sometimes walk through, whereas a
    // curtain wall is a solid you happen to be able to see through. Making it an
    // opening would hand the collider a 12 m gap and put the lounge in the
    // atrium.
    //
    // It was `ext-east` and it was the outside of the building. The wing put a
    // room on the far side of it, so the same glass now looks into a
    // double-height space instead of onto a street — which is a better thing for
    // a lounge to look at and cost nothing but a rename. The two openings are
    // new: a wall you cannot walk through is a wing nobody visits.
    from: { x: BLOCK_E, z: 0 },
    to: { x: BLOCK_E, z: SPINE_S },
    thickness: EXT_THICKNESS,
    surface: GLASS,
    openings: [
      archway(2.6, 2.4), // out of the kitchen
      archway(9.0, 3.0), // off the corridor, the main way through
    ],
  },
  {
    id: "commons-west-solid",
    from: { x: BLOCK_E, z: SPINE_S },
    to: { x: BLOCK_E, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: PAINT,
  },

  // -- The east wing's envelope ---------------------------------------------
  //
  // Two storeys tall, which is what makes the commons a volume rather than a
  // tall room: `Wall.height` overrides the level's, and the level's is one
  // storey because every other wall on it is.
  {
    id: "ext-east",
    from: { x: WIDTH, z: 0 },
    to: { x: WIDTH, z: DEPTH },
    thickness: EXT_THICKNESS,
    height: COMMONS_HEIGHT,
    surface: GLASS,
  },
  {
    id: "wing-north",
    from: { x: BLOCK_E, z: 0 },
    to: { x: WIDTH, z: 0 },
    thickness: EXT_THICKNESS,
    height: COMMONS_HEIGHT,
    surface: GLASS,
  },
  {
    id: "wing-south",
    from: { x: BLOCK_E, z: DEPTH },
    to: { x: WIDTH, z: DEPTH },
    thickness: EXT_THICKNESS,
    height: COMMONS_HEIGHT,
    surface: PAINT,
    // The wing's own way out, which is what lets the stair be an escape stair
    // rather than a feature. `start` is measured from the wall's `from` end, so
    // an 1.8 m pair starting at 11.6 finishes at 13.4 with 600 mm to spare —
    // 12.4 ran 200 mm past the end and `Plan` dropped the whole opening.
    openings: [doorway(11.6, 1.8, 2.4)],
  },

  // -- The stair core -------------------------------------------------------
  {
    id: "stair-west",
    from: { x: STAIR_W, z: STAIR_N },
    to: { x: STAIR_W, z: DEPTH },
    height: COMMONS_HEIGHT,
    openings: [doorway(1.2, 1.2, 2.4)],
  },
  {
    id: "stair-north",
    from: { x: STAIR_W, z: STAIR_N },
    to: { x: WIDTH, z: STAIR_N },
    height: COMMONS_HEIGHT,
  },
  {
    id: "ext-south",
    // Written west-to-east like everything else, so the openings read left to
    // right on the plan. One window per room that deserves one; the server room
    // (26.4 → 30.2) deliberately gets none.
    from: { x: 0, z: DEPTH },
    to: { x: BLOCK_E, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [
      pane(1.0, 4.4),
      pane(7.8, 3.8),
      pane(12.8, 2.6),
      pane(16.4, 2.6),
      pane(20.2, 5.4),
      doorway(32.0), // the escape stair, through the store
    ],
  },
  {
    id: "ext-west",
    from: { x: 0, z: 0 },
    to: { x: 0, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [
      // The way in, from the lift lobby that is not part of this pack. A 1.8 m
      // pair of leaves and a 2.4 m head, because an entrance that reads like an
      // internal door is the first thing that makes a model feel like a model.
      doorway(4.0, 1.8, 2.4),
      pane(7.4, 2.4),
    ],
  },

  // -- Internal, north band -------------------------------------------------
  {
    id: "lobby-east",
    from: { x: LOBBY_E, z: 0 },
    to: { x: LOBBY_E, z: SPINE_N },
    surface: ACCENT_PAINT,
    openings: [archway(6.4, 2.4)],
  },
  {
    id: "kitchen-west",
    // The kitchen's only wall. You can also reach it by walking round through
    // the lounge, which is exactly how real offices work and is why the lounge
    // has no walls at all.
    from: { x: SOCIAL_W, z: KITCHEN_N },
    to: { x: SOCIAL_W, z: SPINE_N },
    openings: [archway(1.6, 1.8)],
  },
  {
    id: "spine-north",
    // One 34 m wall with three holes in it, rather than five walls that have to
    // agree about where they meet. The stretch from 8.4 to 14.4 is the back of
    // the focus booths and is solid for that reason.
    from: { x: 0, z: SPINE_N },
    to: { x: BLOCK_E, z: SPINE_N },
    openings: [
      doorway(1.6), // lobby
      archway(16.0, 3.0), // the main way off the desk floor
      doorway(31.6), // kitchen
    ],
  },

  // -- The focus booths -----------------------------------------------------
  // Seven short walls at 2.3 m — a head above a standing person and well below
  // the ceiling, so the booths read as furniture-scale objects standing on the
  // floor rather than as rooms carved out of it. Their fourth side is
  // `spine-north`, which they share with the corridor: a wall belongs to no
  // room, so nothing needs to be said about that here.
  {
    id: "booth-w",
    from: { x: X_BOOTH_1, z: BOOTH_N },
    to: { x: X_BOOTH_1, z: SPINE_N },
    height: BOOTH_CEILING,
    surface: FELT,
  },
  {
    id: "booth-p1",
    from: { x: X_BOOTH_2, z: BOOTH_N },
    to: { x: X_BOOTH_2, z: SPINE_N },
    height: BOOTH_CEILING,
    surface: FELT,
  },
  {
    id: "booth-p2",
    from: { x: X_BOOTH_3, z: BOOTH_N },
    to: { x: X_BOOTH_3, z: SPINE_N },
    height: BOOTH_CEILING,
    surface: FELT,
  },
  {
    id: "booth-e",
    from: { x: X_BOOTH_E, z: BOOTH_N },
    to: { x: X_BOOTH_E, z: SPINE_N },
    height: BOOTH_CEILING,
    surface: FELT,
  },
  {
    id: "booth-1-front",
    from: { x: X_BOOTH_1, z: BOOTH_N },
    to: { x: X_BOOTH_2, z: BOOTH_N },
    height: BOOTH_CEILING,
    surface: FELT,
    openings: [doorway(0.55)],
  },
  {
    id: "booth-2-front",
    from: { x: X_BOOTH_2, z: BOOTH_N },
    to: { x: X_BOOTH_3, z: BOOTH_N },
    height: BOOTH_CEILING,
    surface: FELT,
    openings: [doorway(0.55)],
  },
  {
    id: "booth-3-front",
    from: { x: X_BOOTH_3, z: BOOTH_N },
    to: { x: X_BOOTH_E, z: BOOTH_N },
    height: BOOTH_CEILING,
    surface: FELT,
    openings: [doorway(0.55)],
  },

  // -- Room fronts on the corridor ------------------------------------------
  // One wall per room rather than one long wall with seven doors, because these
  // are the walls whose *surface* differs: the two glazed meeting rooms are the
  // reason you can tell from the corridor whether a room is free.
  {
    id: "front-alcatraz",
    from: { x: X_ALCATRAZ, z: SPINE_S },
    to: { x: X_BERNAL, z: SPINE_S },
    // A vision panel rather than a glass wall — the board room is the one room
    // people want to be able to close. Sill at 0.9 keeps it out of the collider.
    openings: [pane(1.0, 3.2, 0.9, 2.3), doorway(5.6)],
  },
  {
    id: "front-bernal",
    from: { x: X_BERNAL, z: SPINE_S },
    to: { x: X_BOLINAS, z: SPINE_S },
    surface: GLASS,
    openings: [doorway(0.6)],
  },
  {
    id: "front-bolinas",
    from: { x: X_BOLINAS, z: SPINE_S },
    to: { x: X_QUIET, z: SPINE_S },
    surface: GLASS,
    openings: [doorway(0.5)],
  },
  {
    id: "front-lands-end",
    from: { x: X_QUIET, z: SPINE_S },
    to: { x: X_WORKSHOP, z: SPINE_S },
    openings: [doorway(1.4)],
  },
  {
    id: "front-workshop",
    from: { x: X_WORKSHOP, z: SPINE_S },
    to: { x: X_SERVER, z: SPINE_S },
    // 1.6 m, because things arrive here on trolleys.
    openings: [doorway(2.0, 1.6)],
  },
  {
    id: "front-server",
    from: { x: X_SERVER, z: SPINE_S },
    to: { x: X_STORE, z: SPINE_S },
    openings: [doorway(0.9)],
  },
  {
    id: "front-store",
    from: { x: X_STORE, z: SPINE_S },
    to: { x: BLOCK_E, z: SPINE_S },
    openings: [doorway(1.5)],
  },

  // -- South band cross walls -----------------------------------------------
  // No openings: you get into every one of these rooms from the corridor and
  // nowhere else, which is what makes the corridor worth having.
  {
    id: "cross-bernal",
    from: { x: X_BERNAL, z: SPINE_S },
    to: { x: X_BERNAL, z: DEPTH },
  },
  {
    id: "cross-bolinas",
    from: { x: X_BOLINAS, z: SPINE_S },
    to: { x: X_BOLINAS, z: DEPTH },
  },
  {
    id: "cross-lands-end",
    from: { x: X_QUIET, z: SPINE_S },
    to: { x: X_QUIET, z: DEPTH },
  },
  {
    id: "cross-workshop",
    from: { x: X_WORKSHOP, z: SPINE_S },
    to: { x: X_WORKSHOP, z: DEPTH },
  },
  {
    id: "cross-server",
    from: { x: X_SERVER, z: SPINE_S },
    to: { x: X_SERVER, z: DEPTH },
  },
  {
    id: "cross-store",
    from: { x: X_STORE, z: SPINE_S },
    to: { x: X_STORE, z: DEPTH },
  },
];

// ---- Desk banks -----------------------------------------------------------

/**
 * Thirty-nine desks in five declarations.
 *
 * `Plan` expands each of these into a desk prop, a chair prop and a seat per
 * station, with ids you can predict without running anything: seats are
 * `${seatPrefix ?? id}-01` counting along each row and then down the rows, and
 * the props are `${id}-desk-01` and `${id}-chair-01`. Seat `eng-04` is the
 * fourth desk in the front row of the window bench, today and after the next
 * six edits to this file, which is the property a `Presence` needs.
 *
 * ### Why every bench is two rows and not four
 *
 * `facingRows` turns the first row of each pair around so a pair shares a run
 * of desktop — a bench, rather than two rows of people looking at the back of
 * each other's heads. It does that at the bank's single `rowPitch`, and the
 * pitch that makes two rows meet back-to-back (0.85 m, a desk deep plus a cable
 * trough) is nothing like the pitch you need between one bench and the next
 * (2.4 m of chair, aisle and chair). One bank cannot express both, so a second
 * bench is a second bank. That is a real limit of the format and not an
 * oversight; the alternative is a per-pair pitch field that exists to describe
 * one furniture layout.
 *
 * The four floor banks sit in two rows of two with a 1.3 m aisle up the middle,
 * and the workshop's is a single row of three against the south wall.
 */
const DESK_BANKS: DeskBank[] = [
  {
    id: "eng",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 9.2, z: 1.9 },
    rotation: NORTH,
    columns: 6,
    rows: 2,
    pitch: 1.7, // 1.6 m desks with a 0.1 m gap
    rowPitch: 0.85,
    facingRows: true,
    seatOffset: 0.6,
  },
  {
    id: "ops",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 9.2, z: 5.6 },
    rotation: NORTH,
    columns: 6,
    rows: 2,
    pitch: 1.7,
    rowPitch: 0.85,
    facingRows: true,
    seatOffset: 0.6,
  },
  {
    id: "design",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 20.6, z: 1.9 },
    rotation: NORTH,
    columns: 3,
    rows: 2,
    pitch: 1.7,
    rowPitch: 0.85,
    facingRows: true,
    seatOffset: 0.6,
  },
  {
    id: "sales",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 20.6, z: 5.6 },
    rotation: NORTH,
    columns: 3,
    rows: 2,
    pitch: 1.7,
    rowPitch: 0.85,
    facingRows: true,
    seatOffset: 0.6,
  },
  {
    id: "lab",
    desk: DESK,
    chair: TASK_CHAIR,
    // Against the workshop's south wall, so the bench runs east-to-west and the
    // columns march the other way: at `rotation: SOUTH` the bank's local +X is
    // world −X, which is why the origin is the *east* end of the run. Rotate a
    // bank and the origin stays station (1, 1); it does not become a corner of
    // the building.
    origin: { x: 24.8, z: 17.4 },
    rotation: SOUTH,
    columns: 3,
    rows: 1,
    pitch: 1.8,
    seatOffset: 0.6,
  },
];

// ---- Seats ----------------------------------------------------------------

/**
 * The thirty-nine seats a `DeskBank` cannot generate: chairs round a table,
 * chairs in a lounge, one chair behind a reception desk.
 *
 * A seat is an **address**, not a chair — the chair is a separate prop that
 * happens to be at the same coordinate, and a room with no chairs in it can
 * still have seats. Ids are meant to be stable across edits to this file for
 * the same reason street numbers are stable across repainting the house: a
 * `Presence` arriving from a private API says `bernal-04` and nothing else, and
 * it has no way to notice that the table moved 200 mm.
 *
 * The `facing` is where the occupant looks, which for a chair at a table is
 * across it.
 */
const SEATS: Seat[] = [
  // Reception
  { id: "reception-01", position: { x: 4.0, z: 4.9 }, facing: WEST, pose: "sit" },

  // The lobby's waiting pair
  { id: "lobby-01", position: { x: 4.4, z: 7.35 }, facing: SOUTH, pose: "sit" },
  { id: "lobby-02", position: { x: 4.4, z: 9.05 }, facing: NORTH, pose: "sit" },

  // Focus booths. One seat each, which is the point of them.
  { id: "booth-01", position: { x: 9.4, z: 9.3 }, facing: SOUTH, pose: "sit" },
  { id: "booth-02", position: { x: 11.4, z: 9.3 }, facing: SOUTH, pose: "sit" },
  { id: "booth-03", position: { x: 13.4, z: 9.3 }, facing: SOUTH, pose: "sit" },

  // Alcatraz — ten round a 4.8 m table, five a side, ends left clear so the
  // people at them can see the display on the west wall.
  { id: "alcatraz-01", position: { x: 1.8, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "alcatraz-02", position: { x: 2.7, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "alcatraz-03", position: { x: 3.6, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "alcatraz-04", position: { x: 4.5, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "alcatraz-05", position: { x: 5.4, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "alcatraz-06", position: { x: 1.8, z: 16.0 }, facing: NORTH, pose: "sit" },
  { id: "alcatraz-07", position: { x: 2.7, z: 16.0 }, facing: NORTH, pose: "sit" },
  { id: "alcatraz-08", position: { x: 3.6, z: 16.0 }, facing: NORTH, pose: "sit" },
  { id: "alcatraz-09", position: { x: 4.5, z: 16.0 }, facing: NORTH, pose: "sit" },
  { id: "alcatraz-10", position: { x: 5.4, z: 16.0 }, facing: NORTH, pose: "sit" },

  // Bernal — six, three a side of a table turned through ninety degrees.
  { id: "bernal-01", position: { x: 8.8, z: 14.1 }, facing: EAST, pose: "sit" },
  { id: "bernal-02", position: { x: 8.8, z: 15.0 }, facing: EAST, pose: "sit" },
  { id: "bernal-03", position: { x: 8.8, z: 15.9 }, facing: EAST, pose: "sit" },
  { id: "bernal-04", position: { x: 10.8, z: 14.1 }, facing: WEST, pose: "sit" },
  { id: "bernal-05", position: { x: 10.8, z: 15.0 }, facing: WEST, pose: "sit" },
  { id: "bernal-06", position: { x: 10.8, z: 15.9 }, facing: WEST, pose: "sit" },

  // Bolinas — four. The smallest room that is still worth booking.
  { id: "bolinas-01", position: { x: 13.2, z: 14.45 }, facing: EAST, pose: "sit" },
  { id: "bolinas-02", position: { x: 13.2, z: 15.55 }, facing: EAST, pose: "sit" },
  { id: "bolinas-03", position: { x: 15.2, z: 14.45 }, facing: WEST, pose: "sit" },
  { id: "bolinas-04", position: { x: 15.2, z: 15.55 }, facing: WEST, pose: "sit" },

  // Lands End, the quiet room
  { id: "quiet-01", position: { x: 17.8, z: 14.0 }, facing: SOUTH, pose: "sit" },
  { id: "quiet-02", position: { x: 17.8, z: 16.0 }, facing: NORTH, pose: "sit" },

  // The lounge, four chairs round a low table
  { id: "lounge-01", position: { x: 29.6, z: 1.7 }, facing: SOUTH, pose: "sit" },
  { id: "lounge-02", position: { x: 29.6, z: 4.3 }, facing: NORTH, pose: "sit" },
  { id: "lounge-03", position: { x: 28.2, z: 3.0 }, facing: EAST, pose: "sit" },
  { id: "lounge-04", position: { x: 31.0, z: 3.0 }, facing: WEST, pose: "sit" },

  // The kitchen: three at the island, two by the glass. `pose: "stand"` at the
  // island because people perch there, and the pose is the only thing that tells
  // a consumer how tall an occupant should be drawn.
  { id: "kitchen-01", position: { x: 28.2, z: 8.6 }, facing: NORTH, pose: "stand" },
  { id: "kitchen-02", position: { x: 29.0, z: 8.6 }, facing: NORTH, pose: "stand" },
  { id: "kitchen-03", position: { x: 29.8, z: 8.6 }, facing: NORTH, pose: "stand" },
  { id: "kitchen-04", position: { x: 32.6, z: 6.9 }, facing: SOUTH, pose: "sit" },
  { id: "kitchen-05", position: { x: 32.6, z: 8.6 }, facing: NORTH, pose: "sit" },
];

// ---- Props ----------------------------------------------------------------

/**
 * Everything that is not a wall, a floor or a desk bank.
 *
 * Grouped by room and in the order you would walk through the building, because
 * that is the order in which you will want to change them. Ids carry their room
 * as a prefix so that a prop id is legible on its own — `alcatraz-display` tells
 * you where to look; `prop-142` does not.
 *
 * ### On monitors, and the thing this pack deliberately does not do
 *
 * There are thirty-nine desks here and no monitors on any of them, which at
 * first looks like an omission. `DeskBank` places a desk and a chair and no
 * third thing, so monitors would be thirty-nine hand-written props that have to
 * be kept in step with a bank you are going to move next week.
 *
 * The format's answer is the asset override. Register
 * `acme:desk.workstation` with `overrides: "tera:desk.workstation"`, build a
 * desk with a monitor on it, and every station in every bank in every pack has
 * one — with no edit to this file, and no fork. That is the mechanism working
 * as designed, and it is why the monitors that *are* here are the ones that
 * belong to a place rather than to a desk: reception, and the two meeting rooms.
 */
const PROPS: Prop[] = [
  // -- Reception ------------------------------------------------------------
  // Set back four metres from the entrance and turned to face it. The desk's
  // working side is at its local +Z, so at `WEST` the receptionist is on its
  // east flank looking back at the door.
  { id: "lobby-desk", kind: DESK, position: { x: 3.4, z: 4.9 }, rotation: WEST },
  { id: "lobby-chair", kind: TASK_CHAIR, position: { x: 4.0, z: 4.9 }, rotation: WEST },
  {
    id: "lobby-monitor",
    kind: MONITOR,
    position: { x: 3.4, z: 5.2 },
    rotation: WEST,
    // Standing on the desktop. Desk assets are 0.73 m to the deck, and a monitor
    // is authored on the floor like everything else, so the pack supplies the
    // height rather than the asset assuming one.
    elevation: 0.73,
  },
  { id: "lobby-pedestal", kind: PEDESTAL, position: { x: 3.4, z: 3.8 }, rotation: WEST },

  // The waiting pair, on a rug, under two pendants.
  { id: "lobby-rug", kind: RUG, position: { x: 4.4, z: 8.2 }, rotation: NORTH },
  { id: "lobby-sofa-n", kind: LOUNGE_CHAIR, position: { x: 4.4, z: 7.35 }, rotation: SOUTH },
  { id: "lobby-sofa-s", kind: LOUNGE_CHAIR, position: { x: 4.4, z: 9.05 }, rotation: NORTH },
  { id: "lobby-table", kind: SIDE_TABLE, position: { x: 4.4, z: 8.2 }, rotation: NORTH },
  { id: "lobby-shelf", kind: SHELF, position: { x: 1.2, z: SPINE_N - INT_FACE - 0.18 }, rotation: SOUTH },
  { id: "lobby-tree-1", kind: TREE, position: { x: 0.8, z: 8.9 }, rotation: NORTH },
  { id: "lobby-tree-2", kind: TREE, position: { x: 6.6, z: 1.0 }, rotation: NORTH },
  ...scatter("lobby-pendant", PENDANT, grid(3.9, 8.2, 2, 1, 1.0, 0), { elevation: CEILING }),
  ...scatter("lobby-light", TROFFER, grid(1.4, 1.4, 3, 3, 2.4, 3.0), { elevation: CEILING }),

  // -- The open desk floor --------------------------------------------------
  // A screen down the spine of each bench, one per station. 1.4 m against a
  // 1.7 m pitch leaves a gap you can talk through, which is the entire argument
  // for a 0.45 m screen over a 1.2 m one.
  ...scatter("eng-screen", PARTITION, grid(9.2, 2.325, 6, 1, 1.7, 0)),
  ...scatter("ops-screen", PARTITION, grid(9.2, 6.025, 6, 1, 1.7, 0)),
  ...scatter("design-screen", PARTITION, grid(20.6, 2.325, 3, 1, 1.7, 0)),
  ...scatter("sales-screen", PARTITION, grid(20.6, 6.025, 3, 1, 1.7, 0)),

  // Pedestals park at the aisle ends rather than under the desks, which is
  // where they end up in a building where people move seats.
  ...scatter("open-pedestal", PEDESTAL, [
    { x: 18.75, z: 1.9 },
    { x: 18.75, z: 5.6 },
    { x: 25.2, z: 1.9 },
    { x: 25.2, z: 5.6 },
  ]),

  // Lockers and a whiteboard along the lobby wall — the one wall on this floor
  // with nothing on the other side of it. At `WEST` a locker's 1.2 m face turns
  // to run along Z, so they stack down the wall at 1.3 m centres.
  ...scatter("open-locker", LOCKER, grid(LOBBY_E + INT_FACE + 0.25, 1.0, 1, 3, 0, 1.3), {
    rotation: WEST,
  }),
  {
    id: "open-whiteboard",
    kind: WHITEBOARD,
    position: { x: LOBBY_E + INT_FACE + BOARD_OFFSET, z: 5.2 },
    rotation: WEST,
  },
  {
    id: "open-display",
    kind: DISPLAY,
    // On the corridor wall, clear of the 3 m archway at x 16–19.
    position: { x: 21.0, z: SPINE_N - INT_FACE - DISPLAY_OFFSET },
    rotation: SOUTH,
  },
  ...scatter("open-sill-plant", PLANT, grid(8.6, 0.55, 5, 1, 3.4, 0)),
  { id: "open-tree-1", kind: TREE, position: { x: 19.2, z: 3.8 }, rotation: NORTH },
  { id: "open-tree-2", kind: TREE, position: { x: 19.2, z: 9.6 }, rotation: NORTH },
  { id: "open-tree-3", kind: TREE, position: { x: 25.0, z: 9.6 }, rotation: NORTH },
  ...scatter("open-light", TROFFER, grid(8.8, 1.5, 6, 3, 3.2, 3.6), { elevation: CEILING }),

  // -- Focus booths ---------------------------------------------------------
  // Desk against the corridor wall, occupant facing it. Three booths, three
  // identical fit-outs, one line each.
  ...scatter("booth-desk", DESK, grid(9.4, 9.9, 3, 1, 2.0, 0), { rotation: SOUTH }),
  ...scatter("booth-chair", TASK_CHAIR, grid(9.4, 9.3, 3, 1, 2.0, 0), { rotation: SOUTH }),
  ...scatter("booth-light", TROFFER, grid(9.4, 9.5, 3, 1, 2.0, 0), { elevation: BOOTH_CEILING }),

  // -- Circulation ----------------------------------------------------------
  ...scatter("corridor-shelf", SHELF, grid(22.6, SPINE_N + INT_FACE + 0.18, 2, 1, 1.0, 0)),
  { id: "corridor-plant-w", kind: PLANT, position: { x: 0.7, z: 11.3 }, rotation: NORTH },
  { id: "corridor-plant-e", kind: PLANT, position: { x: 33.3, z: 11.3 }, rotation: NORTH },
  ...scatter("corridor-light", TROFFER, grid(2.0, 11.3, 9, 1, 3.6, 0), { elevation: CEILING }),

  // -- The lounge -----------------------------------------------------------
  // Four chairs facing a low table, which is the arrangement people actually
  // sit in; a row of chairs against a window is furniture nobody uses.
  { id: "lounge-rug", kind: RUG, position: { x: 29.6, z: 3.0 }, rotation: NORTH },
  { id: "lounge-chair-n", kind: LOUNGE_CHAIR, position: { x: 29.6, z: 1.7 }, rotation: SOUTH },
  { id: "lounge-chair-s", kind: LOUNGE_CHAIR, position: { x: 29.6, z: 4.3 }, rotation: NORTH },
  { id: "lounge-chair-w", kind: LOUNGE_CHAIR, position: { x: 28.2, z: 3.0 }, rotation: EAST },
  { id: "lounge-chair-e", kind: LOUNGE_CHAIR, position: { x: 31.0, z: 3.0 }, rotation: WEST },
  { id: "lounge-table", kind: SIDE_TABLE, position: { x: 29.6, z: 3.0 }, rotation: NORTH },
  { id: "lounge-tree-1", kind: TREE, position: { x: 33.2, z: 1.0 }, rotation: NORTH },
  { id: "lounge-tree-2", kind: TREE, position: { x: 26.2, z: 5.2 }, rotation: NORTH },
  ...scatter("lounge-pendant", PENDANT, grid(29.0, 3.0, 2, 1, 1.2, 0), { elevation: CEILING }),
  ...scatter("lounge-light", TROFFER, grid(27.0, 1.4, 3, 2, 2.8, 3.0), { elevation: CEILING }),

  // -- The kitchen ----------------------------------------------------------
  // A meeting table doing duty as an island. There is no `tera:kitchen.island`
  // and there should not be: seventeen assets is the set that gets a floor
  // plate looking like an office, and the honest way to get an island is to
  // register one in your own namespace.
  { id: "kitchen-island", kind: MEETING_TABLE, position: { x: 29.0, z: 7.4 }, rotation: NORTH },
  { id: "kitchen-island-plant", kind: PLANT, position: { x: 29.0, z: 7.4 }, rotation: NORTH, elevation: 0.74 },
  ...scatter("kitchen-stool", TASK_CHAIR, grid(28.2, 8.6, 3, 1, 0.8, 0), { rotation: NORTH }),
  ...scatter("kitchen-counter", LOCKER, grid(26.5, SPINE_N - INT_FACE - 0.25, 3, 1, 1.3, 0), {
    rotation: SOUTH,
  }),
  { id: "kitchen-shelf", kind: SHELF, position: { x: 30.6, z: SPINE_N - INT_FACE - 0.18 }, rotation: SOUTH },
  { id: "kitchen-seat-n", kind: LOUNGE_CHAIR, position: { x: 32.6, z: 6.9 }, rotation: SOUTH },
  { id: "kitchen-seat-s", kind: LOUNGE_CHAIR, position: { x: 32.6, z: 8.6 }, rotation: NORTH },
  { id: "kitchen-table", kind: SIDE_TABLE, position: { x: 32.6, z: 7.75 }, rotation: NORTH },
  ...scatter("kitchen-pendant", PENDANT, grid(28.4, 7.4, 2, 1, 1.2, 0), { elevation: CEILING }),
  ...scatter("kitchen-light", TROFFER, grid(27.0, 7.0, 3, 2, 2.8, 2.4), { elevation: CEILING }),

  // -- Alcatraz, the board room ---------------------------------------------
  // Two 2.4 m tables end to end. A `Prop` carries no parameters — it is an id
  // and a placement — so the 4.8 m table this room wants is two of the 2.4 m
  // one, and the alternative is registering `acme:table.board` at the length
  // you want. Both are one line; only one of them is in this file.
  { id: "alcatraz-table-w", kind: MEETING_TABLE, position: { x: 2.4, z: 15.0 }, rotation: NORTH },
  { id: "alcatraz-table-e", kind: MEETING_TABLE, position: { x: 4.8, z: 15.0 }, rotation: NORTH },
  ...scatter("alcatraz-chair-n", TASK_CHAIR, grid(1.8, 14.0, 5, 1, 0.9, 0), { rotation: SOUTH }),
  ...scatter("alcatraz-chair-s", TASK_CHAIR, grid(1.8, 16.0, 5, 1, 0.9, 0), { rotation: NORTH }),
  {
    id: "alcatraz-display",
    kind: DISPLAY,
    // The west wall is the one blank wall in the room: the south wall has the
    // window, the north wall has the vision panel and the door. Rooms get laid
    // out around where a screen can go more often than anyone admits.
    position: { x: EXT_FACE + DISPLAY_OFFSET, z: 15.0 },
    rotation: WEST,
  },
  {
    id: "alcatraz-whiteboard",
    kind: WHITEBOARD,
    position: { x: X_BERNAL - INT_FACE - BOARD_OFFSET, z: 15.0 },
    rotation: EAST,
  },
  { id: "alcatraz-tree", kind: TREE, position: { x: 6.6, z: 17.2 }, rotation: NORTH },
  ...scatter("alcatraz-pendant", PENDANT, grid(2.0, 15.0, 3, 1, 1.6, 0), { elevation: CEILING }),
  ...scatter("alcatraz-light", TROFFER, grid(1.6, 13.2, 3, 2, 2.4, 3.2), { elevation: CEILING }),

  // -- Bernal, six people ---------------------------------------------------
  // The table turns through ninety degrees so its 2.4 m length runs down the
  // room. `rotation: WEST` on a table is not the table facing anywhere — it is
  // just a right angle, and the yaw convention has no separate vocabulary for
  // things without a front.
  { id: "bernal-table", kind: MEETING_TABLE, position: { x: 9.8, z: 15.0 }, rotation: WEST },
  ...scatter("bernal-chair-w", TASK_CHAIR, grid(8.8, 14.1, 1, 3, 0, 0.9), { rotation: EAST }),
  ...scatter("bernal-chair-e", TASK_CHAIR, grid(10.8, 14.1, 1, 3, 0, 0.9), { rotation: WEST }),
  {
    id: "bernal-display",
    kind: DISPLAY,
    position: { x: X_BERNAL + INT_FACE + DISPLAY_OFFSET, z: 15.0 },
    rotation: WEST,
  },
  { id: "bernal-tree", kind: TREE, position: { x: 11.8, z: 17.2 }, rotation: NORTH },
  ...scatter("bernal-pendant", PENDANT, grid(9.8, 14.2, 1, 2, 0, 1.6), { elevation: CEILING }),
  ...scatter("bernal-light", TROFFER, grid(8.4, 13.4, 2, 2, 2.8, 3.2), { elevation: CEILING }),

  // -- Bolinas, four people -------------------------------------------------
  { id: "bolinas-table", kind: MEETING_TABLE, position: { x: 14.2, z: 15.0 }, rotation: WEST },
  ...scatter("bolinas-chair-w", TASK_CHAIR, grid(13.2, 14.45, 1, 2, 0, 1.1), { rotation: EAST }),
  ...scatter("bolinas-chair-e", TASK_CHAIR, grid(15.2, 14.45, 1, 2, 0, 1.1), { rotation: WEST }),
  {
    id: "bolinas-whiteboard",
    kind: WHITEBOARD,
    position: { x: X_QUIET - INT_FACE - BOARD_OFFSET, z: 15.0 },
    rotation: EAST,
  },
  { id: "bolinas-plant", kind: PLANT, position: { x: 12.9, z: 17.3 }, rotation: NORTH },
  ...scatter("bolinas-pendant", PENDANT, grid(14.2, 15.0, 1, 1, 0, 0), { elevation: CEILING }),
  ...scatter("bolinas-light", TROFFER, grid(13.2, 13.4, 2, 2, 2.0, 3.2), { elevation: CEILING }),

  // -- Lands End, the quiet room --------------------------------------------
  // No table you can put a laptop on, on purpose.
  { id: "quiet-rug", kind: RUG, position: { x: 17.8, z: 15.0 }, rotation: NORTH },
  { id: "quiet-chair-n", kind: LOUNGE_CHAIR, position: { x: 17.8, z: 14.0 }, rotation: SOUTH },
  { id: "quiet-chair-s", kind: LOUNGE_CHAIR, position: { x: 17.8, z: 16.0 }, rotation: NORTH },
  { id: "quiet-table", kind: SIDE_TABLE, position: { x: 17.8, z: 15.0 }, rotation: NORTH },
  { id: "quiet-shelf", kind: SHELF, position: { x: 16.7, z: SPINE_S + INT_FACE + 0.18 }, rotation: NORTH },
  { id: "quiet-tree", kind: TREE, position: { x: 19.0, z: 17.2 }, rotation: NORTH },
  ...scatter("quiet-pendant", PENDANT, grid(17.8, 15.0, 1, 1, 0, 0), { elevation: CEILING }),
  ...scatter("quiet-light", TROFFER, grid(16.9, 13.4, 2, 2, 1.8, 3.2), { elevation: CEILING }),

  // -- Dogpatch, the workshop -----------------------------------------------
  // The bench is the `lab` desk bank; everything here is what goes round it.
  ...scatter("shop-shelf", SHELF, grid(24.6, SPINE_S + INT_FACE + 0.18, 2, 1, 1.0, 0)),
  ...scatter("shop-locker", LOCKER, grid(X_SERVER - INT_FACE - 0.25, 13.4, 1, 2, 0, 1.3), {
    rotation: EAST,
  }),
  {
    id: "shop-whiteboard",
    kind: WHITEBOARD,
    position: { x: X_WORKSHOP + INT_FACE + BOARD_OFFSET, z: 15.0 },
    rotation: WEST,
  },
  { id: "shop-tree", kind: TREE, position: { x: 20.3, z: 14.0 }, rotation: NORTH },
  ...scatter("shop-light", TROFFER, grid(20.6, 13.4, 3, 2, 2.4, 3.2), { elevation: CEILING }),

  // -- Farallon, the server room --------------------------------------------
  // Two rows of cabinets facing each other across a 2.4 m aisle, which is the
  // hot-aisle arrangement and also the only way two rows of anything read as
  // deliberate. A storage locker is not a rack, but it is a 1.2 x 0.5 x 1.8 m
  // box with a front and a back, and at this scale that is a rack.
  //
  // The five objects in this room are the pack's one worked example of
  // `audience: "private"`. The room, its raised floor, its walls and its lid are
  // all still built at public depth — the *architecture* is not the secret — but
  // what is standing in it is not shown to a stranger. How many cabinets an
  // organisation runs and which way the aisle faces is the kind of detail that
  // is worth nothing to a visitor and something to somebody else, and the room
  // already keeps its ceiling for the same reason, which is the pack saying the
  // same thing twice in two vocabularies.
  //
  // At public depth this leaves a lit, empty, raised-floor room, which is an
  // honest picture of a room you are not being shown the inside of. The lights
  // stay: a dark hole in a floor plan reads as a rendering fault, not as
  // discretion.
  ...scatter("mdf-rack-n", LOCKER, grid(27.4, 13.4, 2, 1, 1.3, 0), {
    rotation: NORTH,
    audience: "private",
  }),
  ...scatter("mdf-rack-s", LOCKER, grid(27.4, 15.8, 2, 1, 1.3, 0), {
    rotation: SOUTH,
    audience: "private",
  }),
  {
    id: "mdf-shelf",
    kind: SHELF,
    position: { x: 29.4, z: DEPTH - EXT_FACE - 0.18 },
    rotation: SOUTH,
    audience: "private",
  },
  ...scatter("mdf-light", TROFFER, grid(27.6, 14.0, 2, 2, 2.0, 2.4), { elevation: 2.6 }),

  // -- Facilities -----------------------------------------------------------
  ...scatter("fac-locker", LOCKER, grid(X_STORE + INT_FACE + 0.25, 13.2, 1, 4, 0, 1.3), {
    rotation: WEST,
  }),
  ...scatter("fac-shelf", SHELF, grid(BLOCK_E - EXT_FACE - 0.2, 13.4, 1, 2, 0, 1.0), {
    rotation: EAST,
  }),
  ...scatter("fac-light", TROFFER, grid(32.2, 14.0, 1, 2, 0, 2.4), { elevation: 2.6 }),

  // -- The Commons ----------------------------------------------------------
  //
  // Furnished sparsely on purpose. The wing's whole argument is the volume, and
  // a fourteen-metre room with forty objects in it is a fourteen-metre room you
  // cannot see. Four trees, two clusters of seating, one long table people
  // actually work at, and nothing along the glass.
  ...scatter("commons-tree", TREE, [
    { x: 36.6, z: 3.2 },
    { x: 45.4, z: 4.6 },
    { x: 37.4, z: 15.0 },
    { x: 44.2, z: 9.8 },
  ]),
  {
    id: "commons-rug",
    kind: RUG,
    position: { x: 39.4, z: 5.0 },
    rotation: NORTH,
    colorKey: "social",
  },
  // A ring rather than a row: seating that faces itself is seating people sit
  // in, and seating in a line against a wall is seating people walk past.
  ...scatter("commons-chair", LOUNGE_CHAIR, [
    { x: 38.0, z: 4.2 },
    { x: 40.8, z: 4.2 },
    { x: 38.0, z: 6.0 },
    { x: 40.8, z: 6.0 },
  ]),
  { id: "commons-table-01", kind: SIDE_TABLE, position: { x: 39.4, z: 5.1 }, rotation: NORTH },
  /**
   * The long table, on the axis of the archway off the corridor.
   *
   * Placed where somebody walking out of the block arrives, which is the only
   * placement rule this room follows. `tera:table.meeting` at its default 2.4 m
   * is the longest table in the kit; two of them end to end read as one refectory
   * table, which is what a commons wants and what the kit has no single asset for.
   */
  { id: "commons-long-01", kind: MEETING_TABLE, position: { x: 39.8, z: 10.4 }, rotation: NORTH },
  { id: "commons-long-02", kind: MEETING_TABLE, position: { x: 39.8, z: 12.9 }, rotation: NORTH },
  ...scatter("commons-perch", TASK_CHAIR, [
    { x: 38.6, z: 10.4 },
    { x: 41.0, z: 10.4 },
    { x: 38.6, z: 12.9 },
    { x: 41.0, z: 12.9 },
  ]),
  {
    id: "commons-display",
    kind: DISPLAY,
    // On the stair core's north face, which is the one solid surface in the wing
    // that everybody walking through it is already looking at.
    position: { x: 45.4, z: STAIR_N - INT_FACE - DISPLAY_OFFSET },
    rotation: SOUTH,
    elevation: 1.1,
  },
  /**
   * Pendants at 19 m, hanging into the void from the roof rather than from a
   * ceiling at 2.8 m that is not there.
   *
   * This is the one place in the pack where the ceiling-fixture exception earns
   * its keep: `tera:light.pendant` is authored with its origin at the mounting
   * plane and all its geometry below, so a number here is a real height and not
   * an offset from a lid nobody built.
   */
  ...scatter("commons-pendant", PENDANT, grid(37.2, 4.0, 3, 3, 4.2, 5.4), {
    elevation: COMMONS_HEIGHT - 0.6,
  }),
];

// ---- Zones ----------------------------------------------------------------

/**
 * Four labelled regions of floor, and nothing else.
 *
 * A zone has no behaviour, no geometry and no meaning the engine can see. It is
 * an area with a name and an opaque `colorKey`, so that something outside this
 * repo can highlight it, filter against it or count what is inside it. Whether
 * "eng" is a team, a cost centre or a colour scheme is not the engine's
 * business — the same rule as `Marker.colorKey`, which is why there is no
 * `kind` field to be tempted by.
 *
 * ### Two of them are private, and it is the names that make them so
 *
 * The engine cannot tell these four apart, but a reader can. "Social" and
 * "Focus" describe what the floor is *for*: anyone standing in the lounge can
 * see that it is the lounge, and a public visitor learning that the three
 * glass-lidded boxes are the focus booths has learned nothing they could not
 * have guessed from the plan.
 *
 * "Engineering" and "Studio" describe who *sits* there, and that is a different
 * kind of fact. It is org chart drawn on a floor: how many desks each function
 * has, where they are relative to each other, which corner grew last quarter.
 * Nobody signed in is surprised by it and nobody anonymous is owed it, which is
 * exactly the line `audience` exists to draw — so the two team zones are not
 * built at public depth and the two spatial ones are.
 *
 * This is the granularity the field is for. The alternative anybody reaches for
 * first is a single `private: true` on the whole floorplan, and it is useless:
 * a building is not private or public, the labels on it are.
 */
const ZONES: Zone[] = [
  {
    id: "zone-eng",
    name: "Engineering",
    outline: rect(8.0, 0.6, 18.9, 7.6),
    colorKey: "team-a",
    audience: "private",
  },
  {
    id: "zone-studio",
    name: "Studio",
    outline: rect(19.4, 0.6, 25.2, 7.6),
    colorKey: "team-b",
    audience: "private",
  },
  { id: "zone-social", name: "Social", outline: rect(SOCIAL_W, 0, BLOCK_E, SPINE_N), colorKey: "social" },
  { id: "zone-focus", name: "Focus", outline: rect(X_BOOTH_1, BOOTH_N, X_BOOTH_E, SPINE_N), colorKey: "focus" },
  // Spatial rather than organisational, like "Social" and "Focus" and unlike the
  // two team zones — anybody standing in the wing can see it is the commons.
  {
    id: "zone-commons",
    name: "Commons",
    outline: rect(BLOCK_E, 0, WIDTH, DEPTH),
    colorKey: "social",
  },
];

// ---- Level 2 ---------------------------------------------------------------

/**
 * The upper storey: a studio, a library, three rooms and a balcony.
 *
 * **Every coordinate below is in level 2's own frame**, with the floor at zero.
 * `Plan` adds `STOREY` to all of them exactly once when it resolves the level,
 * which is what lets this section be read without holding 16.8 in your head, and
 * what lets both levels sit in one scene group at the origin.
 *
 * It stops at `BLOCK_E`. The wing below is open to the roof, so there is no slab
 * over it — only `l2-gallery`, a 4.2 m balcony that reaches out over the void so
 * the commons can be looked *down into* rather than merely walked through. A
 * double-height room nobody can stand above is a tall room; the balcony is what
 * makes it a volume.
 *
 * The programme is deliberately the opposite of level 1's. Downstairs is
 * fifteen rooms, thirty-nine desks and a corridor to tie them together; up here
 * is one long studio, one library, and three rooms for the things that cannot
 * happen at a desk. A second floor that repeated the first would have doubled
 * the area and added nothing to look at.
 */

// The lines level 2 is built on. It shares the block's north band / corridor /
// south band rhythm, because the structure below does not move, and divides the
// bands differently because the programme is different.
const L2_STUDIO_W = 6.4; // landing / studio
const L2_LIBRARY_W = 25.2; // studio / library
const L2_MEET_A_E = 8.4; // the two meeting rooms
const L2_MEET_B_E = 15.2; // meeting room / project room
const L2_PROJECT_E = 24.6; // project room / plant

const L2_ROOMS: Room[] = [
  // -- North band -----------------------------------------------------------
  {
    id: "l2-landing",
    name: "The Landing",
    // Where the stair arrives, and the only part of this floor that is
    // circulation rather than a place to be.
    outline: rect(0, 0, L2_STUDIO_W, SPINE_N),
    floor: TERRAZZO,
    ceiling: null,
  },
  {
    id: "l2-studio",
    name: "The Studio",
    // Nineteen metres of unbroken north-lit floor, which is the one thing the
    // block downstairs cannot offer: level 1's equivalent run is interrupted by
    // the lobby wall and the booths.
    outline: rect(L2_STUDIO_W, 0, L2_LIBRARY_W, SPINE_N),
    floor: CARPET,
    ceiling: null,
  },
  {
    id: "l2-library",
    name: "The Library",
    outline: rect(L2_LIBRARY_W, 0, BLOCK_E, SPINE_N),
    floor: WOOD,
    ceiling: null,
  },

  // -- The corridor ---------------------------------------------------------
  {
    id: "l2-corridor",
    name: "Corridor",
    outline: rect(0, SPINE_N, BLOCK_E, SPINE_S),
    floor: CARPET_ACCENT,
    ceiling: null,
  },

  // -- South band -----------------------------------------------------------
  {
    id: "l2-meet-a",
    name: "Tiburon",
    outline: rect(0, SPINE_S, L2_MEET_A_E, DEPTH),
    floor: CARPET_ACCENT,
    ceiling: null,
  },
  {
    id: "l2-meet-b",
    name: "Angel Island",
    outline: rect(L2_MEET_A_E, SPINE_S, L2_MEET_B_E, DEPTH),
    floor: CARPET_ACCENT,
    ceiling: null,
  },
  {
    id: "l2-project",
    name: "The Project Room",
    // Concrete and no soft finishes: the room you are allowed to stick things to
    // the walls of. The workshop downstairs is its equivalent and reads the same
    // way for the same reason.
    outline: rect(L2_MEET_B_E, SPINE_S, L2_PROJECT_E, DEPTH),
    floor: CONCRETE,
    ceiling: null,
  },
  {
    id: "l2-plant",
    name: "Plant",
    // The one room up here with a lid on it, and the same argument as the server
    // room below: you are never meant to see inside, and a ceiling says so more
    // cheaply than a sign.
    outline: rect(L2_PROJECT_E, SPINE_S, BLOCK_E, DEPTH),
    floor: RAISED_FLOOR,
    ceiling: { height: 2.6, surface: CEILING_TILE },
  },

  // -- Over the void --------------------------------------------------------
  {
    id: "l2-gallery",
    name: "The Gallery",
    outline: rect(BLOCK_E, 2.0, GALLERY_E, 16.0),
    floor: WOOD,
    ceiling: null,
  },
];

const L2_WALLS: Wall[] = [
  // -- The envelope, repeated ------------------------------------------------
  //
  // The same lines as level 1, because the structure below does not move. The
  // openings are not the same, and that is the point of restating the walls
  // rather than sharing them: this floor wants glass where the floor below wants
  // a pier, and a format in which two storeys had to agree about their windows
  // would be a format that could only describe one storey.
  {
    id: "l2-ext-north",
    from: { x: 0, z: 0 },
    to: { x: BLOCK_E, z: 0 },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [
      pane(1.2, 4.0, 0.75, 2.35),
      pane(6.4, 4.0, 0.75, 2.35),
      pane(11.6, 4.0, 0.75, 2.35),
      pane(16.8, 4.0, 0.75, 2.35),
      pane(22.0, 4.0, 0.75, 2.35),
      pane(27.2, 4.0, 0.75, 2.35),
    ],
  },
  {
    id: "l2-ext-west",
    from: { x: 0, z: 0 },
    to: { x: 0, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [pane(3.0, 3.4), pane(13.4, 2.6)],
  },
  {
    id: "l2-ext-south",
    from: { x: 0, z: DEPTH },
    to: { x: BLOCK_E, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: PAINT,
    openings: [pane(1.4, 5.2), pane(9.4, 4.4), pane(16.0, 6.6), pane(25.4, 3.0)],
  },

  // -- The edge of the void --------------------------------------------------
  /**
   * The east wall of the block, at level 2, with the gallery on the far side.
   *
   * Glazed for the same reason level 1's is: the studio looks into the commons,
   * and a solid wall here would make the balcony a thing you have to go and find
   * rather than a thing you can see.
   */
  {
    id: "l2-block-east",
    from: { x: BLOCK_E, z: 0 },
    to: { x: BLOCK_E, z: DEPTH },
    thickness: EXT_THICKNESS,
    surface: GLASS,
    openings: [archway(6.0, 3.0), archway(12.0, 2.4)],
  },
  /**
   * The balustrade: a 1.1 m wall around the three open edges of the gallery.
   *
   * A wall and not a prop, and this is the load-bearing reason. `Plan` derives
   * the walk-mode collider from the wall list, so a balustrade authored as
   * furniture is a balcony you can walk off — and the drop is 4.2 m onto
   * terrazzo. An opening is a way through iff `sill <= 0 and head >= 1.1`; this
   * has no openings at all, which is exactly what a balustrade is.
   */
  {
    id: "l2-gallery-north",
    from: { x: BLOCK_E, z: 2.0 },
    to: { x: GALLERY_E, z: 2.0 },
    height: 1.1,
    surface: GLASS,
  },
  {
    id: "l2-gallery-east",
    from: { x: GALLERY_E, z: 2.0 },
    to: { x: GALLERY_E, z: 16.0 },
    height: 1.1,
    surface: GLASS,
  },
  {
    id: "l2-gallery-south",
    from: { x: BLOCK_E, z: 16.0 },
    to: { x: GALLERY_E, z: 16.0 },
    height: 1.1,
    surface: GLASS,
  },

  // -- Internal --------------------------------------------------------------
  {
    id: "l2-landing-east",
    from: { x: L2_STUDIO_W, z: 0 },
    to: { x: L2_STUDIO_W, z: SPINE_N },
    surface: ACCENT_PAINT,
    openings: [archway(5.6, 2.6)],
  },
  {
    id: "l2-library-west",
    from: { x: L2_LIBRARY_W, z: 0 },
    to: { x: L2_LIBRARY_W, z: SPINE_N },
    openings: [archway(6.2, 2.4)],
  },
  {
    id: "l2-spine-north",
    // One wall with three holes, the same arrangement as the floor below and for
    // the same reason: five walls that have to agree about where they meet is
    // five chances to be a centimetre out.
    from: { x: 0, z: SPINE_N },
    to: { x: BLOCK_E, z: SPINE_N },
    openings: [doorway(2.4), archway(14.0, 3.0), doorway(29.0)],
  },
  {
    id: "l2-spine-south",
    from: { x: 0, z: SPINE_S },
    to: { x: BLOCK_E, z: SPINE_S },
    openings: [
      doorway(3.2), // Tiburon
      doorway(10.4), // Angel Island
      doorway(18.0, 1.8, 2.4), // the project room, wide enough to carry things through
      doorway(28.4), // plant
    ],
  },
  {
    id: "l2-meet-divide",
    from: { x: L2_MEET_A_E, z: SPINE_S },
    to: { x: L2_MEET_A_E, z: DEPTH },
  },
  {
    id: "l2-project-west",
    from: { x: L2_MEET_B_E, z: SPINE_S },
    to: { x: L2_MEET_B_E, z: DEPTH },
    // A vision panel rather than a window: it looks at the corridor's neighbour
    // room, so the sill keeps it out of the collider and the head keeps it out
    // of anybody's eyeline from a seat.
    openings: [pane(1.2, 2.4, 1.0, 2.2)],
  },
  {
    id: "l2-plant-west",
    from: { x: L2_PROJECT_E, z: SPINE_S },
    to: { x: L2_PROJECT_E, z: DEPTH },
  },
];

/**
 * Two benches of twelve, facing each other across the studio.
 *
 * Seat ids are `studio-01` … `studio-24`, restarting at 01 as every bank's do.
 * They are unique building-wide because the prefix is — `eng`, `ops`, `design`,
 * `sales` and `lab` are all downstairs — which is the property that matters,
 * since a `Presence` binds to a seat id and ids are unique per kind across the
 * whole building rather than per level.
 */
const L2_DESK_BANKS: DeskBank[] = [
  {
    id: "studio",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 8.4, z: 1.9 },
    rotation: 0,
    columns: 6,
    rows: 2,
    pitch: 1.7,
    rowPitch: 0.85,
    facingRows: true,
  },
  {
    // A second bench is a second bank. One `rowPitch` cannot express both the
    // 0.85 m that makes two rows meet back to back and the 2.4 m of chair,
    // aisle and chair between one bench and the next — that is a real limit of
    // the format and the reason every bench in this file is its own bank.
    id: "studio-b",
    desk: DESK,
    chair: TASK_CHAIR,
    origin: { x: 8.4, z: 6.1 },
    rotation: 0,
    columns: 6,
    rows: 2,
    pitch: 1.7,
    rowPitch: 0.85,
    facingRows: true,
    seatPrefix: "studio-b",
  },
];

const L2_SEATS: Seat[] = [
  // The library: four at the long table, two in the window.
  { id: "library-01", position: { x: 27.4, z: 3.0 }, facing: EAST, pose: "sit" },
  { id: "library-02", position: { x: 27.4, z: 4.6 }, facing: EAST, pose: "sit" },
  { id: "library-03", position: { x: 30.2, z: 3.0 }, facing: WEST, pose: "sit" },
  { id: "library-04", position: { x: 30.2, z: 4.6 }, facing: WEST, pose: "sit" },
  { id: "library-05", position: { x: 28.0, z: 8.4 }, facing: NORTH, pose: "sit" },
  { id: "library-06", position: { x: 31.0, z: 8.4 }, facing: NORTH, pose: "sit" },

  // Tiburon, eight around a 2.4 m table.
  { id: "tiburon-01", position: { x: 3.0, z: 14.0 }, facing: EAST, pose: "sit" },
  { id: "tiburon-02", position: { x: 3.0, z: 15.2 }, facing: EAST, pose: "sit" },
  { id: "tiburon-03", position: { x: 3.0, z: 16.4 }, facing: EAST, pose: "sit" },
  { id: "tiburon-04", position: { x: 5.4, z: 14.0 }, facing: WEST, pose: "sit" },
  { id: "tiburon-05", position: { x: 5.4, z: 15.2 }, facing: WEST, pose: "sit" },
  { id: "tiburon-06", position: { x: 5.4, z: 16.4 }, facing: WEST, pose: "sit" },
  { id: "tiburon-07", position: { x: 4.2, z: 13.4 }, facing: SOUTH, pose: "sit" },
  { id: "tiburon-08", position: { x: 4.2, z: 17.0 }, facing: NORTH, pose: "sit" },

  // Angel Island, six.
  { id: "angel-01", position: { x: 10.4, z: 14.4 }, facing: EAST, pose: "sit" },
  { id: "angel-02", position: { x: 10.4, z: 15.8 }, facing: EAST, pose: "sit" },
  { id: "angel-03", position: { x: 12.8, z: 14.4 }, facing: WEST, pose: "sit" },
  { id: "angel-04", position: { x: 12.8, z: 15.8 }, facing: WEST, pose: "sit" },
  { id: "angel-05", position: { x: 11.6, z: 13.8 }, facing: SOUTH, pose: "sit" },
  { id: "angel-06", position: { x: 11.6, z: 16.6 }, facing: NORTH, pose: "sit" },

  // The project room stands up. `pose: "stand"` is the only thing that tells a
  // consumer how tall to draw an occupant, and people do not sit at a wall of
  // work in progress.
  { id: "project-01", position: { x: 17.4, z: 14.6 }, facing: NORTH, pose: "stand" },
  { id: "project-02", position: { x: 19.0, z: 14.6 }, facing: NORTH, pose: "stand" },
  { id: "project-03", position: { x: 20.6, z: 14.6 }, facing: NORTH, pose: "stand" },

  // The gallery, looking down into the commons.
  { id: "gallery-01", position: { x: 36.0, z: 5.0 }, facing: EAST, pose: "sit" },
  { id: "gallery-02", position: { x: 36.0, z: 6.6 }, facing: EAST, pose: "sit" },
  { id: "gallery-03", position: { x: 36.0, z: 12.0 }, facing: EAST, pose: "sit" },
];

const L2_PROPS: Prop[] = [
  // -- The landing ----------------------------------------------------------
  { id: "l2-landing-plant", kind: TREE, position: { x: 2.2, z: 2.4 }, rotation: NORTH },
  ...scatter("l2-landing-locker", LOCKER, grid(0 + EXT_FACE + 0.25, 5.0, 1, 3, 0, 1.3), {
    rotation: WEST,
  }),
  ...scatter("l2-landing-light", PENDANT, grid(3.2, 3.0, 1, 3, 0, 3.0), { elevation: CEILING }),

  // -- The studio -----------------------------------------------------------
  //
  // A monitor per station is not written out here, and deliberately: thirty-nine
  // monitor props that have to be kept in step with a bank somebody will move
  // next week is the failure mode the README's "Registering your own assets"
  // section exists to prevent. Register a desk that builds a desk with a monitor
  // on it and every station in every bank in the building gets one.
  ...scatter("l2-studio-ped", PEDESTAL, grid(8.4, 3.4, 6, 1, 1.7, 0)),
  ...scatter("l2-studio-part", PARTITION, grid(9.25, 2.3, 5, 1, 1.7, 0)),
  ...scatter("l2-studio-part-b", PARTITION, grid(9.25, 6.5, 5, 1, 1.7, 0)),
  ...scatter("l2-studio-light", TROFFER, grid(8.0, 2.0, 5, 3, 3.4, 3.2), { elevation: CEILING }),
  { id: "l2-studio-board", kind: WHITEBOARD, position: { x: 20.0, z: SPINE_N - INT_FACE - BOARD_OFFSET }, rotation: SOUTH, elevation: 1.0 },
  ...scatter("l2-studio-plant", PLANT, [
    { x: 7.2, z: 9.4 },
    { x: 24.2, z: 9.4 },
    { x: 24.2, z: 1.2 },
  ]),

  // -- The library ----------------------------------------------------------
  ...scatter("l2-library-shelf", SHELF, grid(BLOCK_E - EXT_FACE - 0.2, 1.4, 1, 6, 0, 1.0), {
    rotation: EAST,
  }),
  { id: "l2-library-table", kind: MEETING_TABLE, position: { x: 28.8, z: 3.8 }, rotation: NORTH },
  ...scatter("l2-library-chair", TASK_CHAIR, [
    { x: 27.4, z: 3.0 },
    { x: 27.4, z: 4.6 },
    { x: 30.2, z: 3.0 },
    { x: 30.2, z: 4.6 },
  ]),
  { id: "l2-library-rug", kind: RUG, position: { x: 29.6, z: 8.0 }, rotation: NORTH, colorKey: "focus" },
  ...scatter("l2-library-lounge", LOUNGE_CHAIR, [
    { x: 28.0, z: 8.4 },
    { x: 31.0, z: 8.4 },
  ]),
  { id: "l2-library-side", kind: SIDE_TABLE, position: { x: 29.5, z: 8.4 }, rotation: NORTH },
  ...scatter("l2-library-light", PENDANT, grid(27.4, 3.8, 2, 1, 2.8, 0), { elevation: CEILING }),

  // -- Tiburon and Angel Island ---------------------------------------------
  { id: "l2-tiburon-table", kind: MEETING_TABLE, position: { x: 4.2, z: 15.2 }, rotation: NORTH },
  ...scatter("l2-tiburon-chair", TASK_CHAIR, [
    { x: 3.0, z: 14.0 },
    { x: 3.0, z: 15.2 },
    { x: 3.0, z: 16.4 },
    { x: 5.4, z: 14.0 },
    { x: 5.4, z: 15.2 },
    { x: 5.4, z: 16.4 },
    { x: 4.2, z: 13.4 },
    { x: 4.2, z: 17.0 },
  ]),
  {
    id: "l2-tiburon-display",
    kind: DISPLAY,
    position: { x: 4.2, z: SPINE_S + INT_FACE + DISPLAY_OFFSET },
    rotation: NORTH,
    elevation: 1.15,
  },
  ...scatter("l2-tiburon-light", TROFFER, grid(2.8, 14.4, 2, 2, 2.8, 2.0), { elevation: CEILING }),

  { id: "l2-angel-table", kind: MEETING_TABLE, position: { x: 11.6, z: 15.2 }, rotation: NORTH },
  ...scatter("l2-angel-chair", TASK_CHAIR, [
    { x: 10.4, z: 14.4 },
    { x: 10.4, z: 15.8 },
    { x: 12.8, z: 14.4 },
    { x: 12.8, z: 15.8 },
    { x: 11.6, z: 13.8 },
    { x: 11.6, z: 16.6 },
  ]),
  {
    id: "l2-angel-board",
    kind: WHITEBOARD,
    position: { x: 11.6, z: SPINE_S + INT_FACE + BOARD_OFFSET },
    rotation: NORTH,
    elevation: 1.0,
  },
  ...scatter("l2-angel-light", TROFFER, grid(10.4, 14.6, 2, 2, 2.4, 2.0), { elevation: CEILING }),

  // -- The project room -----------------------------------------------------
  ...scatter("l2-project-board", WHITEBOARD, grid(17.4, SPINE_S + INT_FACE + BOARD_OFFSET, 3, 1, 3.0, 0), {
    rotation: NORTH,
    elevation: 1.0,
  }),
  { id: "l2-project-table", kind: MEETING_TABLE, position: { x: 19.8, z: 16.4 }, rotation: NORTH },
  ...scatter("l2-project-shelf", SHELF, grid(L2_PROJECT_E - INT_FACE - 0.2, 14.0, 1, 2, 0, 1.1), {
    rotation: EAST,
  }),
  ...scatter("l2-project-light", TROFFER, grid(17.0, 14.4, 3, 2, 3.2, 2.4), { elevation: CEILING }),

  // -- Plant ----------------------------------------------------------------
  ...scatter("l2-plant-shelf", SHELF, grid(BLOCK_E - EXT_FACE - 0.2, 14.0, 1, 3, 0, 1.1), {
    rotation: EAST,
    audience: "private",
  }),
  ...scatter("l2-plant-light", TROFFER, grid(26.4, 14.8, 2, 1, 2.4, 0), { elevation: 2.6 }),

  // -- The gallery ----------------------------------------------------------
  //
  // Furnished as a place to sit rather than as a route, which is what a balcony
  // over a double-height room is for. Everything is set back from the
  // balustrade: a chair against a glass rail is a chair nobody chooses.
  ...scatter("l2-gallery-chair", LOUNGE_CHAIR, [
    { x: 36.0, z: 5.0 },
    { x: 36.0, z: 6.6 },
    { x: 36.0, z: 12.0 },
  ], { rotation: EAST }),
  ...scatter("l2-gallery-side", SIDE_TABLE, [
    { x: 36.0, z: 5.8 },
    { x: 36.0, z: 12.8 },
  ]),
  ...scatter("l2-gallery-plant", PLANT, [
    { x: 35.2, z: 3.0 },
    { x: 35.2, z: 15.0 },
  ]),
];

const L2_ZONES: Zone[] = [
  // Spatial, so both audiences get them — the same rule the level below follows.
  {
    id: "l2-zone-studio",
    name: "Studio",
    outline: rect(L2_STUDIO_W, 0, L2_LIBRARY_W, SPINE_N),
    colorKey: "team-b",
    audience: "private",
  },
  {
    id: "l2-zone-quiet",
    name: "Quiet",
    outline: rect(L2_LIBRARY_W, 0, BLOCK_E, SPINE_N),
    colorKey: "focus",
  },
  {
    id: "l2-zone-gallery",
    name: "Gallery",
    outline: rect(BLOCK_E, 2.0, GALLERY_E, 16.0),
    colorKey: "social",
  },
];

// ---- Viewpoints -----------------------------------------------------------

/**
 * Five poses, and the first one is where you arrive.
 *
 * `viewpoints[0]` is the arrival pose. `interiors/types.ts` suggests putting
 * reception there, and this pack does not: arriving inside a room before you
 * have seen the shape of the building is disorienting in a way that a plan view
 * is not, so the establishing shot goes first and reception is second. That is a
 * choice a pack gets to make, which is the point of the field being an order
 * rather than an id.
 *
 * `distance` is metres from the target to the camera and `height` is metres
 * above this level's floor. An office is not a city: 30 m is the whole building
 * and 6 m is standing on a mezzanine, where the equivalent city numbers are in
 * the hundreds.
 */
const VIEWPOINTS: Viewpoint[] = [
  {
    id: "floor",
    number: "01",
    label: "The Whole Floor",
    shortLabel: "The Floor",
    levelId: "level-1",
    // Aimed at the middle of the *whole plate* rather than of the block, and
    // pulled back far enough to hold 48 m of it. Both numbers moved when the
    // wing did: an establishing shot that still framed 34 m put the commons
    // half off the right-hand edge, which is a poor introduction to the largest
    // room in the building.
    // Higher and further back than it was, because the building got taller
    // rather than wider: at a 14 m interstitial the roof is 19.6 m up, and a
    // camera at 19 m was level with it and looking at the parapet.
    focus: { at: { x: 24.0, z: 9.0 }, distance: 58, height: 34, rotation: 0.55 },
    description:
      "Forty-eight metres by eighteen, two storeys a long way apart, glazed along the north edge and open to the roof at the east end. Everything in this building is somewhere in this frame.",
  },
  {
    id: "reception",
    number: "02",
    label: "Reception",
    shortLabel: "Reception",
    levelId: "level-1",
    focus: { at: { x: 3.8, z: 5.4 }, distance: 8.5, height: 2.0, rotation: 1.9 },
    description:
      "Four metres in from the entrance, looking back at the door. The desk faces the way you came in, which is the only thing a reception desk has to do.",
  },
  {
    id: "desks",
    number: "03",
    label: "The Desk Floor",
    shortLabel: "Desks",
    levelId: "level-1",
    focus: { at: { x: 16.4, z: 4.0 }, distance: 14, height: 4.4, rotation: 0.35 },
    description:
      "Thirty-six seats in four benches, two rows each, facing each other across a shared run of desktop. The window bench is eng-01 through eng-12.",
  },
  {
    id: "alcatraz",
    number: "04",
    label: "Alcatraz",
    shortLabel: "Alcatraz",
    levelId: "level-1",
    focus: { at: { x: 3.6, z: 15.2 }, distance: 6.5, height: 2.3, rotation: 5.6 },
    description:
      "The board room: ten seats, a vision panel onto the corridor and a window onto whatever is south of here. The largest of the three meeting rooms.",
  },
  {
    id: "kitchen",
    number: "05",
    label: "The Kitchen",
    shortLabel: "Kitchen",
    levelId: "level-1",
    focus: { at: { x: 29.8, z: 7.6 }, distance: 8.0, height: 2.6, rotation: 2.6 },
    description:
      "The social corner, where the lounge and the kitchen share a glazed edge and the only wall between them and the desk floor is the one with the archway in it.",
  },
  {
    id: "commons",
    number: "06",
    label: "The Commons",
    shortLabel: "Commons",
    levelId: "level-1",
    // Low and looking up the length of the wing, because the thing worth seeing
    // here is the height. An establishing shot from above would show a big empty
    // floor and none of what makes it big.
    focus: { at: { x: 41.0, z: 9.0 }, distance: 15, height: 3.2, rotation: 2.1 },
    description:
      "Fourteen metres of floor open all the way to the roof, with the gallery cutting across one side of it. The lounge and the kitchen look into this through the glass that used to be the outside wall.",
  },
  {
    id: "studio",
    number: "07",
    label: "The Studio",
    shortLabel: "Studio",
    levelId: "level-2",
    focus: { at: { x: 15.6, z: 4.6 }, distance: 15, height: 4.6, rotation: 0.35 },
    description:
      "Level 2: nineteen metres of unbroken north-lit floor and twenty-four seats in two benches. The one run downstairs cannot offer, because the lobby wall and the booths interrupt it.",
  },
  {
    id: "gallery",
    number: "08",
    label: "The Gallery",
    shortLabel: "Gallery",
    levelId: "level-2",
    // Standing at the rail rather than behind it, which is the pose that shows
    // there is something underneath.
    focus: { at: { x: 37.0, z: 9.0 }, distance: 9, height: 1.8, rotation: 1.4 },
    description:
      "The balcony over the commons, four metres of it, with a glass balustrade and nothing below for another seventeen. The reason the void is a volume rather than a tall room.",
  },
];

// ---- The pack -------------------------------------------------------------

const LEVEL_1: Level = {
  id: "level-1",
  name: "Level 1",
  elevation: 0,
  wallHeight: CEILING,
  wallThickness: INT_THICKNESS,
  wallSurface: PAINT,
  floorplan: {
    rooms: ROOMS,
    walls: WALLS,
    props: PROPS,
    deskBanks: DESK_BANKS,
    seats: SEATS,
    zones: ZONES,
  },
};

/**
 * The upper storey, at `STOREY` above the ground floor.
 *
 * `elevation` is floor-to-*floor* and not floor-to-ceiling, which is the number
 * people get wrong: 2.8 m would put level 2's slab in level 1's ceiling void.
 * `Plan` adds this to every coordinate on the level exactly once, so the
 * `L2_*` section above is authored with its own floor at zero and neither storey
 * has to know the other's height.
 */
const LEVEL_2: Level = {
  id: "level-2",
  name: "Level 2",
  elevation: STOREY,
  wallHeight: CEILING,
  wallThickness: INT_THICKNESS,
  wallSurface: PAINT,
  floorplan: {
    rooms: L2_ROOMS,
    walls: L2_WALLS,
    props: L2_PROPS,
    deskBanks: L2_DESK_BANKS,
    seats: L2_SEATS,
    zones: L2_ZONES,
  },
};

export const LUMBRIDGE_HQ: Office = {
  id: "lumbridge-hq",
  name: "Lumbridge HQ",
  levels: [LEVEL_1, LEVEL_2],
  viewpoints: VIEWPOINTS,
  /**
   * High in a downtown tower, on the same board the city view draws.
   *
   * The coordinate is a block in the Transbay cluster, typed by hand from the
   * street grid — not geocoded, for the reason CONTRACT.md §8 gives at length.
   * `elevation` is what makes it a *tower*: 188 m is around the forty-eighth
   * floor, so the horizon sits a long way down and the fog outside the glass is
   * the fog you look across a city at rather than through.
   *
   * `heading: 205` turns the glazed façade — `ext-north`, the wall this pack has
   * always called the daylight side — to face south-south-west. That is the one
   * number that decides whether the sun ever actually enters the building, and
   * pointing the glass at the afternoon is the whole reason the field exists.
   */
  site: LUMBRIDGE_HQ_SITE,
  meta: {
    description:
      "The reference office: two levels around a double-height commons, twenty-six rooms, a hundred and twenty-six seats. Copy this file, change the numbers, keep the seat ids.",
    author: "Lumbridge",
    // The art in this repo is Apache-2.0 with the artistic output additionally
    // dedicated under CC0-1.0 (CONTRACT.md §3.1). A pack is artistic output, so
    // this one says CC0 and means it: take the plan, take the seat ids, take the
    // whole building, and owe nobody anything.
    license: "CC0-1.0",
    version: "1.0.0",
    updated: "2026-08-04",
  },
};

export default LUMBRIDGE_HQ;
