/** * 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({ 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({ 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({ 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"); }, });