feat: tone-mapped render rig, studio devices, LA fidelity pass, UI overhaul
The build the studios needed, across eight workstreams and one strict file partition. **The render rig was the quality ceiling.** The renderer ran three's NoToneMapping default while atmosphere drove the sun to 2.35 and assets set emissives to 3.2, so every value above 1.0 hard-clipped to flat white — which is why walls blew out and every fitting looked like a white rectangle. ACES filmic tone mapping and an explicit output colour space land in `stage.ts`, and the atmosphere intensity table and palette headroom are re-tuned against the new curve rather than left tuned for the clipping we removed. `engine/environmentRig.ts` builds a PMREM environment at runtime, procedurally, so nothing binary is committed. There was no environment map anywhere before, so every `metalness > 0` role had nothing to reflect and rendered dull grey — a defect the code already documented against itself in `office/optimus.ts`, where a whole material role was abandoned over it, and worked around in `modelX.ts` with a fake emissive that this change deletes. Atmosphere remains the sole light owner; the rig derives from the `LightingState` it already produced. **Studio hardware exists.** There was no device concept anywhere in the product: no type, no route, no state. `devices/types.ts` fixes a declaration/state/ capability/command contract that a smart light, a thermostat, a door sensor and a charger all fit without a schema change, and both studios now carry a desk mic and a computer speaker with deterministic simulated behaviour behind an adapter seam a real API can occupy later. Reads are the demo and are open; commands are a signed-in action and are kept off the read body entirely, because a shared cache replaying a GET that turned a microphone on is exactly what the fail-closed cache default exists to prevent. **The ADS-B licence hole is closed.** `TERA_ADSB_ENDPOINT` accepted any URL, the response was served publicly cacheable, and the attribution hardcoded adsb.lol regardless of where the endpoint pointed — one env var away from republishing non-redistributable data under an open-terms credit. The host is now allowlisted, the credit is derived from the host actually configured, public cacheability is conditional on redistributability, and a refused endpoint demotes to simulated flights and says so in `degraded[]`. The gate is on the source, not the feature: live aircraft and their detail cards stay open to anonymous visitors. **The LA studio was never the smaller pack** — 16 rooms and 248 props against SF's 4 and 28. Its deficit was fidelity per square metre: 98 of those props were ceiling troffers, it bound no props to seats, placed none of the habitat kit, and 12 of its 16 rooms had no viewpoint. Density comes from new asset kinds rather than more instances, because `furnish.ts` draws once per kind and folds colour into the batch key, so repeat instances add nothing the eye can read. **The interface stops being forty imperative mutations.** Every visibility decision moves into a pure, tested `ui/chromeState.ts` and one applier, so the chrome has coverage for the first time. Deleted: ~100 lines of CSS and two bindings targeting elements that no longer exist, and a `body:has()` rule that shifted the desktop layout by 160px for touch controls hidden there. Fixed: the office picker tabs that drew their label and their badge on top of each other. Added: a first-run flow, because the product is two verbs and neither was ever stated on screen. Mobile is designed on its own terms instead of being the desktop with things hidden — the plan view comes back, and the keyboard-only shortcuts button is replaced by touch controls. `arena/studioOps.ts` frames the whole thing as the multi-variable environment it is, wrapping the same simulators the renderer drives rather than a headless copy. Also removed `input/vehicle.ts`, which nothing but its own test imported. Tests 385 -> 961, all passing. Typecheck, build, performance budgets across six matrix cells, no-binaries, provenance, dependency licences, zero-config boot and arena source hashes all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+217
-15
@@ -4,8 +4,11 @@ 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.
|
||||
this directory is the small worked example — one storey, four rooms, every core
|
||||
feature — and copying it is the intended way to start. `mateo-court.ts` is the
|
||||
large one: two storeys, sixteen rooms, seat bindings, device declarations and a
|
||||
courtyard, and it is the file to read when you want to see a feature used in
|
||||
anger rather than demonstrated.
|
||||
|
||||
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
|
||||
@@ -34,6 +37,7 @@ export const ACME_HQ: Office = {
|
||||
deskBanks: [...],
|
||||
seats: [...],
|
||||
zones: [...],
|
||||
devices: [...], // smart hardware; see "Devices" below
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -91,14 +95,88 @@ walls the light actually comes through. `0` means your "north" really is north.
|
||||
|
||||
`elevation` is the other one worth thinking about, because it is what the horizon
|
||||
is measured from — the difference between an office on the 48th floor and a shed
|
||||
on an airfield is one number, and it is this one. Both shipped packs are worked
|
||||
examples: `lumbridge-hq.ts` is 188 m up and rotated 205°, `frontier-valley.ts` is
|
||||
4 m up and square to the compass.
|
||||
on an airfield is one number, and it is this one. All three shipped packs are
|
||||
worked examples: `lumbridge-hq.ts` is 188 m up and rotated 205°,
|
||||
`frontier-valley.ts` is 4 m up and square to the compass, and `mateo-court.ts` is
|
||||
1.2 m up — a loading dock — and turned 36° onto the 1781 pueblo grid that
|
||||
downtown Los Angeles still follows.
|
||||
|
||||
### Where the car goes
|
||||
|
||||
A sited building may also say where a vehicle stands outside it:
|
||||
|
||||
```ts
|
||||
arrival: {
|
||||
levelId: "level-1",
|
||||
position: { x: 22.6, z: -3.4 }, // metres, in YOUR plan frame
|
||||
rotation: -Math.PI / 2, // Yaw: which way the car points
|
||||
kind: "vehicle-stall",
|
||||
label: "Mateo Street kerb",
|
||||
},
|
||||
```
|
||||
|
||||
`src/engine/officeExterior.ts` builds an apron and a vehicle there. The position
|
||||
is in **the pack's own frame**, the same one the walls are in — which is why a
|
||||
stall on the street side of a façade authored at `z = 0` has a *negative* `z`,
|
||||
and why you can see it is outside the building without converting anything.
|
||||
|
||||
Put it on ground that is genuinely outside: `Plan` checks the level, the kind and
|
||||
that the numbers are numbers, and deliberately does **not** rule on whether the
|
||||
stall is inside the footprint, because a covered undercroft and a courtyard are
|
||||
things a pack might legitimately mean. `src/test/packs/arrivalAnchors.test.ts`
|
||||
is where the three shipped packs assert that theirs are on the street, the
|
||||
podium kerb and the apron.
|
||||
|
||||
One trap, and it is `lumbridge-hq`'s: a pack whose level-0 floor is 188 m above
|
||||
the ground outside has no pavement to park on at all. Its stall is authored
|
||||
beside its own front door because the plan frame is the only frame a pack has,
|
||||
and what "outside" means *vertically* for a tower is the exterior layer's
|
||||
decision. If your building is up in the air, say so in a comment where the next
|
||||
person will find it.
|
||||
|
||||
Nothing here is geocoded and nothing can be. These are numbers you type, like
|
||||
every other number in a pack — see CONTRACT.md §8 for why a coordinate's
|
||||
provenance is a licensing question in this repo.
|
||||
|
||||
## Levels, and the one thing you cannot do with a second one
|
||||
|
||||
A `Level` is a storey: an `elevation` (floor-to-**floor**, not floor-to-ceiling),
|
||||
a default wall height, and its own floorplan in its own frame with its slab at
|
||||
zero. `Plan` adds the elevation to every coordinate on the level exactly once, so
|
||||
you author an upper floor without holding 5 m in your head.
|
||||
|
||||
> ### ⚠️ A walker cannot change levels. Do not author a staircase.
|
||||
>
|
||||
> **This is the single most expensive thing to discover by building it.** The
|
||||
> walk controller is hard-locked to the level it spawned on, in two places:
|
||||
>
|
||||
> - `src/interiors/walker.ts` rejects any state whose `levelId` differs from the
|
||||
> spawn level — a restored or proposed state on another storey is refused as
|
||||
> *"walker snapshot is incompatible or invalid"*.
|
||||
> - `src/interiors/officeWalker.ts` **throws** if the level it is on stops
|
||||
> resolving — *"office walker lost level"*.
|
||||
>
|
||||
> There is no vertical transition anywhere in the engine: no stair traversal, no
|
||||
> lift, no level handoff, and nothing that changes `levelId` after a spawn. So a
|
||||
> flight of stairs you author is a flight of stairs the collider will never let
|
||||
> anybody climb, however carefully you model it. `mateo-court.ts` has a real
|
||||
> dog-leg stair standing in its courtyard, drawn as a floor finish with a 1.2 m
|
||||
> gap in the balustrade at the head of it, and **its upper floor is unreachable
|
||||
> on foot**. That is a known, accepted state and not a bug in the pack.
|
||||
>
|
||||
> Fixing it is an **engine** change — a cross-level walker state, a transition
|
||||
> volume, and a collider that knows about both storeys — and it is out of scope
|
||||
> for a pack author. Until it lands:
|
||||
>
|
||||
> - Author a second storey for what it *is*: a place the camera flies to, which
|
||||
> viewpoints do perfectly well. `mateo-court` gives its upper floor five of
|
||||
> them.
|
||||
> - Give each level a viewpoint of its own so nothing up there is unreachable by
|
||||
> every means at once.
|
||||
> - Do not spend a day on treads. `viewpoints[0]` is the walk spawn, and it is on
|
||||
> exactly one level; everything else on that level is walkable and everything
|
||||
> on any other level is not.
|
||||
|
||||
## Rooms are slabs. Walls are segments.
|
||||
|
||||
This is the load-bearing idea, and the thing most people get backwards on the
|
||||
@@ -117,9 +195,10 @@ Consequences worth stating out loud:
|
||||
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.
|
||||
Overlapping is legal but every shipped pack avoids it — two coplanar slabs at
|
||||
the same height is a z-fight waiting for the wrong GPU, so `mateo-court`
|
||||
notches its courtyard around the stair standing in it rather than laying one
|
||||
slab over the other.
|
||||
|
||||
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
|
||||
@@ -133,10 +212,18 @@ means **no ceiling at all** — an atrium, a void, or a room you want to look do
|
||||
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.
|
||||
the rooms you are trying to see, so most of both large packs declare
|
||||
`ceiling: null`. The rooms that keep theirs are the ones you are never meant to
|
||||
see inside — the bath and storage in the SF studio, the equipment store in the LA
|
||||
one — and a ceiling is a cheap way of saying so.
|
||||
|
||||
`mateo-court` uses the field in a third sense that is worth knowing about: its
|
||||
courtyard and its stair are `ceiling: null` because they are **outside**, and
|
||||
there is nothing above them at any height. Nothing in the format had to change
|
||||
for that; the field already said what was needed. One consequence is worth
|
||||
carrying, though: a room with no ceiling is a room with nothing to hang a light
|
||||
fitting from, and a ceiling grid over an open courtyard is a lighting plan for a
|
||||
different building.
|
||||
|
||||
## Doors and windows are openings, not props
|
||||
|
||||
@@ -339,6 +426,98 @@ only if you place its centre half its own depth off the wall's face:
|
||||
deep and `tera:whiteboard` is 0.10 m. Name those offsets as constants; you will
|
||||
use them a dozen times.
|
||||
|
||||
## Devices
|
||||
|
||||
A pack may declare smart hardware — a mic, a speaker — on its floorplan:
|
||||
|
||||
```ts
|
||||
devices: [
|
||||
{
|
||||
id: "la-front-mic",
|
||||
kind: "mic",
|
||||
label: "Front desk mic",
|
||||
assetId: "tera:device.mic.desk",
|
||||
anchor: { levelId: "level-1", propId: "front-mic", roomId: "lobby", seatId: "front-01" },
|
||||
capabilities: CANONICAL_CAPABILITIES.mic,
|
||||
provenance: "simulated",
|
||||
disclosure: "Simulated studio hardware. These readings are demonstration data…",
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
`src/devices/types.ts` owns the type and is worth reading; four things about it
|
||||
matter when you are authoring one.
|
||||
|
||||
**A device has no coordinate.** `anchor.propId` is required and it *is* the
|
||||
position: the device derives its transform from that prop's, plus an optional
|
||||
`offset` in the prop's own frame for the few centimetres between a desk's origin
|
||||
and the top of a mic stand. Nudge the desk and the mic goes with it, because
|
||||
there was never a second number to forget. Same rule `Prop.seat` follows for
|
||||
chairs, and the same one a pack follows by importing its site instead of
|
||||
restating the coordinates.
|
||||
|
||||
**The anchor prop is the hardware.** Its `kind` is the device asset —
|
||||
`<namespace>:device.<kind>.<placement>`, so `tera:device.mic.desk` and a
|
||||
self-hoster's `acme:device.mic.boom` both read as a mic with nothing registered.
|
||||
`Plan` drops a declaration whose anchor prop is device hardware of the *wrong*
|
||||
kind: a mic bolted to a speaker is not a rendering mistake, it is a command
|
||||
routed to the wrong instrument.
|
||||
|
||||
**`capabilities` should be `CANONICAL_CAPABILITIES[kind]`.** The device panel
|
||||
builds its controls by walking that array and the arena's observation width is
|
||||
the sum of them, so two studios describing a mic differently changes the shape of
|
||||
an RL observation without anybody editing the arena.
|
||||
|
||||
**`disclosure` is mandatory and it is checked.** A declaration with
|
||||
`provenance: "simulated"` whose disclosure does not contain the word is reported
|
||||
as a problem and dropped — the same check `RobotOperationsDefinition` gets, for
|
||||
the same reason. A level meter that moves, with nothing beside it saying where
|
||||
the number came from, is a claim about a real room.
|
||||
|
||||
What a device is *doing* — powered, muted, its level in dBFS — is a `DeviceState`
|
||||
and **never appears in a pack**. It arrives over the API from a route that can
|
||||
refuse an anonymous caller, exactly as `Presence` does. A declaration is a
|
||||
description of a room and is safe to publish; a reading is not, and the split is
|
||||
the whole design.
|
||||
|
||||
## How full a room should be, and the trap in the answer
|
||||
|
||||
The number to aim at is **0.26 non-light props per square metre**, building-wide,
|
||||
with no room over 20 m² below **0.15**. Both shipped studios clear it:
|
||||
`lumbridge-hq` sits at 0.28 over 100 m², `mateo-court` at 0.29 over 1246 m².
|
||||
`src/test/packs/mateoContent.test.ts` measures it if you want the exact method —
|
||||
it counts prop centres by `Plan.roomAt`, and it excludes every `tera:light.*`
|
||||
because a ceiling grid will satisfy any prop count you like while leaving the
|
||||
floor bare. Mateo Court's first version proved that: ninety-eight of its props
|
||||
were troffers, two of the grids hung in rooms declared `ceiling: null`, and it
|
||||
looked empty from every viewpoint it had.
|
||||
|
||||
**But the ratio is the easy half, and on its own it is a lie.** `furnish.ts`
|
||||
batches props by `(asset, colorKey)` and draws `ctx.rand` **once per batch**, so
|
||||
every instance of a kind is geometrically identical — the same seeded jitter,
|
||||
the same books on the same shelf, the same leaves on the same plant. Ten more
|
||||
shelves in a room are one shelf drawn ten times. So:
|
||||
|
||||
> Apparent density is a function of **distinct kinds**, not of prop count.
|
||||
> A room that looks thin does not get better when you copy what is already in it.
|
||||
|
||||
Two consequences for how you fill a room:
|
||||
|
||||
- **Reach for a kind you have not used yet before you reach for a second copy.**
|
||||
Mateo Court's courtyard went from 13 props of 6 kinds to 44 of 13, and it is
|
||||
the second number that changed what it looks like. Twelve of the assets in
|
||||
`src/assets/office/studio.ts` exist because of exactly that room.
|
||||
- **If the kind you need is not in the catalogue, write it.** `defineAsset` plus
|
||||
a `registerAll` is a smaller change than it looks, `src/assets/office/studio.ts`
|
||||
is a worked example of a dozen of them, and a `colorKey` on an existing kind
|
||||
will not stand in for it — the colour is *in* the batch key, so two tints of
|
||||
one asset are two batches of the same geometry, which is better than one and is
|
||||
not a new object.
|
||||
|
||||
A useful floor for a big room is **seven distinct kinds over 40 m²**, which is
|
||||
what the reference pack's one large room manages. Below that a room reads as a
|
||||
pattern rather than a place, whatever the prop count says.
|
||||
|
||||
## Zones
|
||||
|
||||
A named region of floor with an opaque `colorKey`, no behaviour and no effect on
|
||||
@@ -450,6 +629,12 @@ without capturing console output. In a dev build it also warns.
|
||||
- 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.
|
||||
- a device whose anchor names a prop that does not exist, sits on another level,
|
||||
or is device hardware of a different kind; and one whose own declaration is
|
||||
invalid — no label, a `kind` its `assetId` disagrees with, no capabilities, or
|
||||
a `simulated` provenance whose disclosure does not say so
|
||||
- an `arrival` anchor on a level that does not exist, of an unknown `kind`, or
|
||||
with a position that is not a number
|
||||
|
||||
**Repaired** (silently, and recorded):
|
||||
|
||||
@@ -457,6 +642,8 @@ without capturing console output. In a dev build it also warns.
|
||||
- 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
|
||||
- a device naming an unknown room or an unknown seat — the address is cleared,
|
||||
the device stays and the room is re-derived from where its hardware stands
|
||||
|
||||
Missing required arrays read as `[]`, because a pack that arrived over HTTP has
|
||||
been through no type checker.
|
||||
@@ -477,12 +664,27 @@ dropping one bad opening does not renumber its siblings.
|
||||
|
||||
## A checklist before you call it done
|
||||
|
||||
1. `new Plan(office).problems` is empty.
|
||||
1. `new Plan(office).problems` is empty — **at both depths**, `"full"` and
|
||||
`"public"`. They are different builds and only one of them is what a visitor
|
||||
gets.
|
||||
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.
|
||||
4. Seat ids are the ones you are willing to live with for a year, and every seat
|
||||
somebody is meant to occupy has a prop bound to it with `seat:`.
|
||||
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/`.
|
||||
6. `JSON.parse(JSON.stringify(office))` **deep-equals** the office. The usual way
|
||||
to fail this is a helper that writes `elevation: opts.elevation`
|
||||
unconditionally: `{ elevation: undefined }` and `{}` are different objects and
|
||||
only one of them survives the wire. Spread optional fields, do not assign
|
||||
them.
|
||||
7. Every room worth looking at has a viewpoint whose `focus.at` lands inside it,
|
||||
and `viewpoints[0]` is somewhere a person can stand — it is the walk spawn as
|
||||
well as the arrival camera.
|
||||
8. At least 0.26 non-light props/m² building-wide, no room over 20 m² below
|
||||
0.15, and — the one that matters — **at least seven distinct kinds in every
|
||||
room over 40 m²**. See "How full a room should be" above for why the second
|
||||
number is the real one.
|
||||
9. Nothing binary landed under `src/`.
|
||||
|
||||
@@ -182,8 +182,13 @@ function scatter(
|
||||
kind,
|
||||
position,
|
||||
rotation: opts.rotation ?? NORTH,
|
||||
elevation: opts.elevation,
|
||||
colorKey: opts.colorKey,
|
||||
// Spread rather than assigned, because `elevation: undefined` is not the
|
||||
// same shape as no `elevation` at all: `JSON.stringify` drops the key and a
|
||||
// pack that has been through the wire stops deep-equalling the one in the
|
||||
// bundle. CONTRACT.md §2 says those two have to be literally the same
|
||||
// thing, and `src/test/packs/packRegression.test.ts` now checks it.
|
||||
...(opts.elevation === undefined ? {} : { elevation: opts.elevation }),
|
||||
...(opts.colorKey === undefined ? {} : { colorKey: opts.colorKey }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* without invalidating those addresses.
|
||||
*/
|
||||
|
||||
import { CANONICAL_CAPABILITIES, type DeviceDeclaration } from "../devices/types.ts";
|
||||
import type {
|
||||
AssetId,
|
||||
DeskBank,
|
||||
@@ -64,6 +65,8 @@ const PENDANT: AssetId = "tera:light.pendant";
|
||||
const TROFFER: AssetId = "tera:light.troffer";
|
||||
const RUG: AssetId = "tera:rug";
|
||||
const WHITEBOARD: AssetId = "tera:whiteboard";
|
||||
const MIC: AssetId = "tera:device.mic.desk";
|
||||
const SPEAKER: AssetId = "tera:device.speaker.desk";
|
||||
|
||||
const CONCRETE = "tera:concrete.polished";
|
||||
const WOOD = "tera:wood.plank";
|
||||
@@ -230,6 +233,14 @@ const PROPS: Prop[] = [
|
||||
// screen grants survive this content rewrite.
|
||||
prop("lobby-monitor", MONITOR, 2.15, 4.45, NORTH, { elevation: 0.73 }),
|
||||
prop("agent-monitor-b", MONITOR, 3.97, 4.45, NORTH, { elevation: 0.73 }),
|
||||
// The two pieces of hardware the studio operates, and the only two props in
|
||||
// this pack that a `DeviceDeclaration` points at: a desk condenser on the
|
||||
// left-hand workstation and a monitor speaker beside its screen. Both stand on
|
||||
// the 0.73 m desktop, so both carry the elevation rather than assuming one.
|
||||
// The declarations are in `DEVICES` at the foot of this file and carry no
|
||||
// coordinate — they name these ids and take the transform from here.
|
||||
prop("agent-mic", MIC, 1.62, 4.72, NORTH, { elevation: 0.73, seat: "sf-agent-01" }),
|
||||
prop("agent-speaker", SPEAKER, 2.72, 4.5, NORTH, { elevation: 0.73 }),
|
||||
prop("agent-tree", TREE, 5.15, 4.15),
|
||||
prop("agent-light-a", TROFFER, 2.2, 4.6, NORTH, { elevation: CEILING }),
|
||||
prop("agent-light-b", TROFFER, 4.0, 4.6, NORTH, { elevation: CEILING }),
|
||||
@@ -264,6 +275,69 @@ const PROPS: Prop[] = [
|
||||
prop("lounge-plant", PLANT, 6.4, 7.95),
|
||||
];
|
||||
|
||||
/**
|
||||
* One sentence, shown to a viewer beside every reading this building publishes.
|
||||
*
|
||||
* Mandatory and checked: `validateDeviceDeclaration` refuses a `simulated`
|
||||
* declaration whose disclosure does not contain the word, exactly as
|
||||
* `resolveRobotOperations` refuses a robot definition that does not say its
|
||||
* activity is simulated. A live-looking level meter with no provenance beside it
|
||||
* is a claim about a real room, and this is the field that stops the pack making
|
||||
* one by omission.
|
||||
*/
|
||||
const DISCLOSURE =
|
||||
"Simulated studio hardware. These readings are demonstration data, never live " +
|
||||
"presence data.";
|
||||
|
||||
/**
|
||||
* Two devices: the mic on the desk and the speaker on the computer.
|
||||
*
|
||||
* Both anchored to a prop and neither carrying a coordinate — see `DeviceAnchor`
|
||||
* in `src/devices/types.ts` for why that is the whole design of the type. Move
|
||||
* `agent-mic` 200 mm and the mic moves with it, because there was never a second
|
||||
* number to forget.
|
||||
*
|
||||
* `capabilities` is `CANONICAL_CAPABILITIES[kind]` rather than a hand-written
|
||||
* list. Two studios authored months apart should describe the same instrument
|
||||
* the same way — the device panel builds its controls by walking this array, and
|
||||
* the arena's observation width is the sum of them.
|
||||
*/
|
||||
const DEVICES: DeviceDeclaration[] = [
|
||||
{
|
||||
id: "sf-desk-mic",
|
||||
kind: "mic",
|
||||
label: "Desk mic",
|
||||
assetId: MIC,
|
||||
anchor: {
|
||||
levelId: "level-1",
|
||||
propId: "agent-mic",
|
||||
roomId: "live-work",
|
||||
// The seat this mic is in front of. An address, like every other seat
|
||||
// reference in a pack — it is what lets a consumer ask whether anybody is
|
||||
// sitting where the mic is pointed without being told a coordinate.
|
||||
seatId: "sf-agent-01",
|
||||
},
|
||||
capabilities: CANONICAL_CAPABILITIES.mic,
|
||||
provenance: "simulated",
|
||||
disclosure: DISCLOSURE,
|
||||
},
|
||||
{
|
||||
id: "sf-desk-speaker",
|
||||
kind: "speaker",
|
||||
label: "Desk speaker",
|
||||
assetId: SPEAKER,
|
||||
anchor: {
|
||||
levelId: "level-1",
|
||||
propId: "agent-speaker",
|
||||
roomId: "live-work",
|
||||
seatId: "sf-agent-01",
|
||||
},
|
||||
capabilities: CANONICAL_CAPABILITIES.speaker,
|
||||
provenance: "simulated",
|
||||
disclosure: DISCLOSURE,
|
||||
},
|
||||
];
|
||||
|
||||
const ZONES: Zone[] = [
|
||||
{ id: "zone-entry", name: "Entry", outline: rect(FACE, 6.7, 1.75, DEPTH - FACE), colorKey: "social" },
|
||||
{ id: "zone-agent-bench", name: "Agent Bench", outline: rect(1.1, 3.5, 5.15, 5.6), colorKey: "focus" },
|
||||
@@ -338,6 +412,7 @@ const LEVEL: Level = {
|
||||
deskBanks: DESK_BANKS,
|
||||
seats: SEATS,
|
||||
zones: ZONES,
|
||||
devices: DEVICES,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -354,8 +429,8 @@ export const LUMBRIDGE_HQ: Office = {
|
||||
"A compact 12 × 9 metre San Francisco live/work studio: two agent workstations, a demo lounge, kitchen, sleeping alcove and bath/storage.",
|
||||
author: "Lumbridge",
|
||||
license: "CC0-1.0",
|
||||
version: "2.0.0",
|
||||
updated: "2026-08-19",
|
||||
version: "2.1.0",
|
||||
updated: "2026-08-21",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+1105
-61
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,13 @@ export const MATEO_COURT_ROBOT_OPERATIONS = Object.freeze({
|
||||
officeId: "mateo-court",
|
||||
disclosure: "Seeded robot simulation — no live people, company activity, or operational data.",
|
||||
stations: [
|
||||
{ id: "la-l1-dock", label: "Court charge dock", role: "charge", anchor: { kind: "point", levelId: "level-1", position: { x: 18.4, z: 13.2 }, facing: { x: 1, z: 0 } } },
|
||||
// Against the west wall of the yard, standing on `court-dock` — the
|
||||
// `tera:dock.robot` prop the pack now places at (10.15, 16.9). This used to
|
||||
// read (18.4, 13.2), which is underneath the courtyard's long table: fine
|
||||
// for as long as nothing was drawn there, and a robot standing in the lunch
|
||||
// the moment something was. The dock faces east out of the pad, which is
|
||||
// where its `facing` comes from.
|
||||
{ id: "la-l1-dock", label: "Court charge dock", role: "charge", anchor: { kind: "point", levelId: "level-1", position: { x: 10.9, z: 16.9 }, facing: { x: 1, z: 0 } } },
|
||||
{ id: "la-l1-paseo", label: "Paseo patrol point", role: "patrol", anchor: { kind: "room", roomId: "paseo" } },
|
||||
{ id: "la-l1-mess", label: "Mess patrol point", role: "patrol", anchor: { kind: "room", roomId: "mess" } },
|
||||
{ id: "la-l1-court", label: "Court patrol point", role: "patrol", anchor: { kind: "room", roomId: "court" } },
|
||||
@@ -16,7 +22,11 @@ export const MATEO_COURT_ROBOT_OPERATIONS = Object.freeze({
|
||||
{ id: "la-l1-directory", label: "Paseo directory", role: "inspect", anchor: { kind: "prop", propId: "paseo-directory", standoffM: 0.7, side: -1 } },
|
||||
{ id: "la-l1-display", label: "Works display", role: "inspect", anchor: { kind: "prop", propId: "works-display", standoffM: 0.72, side: -1 } },
|
||||
|
||||
{ id: "la-l2-dock", label: "Loft charge dock", role: "charge", anchor: { kind: "point", levelId: "level-2", position: { x: 13.2, z: 3.0 }, facing: { x: 1, z: 0 } } },
|
||||
// The upper dock moved east out of the new `loft-b` bench, whose three
|
||||
// columns now occupy x 13.4–16.8. It stands on the `loft-dock` prop in the
|
||||
// model bay instead, beside the racks, on the one stretch of the street wall
|
||||
// upstairs with no window in it. Its bay opens south, hence the facing.
|
||||
{ id: "la-l2-dock", label: "Loft charge dock", role: "charge", anchor: { kind: "point", levelId: "level-2", position: { x: 22.6, z: 1.5 }, facing: { x: 0, z: 1 } } },
|
||||
{ id: "la-l2-loft", label: "Loft patrol point", role: "patrol", anchor: { kind: "room", roomId: "loft" } },
|
||||
{ id: "la-l2-palmetto", label: "Palmetto patrol point", role: "patrol", anchor: { kind: "room", roomId: "palmetto" } },
|
||||
{ id: "la-l2-loggia", label: "Loggia patrol point", role: "patrol", anchor: { kind: "point", levelId: "level-2", position: { x: 10.0, z: 8.0 } } },
|
||||
|
||||
@@ -19,6 +19,22 @@
|
||||
* pack importing its own site **from here** rather than declaring it inline. One
|
||||
* source of truth, and the direction of the dependency is the safe one: the
|
||||
* small thing does not know about the large one.
|
||||
*
|
||||
* ### Where the car stands
|
||||
*
|
||||
* Each site below carries an `arrival` anchor: one marked stall on the ground
|
||||
* outside, in **the pack's own plan frame**, which is the only frame a pack has
|
||||
* and the same one its walls are in. `src/engine/officeExterior.ts` builds the
|
||||
* apron and the vehicle there.
|
||||
*
|
||||
* Two of the three are honest ground. `mateo-court` sits 1.2 m above its street
|
||||
* and `frontier-valley` 4 m above an airfield, so a stall a few metres outside
|
||||
* the façade is a stall on the pavement. **`lumbridge-hq` is 188 m up a tower**
|
||||
* and there is no pavement outside its west wall at all — its anchor is the
|
||||
* kerb of the podium, authored beside the front door because the pack frame is
|
||||
* the only place it can be authored, and what "outside" means vertically for a
|
||||
* tower is the exterior layer's decision and not this file's. It is called out
|
||||
* here rather than left for somebody to discover from a car parked in the sky.
|
||||
*/
|
||||
|
||||
import type { OfficeSite } from "../interiors/types.ts";
|
||||
@@ -44,6 +60,17 @@ export const LUMBRIDGE_HQ_SITE: OfficeSite = {
|
||||
seed: 115,
|
||||
bodyColor: 0x8799a8,
|
||||
},
|
||||
// West of the studio's own front door, which is the doorway 6.8 m along
|
||||
// `ext-west`. Parallel to the façade and nosed north, the way a kerbside bay
|
||||
// on a one-way downtown street runs. See the note at the top of this file
|
||||
// about what 188 m of elevation does to the word "outside".
|
||||
arrival: {
|
||||
levelId: "level-1",
|
||||
position: { x: -4.0, z: 7.4 },
|
||||
rotation: 0,
|
||||
kind: "vehicle-stall",
|
||||
label: "Podium kerb",
|
||||
},
|
||||
};
|
||||
|
||||
/** A hangar on the old naval air station. See `frontier-valley.ts`. */
|
||||
@@ -64,6 +91,16 @@ export const FRONTIER_VALLEY_SITE: OfficeSite = {
|
||||
seed: 2718,
|
||||
bodyColor: 0x899397,
|
||||
},
|
||||
// On the apron, seven metres clear of the twelve-metre hangar door in the east
|
||||
// gable and centred on it, nosed in. An apron is the one place in these three
|
||||
// packs where a vehicle is not a visitor but part of the programme.
|
||||
arrival: {
|
||||
levelId: "level-1",
|
||||
position: { x: 61.0, z: 12.0 },
|
||||
rotation: Math.PI / 2,
|
||||
kind: "vehicle-stall",
|
||||
label: "Hangar apron",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -89,6 +126,18 @@ export const MATEO_COURT_SITE: OfficeSite = {
|
||||
seed: 1781,
|
||||
bodyColor: 0xa87960,
|
||||
},
|
||||
// On the street, north of the brick façade and a few metres east of the paseo
|
||||
// arch, so that it is in frame from the arrival viewpoint and three steps from
|
||||
// the front door. `z` is negative because the street façade is authored at
|
||||
// `z = 0` and the pavement is on the other side of it, which is the whole
|
||||
// reason the anchor is in the plan's frame rather than the world's.
|
||||
arrival: {
|
||||
levelId: "level-1",
|
||||
position: { x: 22.6, z: -3.4 },
|
||||
rotation: -Math.PI / 2,
|
||||
kind: "vehicle-stall",
|
||||
label: "Mateo Street kerb",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user