Files
tera/src/offices/lumbridge-hq.ts
T
Karti Tripathi d464459838 Spaces: the inside of the world, and a sun that is actually where it should be
Ten agents wrote this in parallel against CONTRACT.md, which exists because the
five design agents before them collided on fifteen blocking points — four files
specified twice with incompatible contents, three separate backends for one box,
and `Environment` exported twice meaning different things.

What landed: a Stage owning only the renderer and the loop, with the city and an
office as two scenes over it. They cannot share one — San Francisco is ~94 m per
scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and
the city is paused rather than disposed on the way in, because rebuilding its
336,864-point heightfield costs about a second on the way back out.

Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six
seats, and it is the file a self-hoster copies. Walls are a segment list with
1-D openings, so doors and windows are holes punched in a wall rather than
placed objects, and the pass that splits a wall around its openings hands the
walk-mode collider its segments for free.

The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at
all — not even three.js — so time of day keeps working on a laptop in a field.
Verified against known values: 75.45 degrees at the June solstice in SF, 28.79
at December, sunset at 03:15Z. The first screenshot after wiring it was a black
rectangle, which turned out to be correct: it was midnight in San Francisco.

Presence binds to a seat id and never to a coordinate. The pack knows where
`eng-04` is; who is sitting in it is private data behind an API. Same shape as
the marker rule, one level in.

Two corrections to ARCHITECTURE.md are in here. Containment does not discharge
ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database
wherever the rows live, so the rule is about the geocoder (US Census, public
domain) and not the storage. And a person at a desk is not a Marker; markers are
geographic.

One contract gap surfaced only in a screenshot: two agents read `height` on a
viewpoint differently, so the establishing shot aimed at empty air fourteen
metres above the roof. It now means what the same field means for a city.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 00:11:01 -07:00

1293 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*
* ### 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,
DeskBank,
Level,
Office,
Opening,
Outline,
Point2,
Prop,
Room,
Seat,
Viewpoint,
Wall,
Yaw,
Zone,
} from "../interiors/types.ts";
// ---- The floor plate ------------------------------------------------------
/**
* The building, in six numbers. Everything else in this file is measured off
* these, so a self-hoster with a different slab starts here.
*
* 34 x 18 m is 612 m², which is 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.
*/
const WIDTH = 34.0;
const DEPTH = 18.0;
/** Floor to underside of ceiling. 2.8 m is an ordinary commercial storey. */
const CEILING = 2.8;
/**
* 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;
// ---- 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 } = {},
): 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 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, WIDTH, KITCHEN_N),
floor: CARPET_ACCENT,
ceiling: null,
},
{
id: "kitchen",
name: "The Kitchen",
outline: rect(SOCIAL_W, KITCHEN_N, WIDTH, 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, WIDTH, 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, WIDTH, DEPTH),
floor: CONCRETE,
ceiling: { height: 2.6, surface: CEILING_TILE },
},
];
// ---- 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: WIDTH, 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: "ext-east-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 on the
// pavement.
from: { x: WIDTH, z: 0 },
to: { x: WIDTH, z: SPINE_S },
thickness: EXT_THICKNESS,
surface: GLASS,
},
{
id: "ext-east-solid",
from: { x: WIDTH, z: SPINE_S },
to: { x: WIDTH, z: DEPTH },
thickness: EXT_THICKNESS,
surface: PAINT,
},
{
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: WIDTH, 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: WIDTH, 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: WIDTH, 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 1619.
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.
...scatter("mdf-rack-n", LOCKER, grid(27.4, 13.4, 2, 1, 1.3, 0), { rotation: NORTH }),
...scatter("mdf-rack-s", LOCKER, grid(27.4, 15.8, 2, 1, 1.3, 0), { rotation: SOUTH }),
{ id: "mdf-shelf", kind: SHELF, position: { x: 29.4, z: DEPTH - EXT_FACE - 0.18 }, rotation: SOUTH },
...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(WIDTH - 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 }),
];
// ---- 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.
*/
const ZONES: Zone[] = [
{ id: "zone-eng", name: "Engineering", outline: rect(8.0, 0.6, 18.9, 7.6), colorKey: "team-a" },
{ id: "zone-studio", name: "Studio", outline: rect(19.4, 0.6, 25.2, 7.6), colorKey: "team-b" },
{ id: "zone-social", name: "Social", outline: rect(SOCIAL_W, 0, WIDTH, SPINE_N), colorKey: "social" },
{ id: "zone-focus", name: "Focus", outline: rect(X_BOOTH_1, BOOTH_N, X_BOOTH_E, SPINE_N), colorKey: "focus" },
];
// ---- 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",
focus: { at: { x: 17.0, z: 9.0 }, distance: 32, height: 14, rotation: 0.55 },
description:
"Thirty-four metres by eighteen, one storey, glazed along the north edge. 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.",
},
];
// ---- The pack -------------------------------------------------------------
const LEVEL_1: Level = {
id: "level-1",
name: "Level 1",
// Ground. A second storey would repeat this object with `elevation: 4.2` —
// storey height, not ceiling height — and `Plan` adds the offset to every
// coordinate it resolves, exactly once, so both levels can go into one scene
// group sitting at the origin.
elevation: 0,
wallHeight: CEILING,
wallThickness: INT_THICKNESS,
wallSurface: PAINT,
floorplan: {
rooms: ROOMS,
walls: WALLS,
props: PROPS,
deskBanks: DESK_BANKS,
seats: SEATS,
zones: ZONES,
},
};
export const LUMBRIDGE_HQ: Office = {
id: "lumbridge-hq",
name: "Lumbridge HQ",
levels: [LEVEL_1],
viewpoints: VIEWPOINTS,
meta: {
description:
"The reference office: one level, fifteen rooms, seventy-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;