d464459838
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>
234 lines
6.9 KiB
TypeScript
234 lines
6.9 KiB
TypeScript
/**
|
||
* Desks: the workstation, the pedestal that lives under it, and the screen that
|
||
* separates it from the next one.
|
||
*
|
||
* `desk.workstation` is the anchor of the library. It is the asset a `DeskBank`
|
||
* repeats, it is the one every self-hoster will override first, and its
|
||
* dimensions are what the rest of the furniture is sized against: a 730 mm
|
||
* working height, a 1.6 × 0.8 m desktop, and a person sitting at +Z facing −Z.
|
||
*/
|
||
|
||
import { defineAsset } from "../kit.ts";
|
||
import { MeshBin } from "../parts.ts";
|
||
import { clamp, panelSlab, slab, tintable } from "./common.ts";
|
||
|
||
type WorkstationParams = {
|
||
width: number;
|
||
depth: number;
|
||
/** Working height of the desktop, metres. 1.05 or so for a standing desk. */
|
||
height: number;
|
||
/**
|
||
* `"loop"` is the cantilever frame most office desks actually have — a foot
|
||
* bar, two uprights and a top rail at each end. `"post"` is four legs, which
|
||
* reads as a table and is here for the rooms where that is wanted.
|
||
*/
|
||
legs: "loop" | "post";
|
||
/** The panel across the far edge. Tintable; this is the desk's colour key. */
|
||
modesty: boolean;
|
||
};
|
||
|
||
const TOP_THICKNESS = 0.03;
|
||
|
||
export const deskWorkstation = defineAsset<WorkstationParams>({
|
||
id: "tera:desk.workstation",
|
||
label: "Workstation",
|
||
defaults: { width: 1.6, depth: 0.8, height: 0.73, legs: "loop", modesty: true },
|
||
|
||
footprint(p) {
|
||
// The clearance is a chair pulled out, not a chair tucked in: 900 mm is
|
||
// what a person needs to stand up and leave without moving the desk.
|
||
return { width: p.width, depth: p.depth, height: p.height, clearance: 0.9 };
|
||
},
|
||
|
||
build(p, ctx) {
|
||
const P = ctx.parts;
|
||
const bin = new MeshBin();
|
||
const frame = ctx.materials.get("deskFrame");
|
||
const deckY = p.height - TOP_THICKNESS;
|
||
|
||
slab(bin, ctx, ctx.materials.get("deskSurface"), {
|
||
y: deckY,
|
||
width: p.width,
|
||
depth: p.depth,
|
||
thickness: TOP_THICKNESS,
|
||
});
|
||
|
||
const legX = p.width / 2 - 0.09;
|
||
const legZ = p.depth / 2 - 0.11;
|
||
|
||
if (p.legs === "post") {
|
||
for (const sx of [-1, 1]) {
|
||
for (const sz of [-1, 1]) {
|
||
bin.add(P.rod(), frame, {
|
||
x: sx * legX,
|
||
z: sz * legZ,
|
||
size: [0.055, deckY, 0.055],
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
const barDepth = p.depth - 0.18;
|
||
for (const sx of [-1, 1]) {
|
||
const x = sx * legX;
|
||
bin.add(P.box(), frame, { x, size: [0.07, 0.045, barDepth] });
|
||
bin.add(P.box(), frame, { x, y: deckY - 0.05, size: [0.07, 0.05, barDepth] });
|
||
for (const sz of [-1, 1]) {
|
||
bin.add(P.box(), frame, {
|
||
x,
|
||
z: sz * legZ,
|
||
size: [0.05, deckY - 0.05, 0.05],
|
||
});
|
||
}
|
||
}
|
||
// The spine between the two end frames. Without it a cantilever desk
|
||
// looks like two separate trestles that happen to be under one board.
|
||
bin.add(P.box(), frame, {
|
||
y: deckY - 0.19,
|
||
size: [Math.max(0.2, p.width - 0.28), 0.055, 0.055],
|
||
});
|
||
}
|
||
|
||
if (p.modesty) {
|
||
const height = clamp(deckY - 0.3, 0.14, 0.4);
|
||
panelSlab(bin, ctx, tintable(ctx, "partitionFabric"), {
|
||
y: deckY - 0.05 - height,
|
||
z: -(p.depth / 2 - 0.08),
|
||
width: p.width - 0.24,
|
||
height,
|
||
thickness: 0.018,
|
||
});
|
||
}
|
||
|
||
return bin.build("desk.workstation");
|
||
},
|
||
});
|
||
|
||
type PedestalParams = {
|
||
width: number;
|
||
depth: number;
|
||
height: number;
|
||
drawers: number;
|
||
/** Castors, for the pedestal that gets rolled out and sat on. */
|
||
mobile: boolean;
|
||
};
|
||
|
||
/**
|
||
* The under-desk drawer unit. Its fronts are at +Z — the same side the person
|
||
* is on — so a pack gives it the same rotation as the desk it belongs to and
|
||
* the drawers open toward the chair.
|
||
*/
|
||
export const deskPedestal = defineAsset<PedestalParams>({
|
||
id: "tera:desk.pedestal",
|
||
label: "Desk pedestal",
|
||
defaults: { width: 0.42, depth: 0.6, height: 0.6, drawers: 3, mobile: true },
|
||
|
||
footprint(p) {
|
||
return { width: p.width, depth: p.depth, height: p.height, clearance: 0.5 };
|
||
},
|
||
|
||
build(p, ctx) {
|
||
const P = ctx.parts;
|
||
const bin = new MeshBin();
|
||
const carcass = ctx.materials.get("cabinet");
|
||
const trim = ctx.materials.get("metalTrim");
|
||
|
||
const lift = p.mobile ? 0.05 : 0.02;
|
||
const bodyH = p.height - lift;
|
||
bin.add(P.box(), carcass, { y: lift, size: [p.width, bodyH, p.depth] });
|
||
|
||
if (p.mobile) {
|
||
for (const sx of [-1, 1]) {
|
||
for (const sz of [-1, 1]) {
|
||
bin.add(P.cylinder(8), trim, {
|
||
x: sx * (p.width / 2 - 0.07),
|
||
z: sz * (p.depth / 2 - 0.07),
|
||
size: [0.05, lift, 0.05],
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const count = Math.max(1, Math.round(p.drawers));
|
||
const pitch = (bodyH - 0.03) / count;
|
||
const face = tintable(ctx, "cabinet");
|
||
for (let i = 0; i < count; i++) {
|
||
const y = lift + 0.015 + i * pitch;
|
||
bin.add(P.box(), face, {
|
||
y,
|
||
z: p.depth / 2,
|
||
size: [p.width - 0.03, pitch - 0.012, 0.02],
|
||
});
|
||
// A recessed pull rather than a handle: a D-handle at this scale is four
|
||
// more parts and reads as a smudge from any distance you see a pedestal.
|
||
bin.add(P.box(), trim, {
|
||
y: y + pitch - 0.05,
|
||
z: p.depth / 2 + 0.012,
|
||
size: [p.width * 0.42, 0.014, 0.012],
|
||
});
|
||
}
|
||
|
||
return bin.build("desk.pedestal");
|
||
},
|
||
});
|
||
|
||
type PartitionParams = {
|
||
width: number;
|
||
height: number;
|
||
thickness: number;
|
||
/** Floor-standing feet. Off by default: most of these clamp to a desk. */
|
||
feet: boolean;
|
||
};
|
||
|
||
/**
|
||
* A fabric screen. Authored standing on the floor like everything else, so the
|
||
* desk-mounted case is `elevation: 0.73` in the pack rather than a `mount`
|
||
* parameter here — the same screen clamps to a desk, stands on the floor and
|
||
* caps a bench run, and only the pack knows which.
|
||
*
|
||
* The fabric is the tintable part.
|
||
*/
|
||
export const deskPartition = defineAsset<PartitionParams>({
|
||
id: "tera:desk.partition",
|
||
label: "Desk partition",
|
||
defaults: { width: 1.4, height: 0.45, thickness: 0.04, feet: false },
|
||
|
||
footprint(p) {
|
||
return { width: p.width, depth: p.feet ? 0.34 : p.thickness + 0.02, height: p.height };
|
||
},
|
||
|
||
build(p, ctx) {
|
||
const P = ctx.parts;
|
||
const bin = new MeshBin();
|
||
const frame = ctx.materials.get("partitionFrame");
|
||
|
||
panelSlab(bin, ctx, tintable(ctx, "partitionFabric"), {
|
||
y: 0,
|
||
width: p.width - 0.03,
|
||
height: p.height,
|
||
thickness: p.thickness,
|
||
});
|
||
|
||
bin.add(P.box(), frame, {
|
||
y: p.height - 0.018,
|
||
size: [p.width, 0.018, p.thickness + 0.012],
|
||
});
|
||
for (const sx of [-1, 1]) {
|
||
bin.add(P.box(), frame, {
|
||
x: sx * (p.width / 2 - 0.008),
|
||
size: [0.016, p.height, p.thickness + 0.012],
|
||
});
|
||
}
|
||
|
||
if (p.feet) {
|
||
for (const sx of [-1, 1]) {
|
||
bin.add(P.box(), frame, {
|
||
x: sx * (p.width / 2 - 0.1),
|
||
size: [0.05, 0.02, 0.32],
|
||
});
|
||
}
|
||
}
|
||
|
||
return bin.build("desk.partition");
|
||
},
|
||
});
|