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>
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
# Authoring an office
|
||||
|
||||
An **office pack** is one JSON-shaped object describing the inside of a
|
||||
building: floor slabs, walls, holes in the walls, furniture, seats, and a few
|
||||
camera poses. `src/interiors/types.ts` is the contract — it is short, it is
|
||||
commented, and it wins any argument with this document. `lumbridge-hq.ts` in
|
||||
this directory is a worked example of every feature described here, and copying
|
||||
it is the intended way to start.
|
||||
|
||||
Nothing in a pack requires an account, a key or a network. If you can run the
|
||||
repo you can author an office, and if you can author an office you can hand
|
||||
someone the file.
|
||||
|
||||
## The shape of it
|
||||
|
||||
```ts
|
||||
import type { Office } from "../interiors/types.ts";
|
||||
|
||||
export const ACME_HQ: Office = {
|
||||
id: "acme-hq",
|
||||
name: "Acme HQ",
|
||||
levels: [
|
||||
{
|
||||
id: "level-1",
|
||||
name: "Level 1",
|
||||
elevation: 0, // floor height above the office origin
|
||||
wallHeight: 2.8, // the storey's default wall top
|
||||
wallThickness: 0.12,
|
||||
wallSurface: "tera:paint.matt",
|
||||
floorplan: {
|
||||
rooms: [...], // required
|
||||
walls: [...], // required
|
||||
props: [...], // optional, read as [] when absent
|
||||
deskBanks: [...],
|
||||
seats: [...],
|
||||
zones: [...],
|
||||
},
|
||||
},
|
||||
],
|
||||
viewpoints: [...], // viewpoints[0] is where you arrive
|
||||
meta: { author: "you", license: "CC0-1.0" },
|
||||
};
|
||||
```
|
||||
|
||||
`Office` must stay **strictly JSON-serialisable**: no functions, no classes, no
|
||||
`Date`, no `THREE` types. A pack you hand-write as a `.ts` module and a pack
|
||||
that arrives as a `.json` body over HTTP have to be literally the same thing,
|
||||
because the server (`GET /api/v1/offices/:id`) serves exactly this object inside
|
||||
an `OfficeDoc`.
|
||||
|
||||
Helper *functions* in your source file are fine — `lumbridge-hq.ts` uses five,
|
||||
and they all run at module load and return plain objects. The rule is about the
|
||||
value, not the file.
|
||||
|
||||
## The coordinate frame
|
||||
|
||||
Metres, `1 unit = 1 m`. The floor is the **XZ plane** with **+Y up**, which is
|
||||
three.js's convention with no conversion anywhere.
|
||||
|
||||
"Plan view" throughout means looking down at that plane with **+X to the right
|
||||
and +Z down the page**. That is the one thing worth internalising, because +Z
|
||||
going *down* is what makes the winding rule counter-intuitive:
|
||||
|
||||
- Outlines are **implicitly closed** — do not repeat the first point.
|
||||
- Author them **counter-clockwise in plan view**, which is NW → SW → SE → NE for
|
||||
a rectangle, and which has a *negative* shoelace area over `(x, z)`.
|
||||
- Get it wrong and nothing breaks: `Plan` silently re-winds a reversed polygon.
|
||||
|
||||
An office has no orientation on the earth. Calling an edge "north" is a
|
||||
convenience for reading your own file. Offices get no `Atmosphere`, no sun and
|
||||
no sky (CONTRACT.md §4); interior lighting is a fixed rig owned by the scene.
|
||||
|
||||
## Rooms are slabs. Walls are segments.
|
||||
|
||||
This is the load-bearing idea, and the thing most people get backwards on the
|
||||
first attempt.
|
||||
|
||||
A **`Room` is a floor finish with a name and a polygon. It implies no walls.**
|
||||
A **`Wall` is a separate straight segment that belongs to no room** and stands
|
||||
wherever you put it. The two lists are not derived from each other.
|
||||
|
||||
Consequences worth stating out loud:
|
||||
|
||||
- An open plan is simply rooms with nothing standing between them. In the
|
||||
reference pack the lounge, the kitchen and the desk floor share three edges and
|
||||
only one of those edges carries a wall.
|
||||
- A room can be a corridor, a lift lobby, a zone of different carpet. Give it an
|
||||
id if you ever want to name it.
|
||||
- Rooms may overlap and may leave gaps. `Plan.roomAt` resolves *later* rooms
|
||||
first, so a room declared after another wins the lookup where they cross.
|
||||
Overlapping is legal but the reference pack avoids it — two coplanar slabs at
|
||||
the same height is a z-fight waiting for the wrong GPU, so the open floor is
|
||||
notched around the focus booths rather than passing underneath them.
|
||||
|
||||
Rooms and walls should be authored against the **same numbers**. A wall is
|
||||
centred on its line and straddles the boundary between the two slabs meeting
|
||||
there. If the slab edge says `10.4` and the wall line says `10.42` you get a
|
||||
seam you will never find again.
|
||||
|
||||
### Ceilings
|
||||
|
||||
`ceiling` omitted gives a ceiling at the level's `wallHeight`. `ceiling: null`
|
||||
means **no ceiling at all** — an atrium, a void, or a room you want to look down
|
||||
into. `{ height, surface }` overrides one room.
|
||||
|
||||
An office you look down into from an establishing viewpoint cannot have lids on
|
||||
the rooms you are trying to see, so most of the reference pack declares
|
||||
`ceiling: null`. The three that keep theirs — the focus booths, the server room,
|
||||
the store — are the three you are never meant to see inside, and a ceiling is a
|
||||
cheap way of saying so.
|
||||
|
||||
## Doors and windows are openings, not props
|
||||
|
||||
There is no `tera:shell.door`. A door is a 1-D interval punched out of a wall:
|
||||
|
||||
```ts
|
||||
{ kind: "door", start: 5.6, width: 0.9, sill: 0, head: 2.1 }
|
||||
```
|
||||
|
||||
`start` is measured **from the wall's `from` end**, along the wall, in metres.
|
||||
That is the whole of the horizontal placement, which is why an opening can never
|
||||
drift off its wall. It also means **the direction you write a wall in is the
|
||||
direction its openings are measured in** — author every wall consistently (the
|
||||
reference pack goes west-to-east and north-to-south, without exception) or you
|
||||
will eventually put a door at the wrong end of a room.
|
||||
|
||||
`sill` and `head` are metres above the level's floor, and both are required.
|
||||
There are deliberately no per-kind defaults: a contract where the numbers you
|
||||
read are not the numbers you get is worse than a contract that makes you type
|
||||
`sill: 0`. Typical values:
|
||||
|
||||
| kind | sill | head | note |
|
||||
| --- | --- | --- | --- |
|
||||
| `door` | 0 | 2.1 | 0.9 m wide for a single leaf, 1.8 m for a pair |
|
||||
| `arch` | 0 | 2.4 | a cased opening with no leaf |
|
||||
| `window` | 0.9 | 2.2 | punched window |
|
||||
| `window` | 0.75 | 2.35 | ribbon glazing on a façade |
|
||||
|
||||
### The passability rule, which is the whole reason for this design
|
||||
|
||||
The pass that splits a wall around its openings has to run anyway to produce the
|
||||
solid runs you can see. Running it once also produces the **walk-mode collision
|
||||
segments for free**, with gaps in exactly the places you can walk through.
|
||||
|
||||
An opening is a way through **iff `sill <= 0` and `head >= 1.1 m`**. There is no
|
||||
per-kind special-casing: a door passes, an arch passes, a window with a 0.9 m
|
||||
sill does not, a serving hatch does not. Which means a glazed opening at ankle
|
||||
height quietly becomes a hole in your wall that people walk through, so keep
|
||||
sills honest.
|
||||
|
||||
`Plan.blocked(levelId, from, to, radius)` is the test to use from a walk
|
||||
controller; do not re-derive the inflation yourself.
|
||||
|
||||
### Glass walls versus window openings
|
||||
|
||||
Full-height glazing is a **wall with a glass surface**, not one enormous
|
||||
opening:
|
||||
|
||||
```ts
|
||||
{ id: "ext-east", from: { x: 34, z: 0 }, to: { x: 34, z: 12.2 }, surface: "tera:glass.curtain" }
|
||||
```
|
||||
|
||||
An opening is a hole, and a hole is something you can sometimes walk through. A
|
||||
curtain wall is a solid you happen to be able to see through. Modelling one as
|
||||
the other hands the collider a twelve-metre gap and puts your lounge on the
|
||||
pavement.
|
||||
|
||||
## Desk banks
|
||||
|
||||
Hundreds of hand-written desk literals is not a file anybody edits twice. A
|
||||
`DeskBank` is one declaration that `Plan` expands into a desk prop, a chair prop
|
||||
and a seat per station:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: "eng",
|
||||
desk: "tera:desk.workstation",
|
||||
chair: "tera:seat.task-chair",
|
||||
origin: { x: 9.2, z: 1.9 }, // centre of station (1, 1), before rotation
|
||||
rotation: 0,
|
||||
columns: 6, rows: 2,
|
||||
pitch: 1.7, // centre-to-centre across
|
||||
rowPitch: 0.85, // centre-to-centre down; defaults to `pitch`
|
||||
facingRows: true,
|
||||
seatOffset: 0.6, // desk centre to seat; defaults to 0.6
|
||||
pose: "sit", // defaults to "sit"
|
||||
seatPrefix: undefined, // defaults to `id`
|
||||
}
|
||||
```
|
||||
|
||||
The grid is laid out in the bank's own frame — `columns` along local +X, `rows`
|
||||
along local +Z — then rotated by `rotation` about `origin`. Rotating a bank does
|
||||
not move the origin to a corner of the building; the origin stays station
|
||||
(1, 1). At `rotation: Math.PI` the bank's local +X is world −X, so the origin is
|
||||
the *east* end of the run.
|
||||
|
||||
**The generated ids are contractual**, because a `Presence` binds to a seat id
|
||||
and you have to be able to predict it without running anything. Stations are
|
||||
numbered from 1, along each row and then down the rows, zero-padded to two:
|
||||
|
||||
- seat `${seatPrefix ?? id}-01`, `-02`, …
|
||||
- desk prop `${id}-desk-01`, chair prop `${id}-chair-01`
|
||||
|
||||
So the bank above gives you `eng-01` … `eng-12`, and `eng-04` is the fourth desk
|
||||
in the front row.
|
||||
|
||||
### Why every bench in the reference pack is two rows
|
||||
|
||||
`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. **A second bench is a
|
||||
second bank.** That is a real limit of the format, not an oversight.
|
||||
|
||||
Seat ids restart at `01` in every bank, so two banks that want to share a
|
||||
numbering series cannot; give them distinct prefixes (`eng`, `ops`, `design`)
|
||||
rather than trying.
|
||||
|
||||
A bank places a desk and a chair and **no third thing**. If you want a monitor
|
||||
on every desk, do not write thirty-nine monitor props that have to be kept in
|
||||
step with a bank you will move next week — register your own desk asset (see
|
||||
below) that builds a desk with a monitor on it, and every station in every bank
|
||||
in every pack gets one.
|
||||
|
||||
## Seats
|
||||
|
||||
```ts
|
||||
{ id: "bernal-04", position: { x: 10.8, z: 14.1 }, facing: Math.PI / 2, pose: "sit" }
|
||||
```
|
||||
|
||||
**A seat is an address, not a chair.** The chair is a separate prop that happens
|
||||
to be at the same coordinate; a room with no chairs in it can still have seats,
|
||||
and a chair with nobody's name on it needs no seat.
|
||||
|
||||
This is the most important thing in the format. A `Presence` — somebody at a
|
||||
desk — binds to a **`seatId` and never to a coordinate**, and never appears in a
|
||||
pack. The pack knows where `eng-04` is; a private API knows who is sitting in
|
||||
it; neither knows the other. That is what lets occupancy be private data behind
|
||||
authentication while the geometry stays public, open-source and copyable. If a
|
||||
presence carried an `{x, z}`, publishing the building and publishing the people
|
||||
would be the same act, and one of them could never be published at all.
|
||||
|
||||
So: **keep seat ids stable across edits**, for the same reason street numbers
|
||||
survive repainting the house. Move the table 200 mm; do not renumber the seats.
|
||||
|
||||
`facing` is where the occupant looks. `pose` is `"sit"` or `"stand"` and is the
|
||||
only thing that tells a consumer how tall to draw an occupant — people perch at
|
||||
a kitchen island, so those seats are `"stand"`.
|
||||
|
||||
## Props, and which way things face
|
||||
|
||||
```ts
|
||||
{ id: "lobby-monitor", kind: "tera:screen.monitor", position: { x: 3.4, z: 5.2 },
|
||||
rotation: Math.PI / 2, elevation: 0.73, scale: 1, colorKey: "accent", seat: "reception-01" }
|
||||
```
|
||||
|
||||
`kind` is an `AssetId`. The prop registry and the asset registry are the same
|
||||
registry — there is no separate table of things you are allowed to put in a
|
||||
room, and an unregistered id resolves to a placeholder box rather than throwing,
|
||||
so one typo does not stop the office opening.
|
||||
|
||||
`position` is on the floor plane; `elevation` is metres above the level's floor
|
||||
and is omitted for the ninety per cent of things that stand on it. `colorKey` is
|
||||
an **opaque** palette key resolved by the consuming app, exactly as
|
||||
`Pin.colorKey` is — the engine will never learn that `"focus"` means a quiet
|
||||
booth. `seat` binds a prop to a seat id as an address, which is what lets an
|
||||
occupancy layer dim the empty chairs without knowing which mesh is which.
|
||||
|
||||
### The yaw convention
|
||||
|
||||
`Yaw` is radians about +Y. **Zero faces −Z**, and the angle increases
|
||||
counter-clockwise seen from above. That is exactly `object.rotation.y`, and
|
||||
nothing anywhere converts it — there is no sign flip between your file and the
|
||||
scene graph. (The city's `District.gridAngle` uses degrees clockwise from north,
|
||||
which reads better for a map; interiors deliberately does not.)
|
||||
|
||||
An asset is built with its **origin at the centre of its footprint on the
|
||||
floor**, facing −Z at yaw zero, and its **working side at +Z** — drawer fronts,
|
||||
the open front of a shelf, a monitor's glass, a whiteboard's writing face — with
|
||||
the solid back of anything that stands against a wall at −Z.
|
||||
|
||||
Those combine into one rule that covers every prop:
|
||||
|
||||
> A prop takes the yaw of **the wall it backs onto**. A seat takes the yaw of
|
||||
> **the direction the occupant looks**.
|
||||
|
||||
A shelf against the north wall is `0`. A locker against the east wall is
|
||||
`-Math.PI / 2`. A desk against the south wall is `Math.PI`, and the person at it
|
||||
is also `Math.PI`, looking south at it. This is why a `DeskBank` can hand one
|
||||
`rotation` to its desk, its chair and the seat between them.
|
||||
|
||||
### The one exception: ceiling fixtures
|
||||
|
||||
`tera:light.pendant` and `tera:light.troffer` are authored with their origin at
|
||||
the **mounting plane** and all their geometry below it. Write `elevation: 2.8`
|
||||
and you get a fitting hanging from a 2.8 m ceiling, rather than a fitting whose
|
||||
author had to know your ceiling height. Everything else, including things that
|
||||
stand on a desk or hang on a wall, is authored on the floor.
|
||||
|
||||
Light fittings emit **no light**. The interior rig belongs to the scene
|
||||
(CONTRACT.md §4).
|
||||
|
||||
### Wall-mounted things
|
||||
|
||||
A prop's origin is the centre of its footprint, so a panel hangs *on* a wall
|
||||
only if you place its centre half its own depth off the wall's face:
|
||||
`wallLine + thickness / 2 + depth / 2`. `tera:screen.wall-display` is 0.12 m
|
||||
deep and `tera:whiteboard` is 0.10 m. Name those offsets as constants; you will
|
||||
use them a dozen times.
|
||||
|
||||
## Zones
|
||||
|
||||
A named region of floor with an opaque `colorKey`, no behaviour and no effect on
|
||||
geometry. A consuming app 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, which is why there is no `kind` field to be tempted by.
|
||||
|
||||
## Viewpoints
|
||||
|
||||
```ts
|
||||
{ id: "floor", label: "The Whole Floor", shortLabel: "The Floor", number: "01",
|
||||
description: "…", levelId: "level-1",
|
||||
focus: { at: { x: 17, z: 9 }, distance: 32, height: 14, rotation: 0.55 } }
|
||||
```
|
||||
|
||||
`distance` is metres from the target to the camera; `height` is metres above the
|
||||
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.
|
||||
|
||||
**`viewpoints[0]` is the arrival pose.** Reception is the obvious choice; the
|
||||
reference pack puts the establishing shot there instead, on the grounds that
|
||||
arriving inside a room before you have seen the shape of the building is
|
||||
disorienting. Either is fine — that is a choice a pack gets to make, which is
|
||||
why the field is an order and not an id.
|
||||
|
||||
## Surfaces
|
||||
|
||||
`Room.floor`, `Wall.surface`, `RoomCeiling.surface` and `Level.wallSurface` take
|
||||
a `SurfaceId` string. `MaterialRegistry.resolve` throws away the namespace,
|
||||
reads the **first dot-segment** as a role name, tries a small alias table, and
|
||||
falls back rather than throwing:
|
||||
|
||||
```
|
||||
tera:carpet.loop -> carpet
|
||||
acme:carpet.broadloom -> carpet
|
||||
tera:wood.plank -> woodFloor (via the alias table)
|
||||
tera:glass.curtain -> glazing (alias)
|
||||
tera:carpetAccent.x -> carpetAccent (an exact role name)
|
||||
tera:nonsense.at.all -> the caller's fallback
|
||||
```
|
||||
|
||||
The closed list of roles lives in `src/assets/materials.ts`. Aliases today
|
||||
include `paint`, `plasterboard`, `wall`, `wood`, `timber`, `concrete`, `glass`,
|
||||
`ceiling`, `felt`, `fabric`, `laminate`, `steel`, `metal`, `aluminium`,
|
||||
`screen`, `plant`.
|
||||
|
||||
There is deliberately **no per-instance tint on a surface**, unlike
|
||||
`Prop.colorKey`. One blue meeting room is a *material* — register
|
||||
`acme:paint.blue` and point the wall at it. One red chair in a row of grey ones
|
||||
is genuinely an instance.
|
||||
|
||||
## Registering your own assets instead of forking
|
||||
|
||||
You do not fork this repo to change what a desk looks like. Register your own
|
||||
asset with an `overrides:` pointing at the built-in id:
|
||||
|
||||
```ts
|
||||
import { defineAsset, kit } from "../assets/kit.ts";
|
||||
|
||||
type StandingParams = { width: number; depth: number; height: number };
|
||||
|
||||
kit.register(defineAsset<StandingParams>({
|
||||
id: "acme:desk.standing",
|
||||
overrides: "tera:desk.workstation", // ← every reference to the tera id resolves here
|
||||
label: "Standing desk",
|
||||
defaults: { width: 1.6, depth: 0.8, height: 1.05 },
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.height, clearance: 0.9 };
|
||||
},
|
||||
build(p, ctx) {
|
||||
/* compose cached unit primitives from ctx.parts into a MeshBin */
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
Declare the parameter type as a `type` alias and not an `interface`: an
|
||||
`interface` has no implicit index signature and will not satisfy `AssetParams`.
|
||||
|
||||
Every pack in the world that says `tera:desk.workstation` — including the
|
||||
reference office, unmodified — now draws yours. That is the mechanism working as
|
||||
designed: reskin, do not fork. Point an id at your own namespace *in a pack*
|
||||
only when you want that one building to differ.
|
||||
|
||||
**Everything is procedural.** A mesh is a function composing cached unit
|
||||
primitives; a texture is drawn on a 2D canvas from seeded noise. **No binary art
|
||||
is ever committed to `src/`** — no `.glb`, no `.png`, no fonts, no logos. This
|
||||
is the same property that gives the city zero asset-licensing exposure and it is
|
||||
worth more than the quality ceiling it costs. Your *own* binary assets are your
|
||||
business: `public/props/`, `public/kits/` and `public/offices/` are hard-exempt
|
||||
from the repo's binary gate and gitignored as self-hoster space.
|
||||
|
||||
If you contribute an asset back, `CONTRIBUTING.md` and `src/assets/LICENSE-ART`
|
||||
are the terms: Apache-2.0 on the code, with the artistic output additionally
|
||||
dedicated under CC0-1.0.
|
||||
|
||||
## Validation: what `Plan` forgives and what it drops
|
||||
|
||||
`new Plan(office)` resolves the whole thing once and never throws. Every
|
||||
complaint lands in `plan.problems`, so a pack can be asserted on in a test
|
||||
without capturing console output. In a dev build it also warns.
|
||||
|
||||
**Dropped** (the offending item disappears, the rest of the pack survives):
|
||||
|
||||
- self-intersecting or degenerate room outlines, zero-length walls
|
||||
- an opening that runs past either end of its wall, or overlaps another opening
|
||||
- `head <= sill`
|
||||
- a desk bank with fewer than one station, or a non-positive pitch
|
||||
- a viewpoint on a level that does not exist
|
||||
- duplicate ids — **ids are unique per kind and building-wide, not per level**,
|
||||
because a `Presence` binds to a seat id and an occupancy layer dims a prop id,
|
||||
so both have to mean one thing in the building. Later loses.
|
||||
|
||||
**Repaired** (silently, and recorded):
|
||||
|
||||
- an outline hand-closed with a duplicated first point
|
||||
- an outline wound the wrong way
|
||||
- a negative sill clamped to the floor; a head above the wall clamped down
|
||||
- a prop bound to an unknown seat id — the binding is cleared, the prop stays
|
||||
|
||||
Missing required arrays read as `[]`, because a pack that arrived over HTTP has
|
||||
been through no type checker.
|
||||
|
||||
Opening ids are `"${wallId}#${authoredIndex}"` using the **authored** index, so
|
||||
dropping one bad opening does not renumber its siblings.
|
||||
|
||||
## Shipping a pack
|
||||
|
||||
- **In the browser.** Import it and hand it to `Plan`. The reference office is
|
||||
the default; that is the zero-config path and it needs nothing else.
|
||||
- **Over HTTP.** Drop the JSON in the server's office directory and it is served
|
||||
at `GET /api/v1/offices/:id` wrapped in an `OfficeDoc`
|
||||
(`{ id, name, floor, visibility, updated? }`, defined in `src/server/wire.ts`).
|
||||
A pack that omits `visibility` is treated as **private**, and a private office
|
||||
returns **404, not 403**, so the endpoint cannot be used to enumerate what
|
||||
exists.
|
||||
|
||||
## A checklist before you call it done
|
||||
|
||||
1. `new Plan(office).problems` is empty.
|
||||
2. Every room you can walk into has a `door` or `arch` with `sill: 0` reaching
|
||||
at least 1.1 m of head. Walk the graph, or spot-check with `Plan.blocked`.
|
||||
3. No window opening has `sill: 0` unless you meant a doorway.
|
||||
4. Seat ids are the ones you are willing to live with for a year.
|
||||
5. Corridors are at least 1.2 m clear, doors 0.9 m, desks 1.4–1.6 m. Numbers a
|
||||
person would recognise are the whole difference between a floor plan and a
|
||||
diagram.
|
||||
6. Nothing binary landed under `src/`.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user