/** * 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 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. ...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;