feat: add active SF and LA HQ environments
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* The small-space habitat kit.
|
||||
*
|
||||
* Offices prove the scene format with desks and meeting rooms. A studio home
|
||||
* needs a different handful of silhouettes before it reads honestly: a bed is
|
||||
* not a wide desk, a kitchen is not a row of lockers, and two lounge chairs do
|
||||
* not become a sofa because they touch. These assets stay inside the same
|
||||
* primitive grammar and material palette as the office kit, so they add no
|
||||
* binary art, texture download, or second visual language.
|
||||
*
|
||||
* As with every asset in this directory, source is Apache-2.0 and the generated
|
||||
* artistic output is dedicated under CC0-1.0 by `src/assets/LICENSE-ART`.
|
||||
*/
|
||||
|
||||
import * as THREE from "three";
|
||||
import { defineAsset } from "../kit.ts";
|
||||
import { MeshBin } from "../parts.ts";
|
||||
import { panelSlab, slab, tintable } from "./common.ts";
|
||||
|
||||
type BedParams = {
|
||||
width: number;
|
||||
length: number;
|
||||
platformHeight: number;
|
||||
headboardHeight: number;
|
||||
};
|
||||
|
||||
/** A low platform bed. The headboard is at local -Z and the foot faces +Z. */
|
||||
export const bedPlatform = defineAsset<BedParams>({
|
||||
id: "tera:bed.platform",
|
||||
label: "Platform bed",
|
||||
defaults: { width: 1.52, length: 2.03, platformHeight: 0.28, headboardHeight: 0.92 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.length, height: p.headboardHeight, clearance: 0.7 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const wood = ctx.materials.get("shelf");
|
||||
const textile = tintable(ctx, "upholstery");
|
||||
const linen = ctx.materials.get("paper");
|
||||
const frameH = Math.max(0.12, p.platformHeight);
|
||||
const mattressH = 0.2;
|
||||
|
||||
// A recessed plinth gives the platform a shadow line instead of a box-on-floor read.
|
||||
bin.add(ctx.parts.box(), ctx.materials.get("chairBase"), {
|
||||
y: 0.035,
|
||||
size: [p.width - 0.22, 0.07, p.length - 0.18],
|
||||
});
|
||||
slab(bin, ctx, wood, {
|
||||
y: 0.09,
|
||||
width: p.width,
|
||||
depth: p.length,
|
||||
thickness: frameH - 0.09,
|
||||
});
|
||||
bin.add(ctx.parts.roundedBox(0.09), textile, {
|
||||
y: frameH,
|
||||
z: 0.03,
|
||||
size: [p.width - 0.12, mattressH, p.length - 0.12],
|
||||
});
|
||||
panelSlab(bin, ctx, wood, {
|
||||
y: 0,
|
||||
z: -p.length / 2 + 0.035,
|
||||
width: p.width,
|
||||
height: p.headboardHeight,
|
||||
thickness: 0.07,
|
||||
faces: "front",
|
||||
});
|
||||
|
||||
// Two pillows, separated enough that the silhouette survives the dollhouse view.
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(ctx.parts.roundedBox(0.12), linen, {
|
||||
x: sx * p.width * 0.23,
|
||||
y: frameH + mattressH + 0.015,
|
||||
z: -p.length / 2 + 0.38,
|
||||
size: [p.width * 0.38, 0.11, 0.48],
|
||||
yaw: sx * 0.035,
|
||||
});
|
||||
}
|
||||
|
||||
return bin.build("bed.platform");
|
||||
},
|
||||
});
|
||||
|
||||
type SofaParams = {
|
||||
width: number;
|
||||
depth: number;
|
||||
seatHeight: number;
|
||||
backHeight: number;
|
||||
modules: number;
|
||||
};
|
||||
|
||||
/** A low modular sofa with individually readable seat and back cushions. */
|
||||
export const sofaModular = defineAsset<SofaParams>({
|
||||
id: "tera:sofa.modular",
|
||||
label: "Modular sofa",
|
||||
defaults: { width: 2.2, depth: 0.9, seatHeight: 0.42, backHeight: 0.82, modules: 3 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.backHeight, clearance: 0.7 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const P = ctx.parts;
|
||||
const base = ctx.materials.get("chairShell");
|
||||
const textile = tintable(ctx, "upholstery");
|
||||
const modules = Math.max(2, Math.min(4, Math.round(p.modules)));
|
||||
const innerW = p.width - 0.28;
|
||||
const moduleW = innerW / modules;
|
||||
|
||||
for (const sx of [-1, 1]) {
|
||||
for (const sz of [-1, 1]) {
|
||||
bin.add(P.rod(), ctx.materials.get("chairBase"), {
|
||||
x: sx * (p.width / 2 - 0.12),
|
||||
z: sz * (p.depth / 2 - 0.12),
|
||||
size: [0.04, 0.08, 0.04],
|
||||
});
|
||||
}
|
||||
}
|
||||
bin.add(P.box(), base, { y: 0.08, size: [innerW, p.seatHeight - 0.15, p.depth - 0.14] });
|
||||
|
||||
for (let i = 0; i < modules; i += 1) {
|
||||
const x = -innerW / 2 + moduleW * (i + 0.5);
|
||||
bin.add(P.roundedBox(0.07), textile, {
|
||||
x,
|
||||
y: p.seatHeight - 0.12,
|
||||
z: -0.04,
|
||||
size: [moduleW - 0.025, 0.14, p.depth - 0.25],
|
||||
});
|
||||
bin.add(P.roundedBox(0.09), textile, {
|
||||
x,
|
||||
y: p.seatHeight + 0.015,
|
||||
z: p.depth / 2 - 0.15,
|
||||
size: [moduleW - 0.035, p.backHeight - p.seatHeight, 0.17],
|
||||
pitch: 0.1,
|
||||
});
|
||||
}
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(P.roundedBox(0.09), textile, {
|
||||
x: sx * (p.width / 2 - 0.07),
|
||||
y: 0.08,
|
||||
size: [0.14, p.seatHeight + 0.18, p.depth - 0.1],
|
||||
});
|
||||
}
|
||||
|
||||
return bin.build("sofa.modular");
|
||||
},
|
||||
});
|
||||
|
||||
type KitchenRunParams = {
|
||||
width: number;
|
||||
depth: number;
|
||||
counterHeight: number;
|
||||
height: number;
|
||||
bays: number;
|
||||
};
|
||||
|
||||
/** A complete compact kitchen wall: base units, worktop, sink, backsplash and uppers. */
|
||||
export const kitchenRun = defineAsset<KitchenRunParams>({
|
||||
id: "tera:kitchen.run",
|
||||
label: "Kitchen wall",
|
||||
defaults: { width: 3.6, depth: 0.62, counterHeight: 0.91, height: 2.16, bays: 5 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.height, clearance: 1 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const P = ctx.parts;
|
||||
const cabinet = tintable(ctx, "cabinet");
|
||||
const top = ctx.materials.get("tableTop");
|
||||
const metal = ctx.materials.get("metalTrim");
|
||||
const bays = Math.max(3, Math.min(7, Math.round(p.bays)));
|
||||
const bayW = p.width / bays;
|
||||
|
||||
bin.add(P.box(), ctx.materials.get("chairBase"), {
|
||||
y: 0.02,
|
||||
z: -0.035,
|
||||
size: [p.width - 0.08, 0.08, p.depth - 0.12],
|
||||
});
|
||||
for (let i = 0; i < bays; i += 1) {
|
||||
const x = -p.width / 2 + bayW * (i + 0.5);
|
||||
bin.add(P.box(), cabinet, {
|
||||
x,
|
||||
y: 0.1,
|
||||
z: -0.03,
|
||||
size: [bayW - 0.018, p.counterHeight - 0.15, p.depth - 0.06],
|
||||
});
|
||||
bin.add(P.box(), metal, {
|
||||
x: x + bayW * 0.34,
|
||||
y: p.counterHeight * 0.55,
|
||||
z: p.depth / 2 + 0.006,
|
||||
size: [0.012, 0.12, 0.012],
|
||||
});
|
||||
}
|
||||
slab(bin, ctx, top, {
|
||||
y: p.counterHeight - 0.045,
|
||||
width: p.width,
|
||||
depth: p.depth + 0.05,
|
||||
thickness: 0.045,
|
||||
});
|
||||
|
||||
// Backsplash and upper units stop short over the sink to create a focal bay.
|
||||
panelSlab(bin, ctx, ctx.materials.get("tile"), {
|
||||
y: p.counterHeight,
|
||||
z: -p.depth / 2 + 0.012,
|
||||
width: p.width,
|
||||
height: 0.62,
|
||||
thickness: 0.024,
|
||||
faces: "front",
|
||||
});
|
||||
const upperY = 1.48;
|
||||
for (let i = 0; i < bays; i += 1) {
|
||||
if (i === Math.floor(bays / 2)) continue;
|
||||
const x = -p.width / 2 + bayW * (i + 0.5);
|
||||
bin.add(P.box(), cabinet, {
|
||||
x,
|
||||
y: upperY,
|
||||
z: -p.depth / 2 + 0.17,
|
||||
size: [bayW - 0.018, p.height - upperY, 0.34],
|
||||
});
|
||||
bin.add(P.box(), metal, {
|
||||
x: x + bayW * 0.34,
|
||||
y: upperY + 0.2,
|
||||
z: -p.depth / 2 + 0.345,
|
||||
size: [0.012, 0.13, 0.012],
|
||||
});
|
||||
}
|
||||
|
||||
const sinkX = -p.width / 2 + bayW * (Math.floor(bays / 2) + 0.5);
|
||||
bin.add(P.box(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.003,
|
||||
size: [bayW * 0.62, 0.018, p.depth * 0.5],
|
||||
});
|
||||
bin.add(P.rod(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.02,
|
||||
z: -0.12,
|
||||
size: [0.025, 0.24, 0.025],
|
||||
});
|
||||
bin.add(P.rod(), metal, {
|
||||
x: sinkX,
|
||||
y: p.counterHeight + 0.24,
|
||||
z: -0.04,
|
||||
size: [0.025, 0.025, 0.16],
|
||||
});
|
||||
|
||||
return bin.build("kitchen.run");
|
||||
},
|
||||
});
|
||||
|
||||
type IslandParams = {
|
||||
length: number;
|
||||
depth: number;
|
||||
height: number;
|
||||
overhang: number;
|
||||
};
|
||||
|
||||
/** A storage island with a real seating overhang on its +Z side. */
|
||||
export const kitchenIsland = defineAsset<IslandParams>({
|
||||
id: "tera:kitchen.island",
|
||||
label: "Kitchen island",
|
||||
defaults: { length: 2.2, depth: 0.9, height: 0.92, overhang: 0.24 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.length, depth: p.depth, height: p.height, clearance: 0.8 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const cabinetDepth = Math.max(0.42, p.depth - p.overhang);
|
||||
bin.add(ctx.parts.box(), tintable(ctx, "cabinet"), {
|
||||
y: 0.06,
|
||||
z: -p.overhang / 2,
|
||||
size: [p.length - 0.12, p.height - 0.12, cabinetDepth],
|
||||
});
|
||||
slab(bin, ctx, ctx.materials.get("tableTop"), {
|
||||
y: p.height - 0.05,
|
||||
width: p.length,
|
||||
depth: p.depth,
|
||||
thickness: 0.05,
|
||||
});
|
||||
for (const sx of [-1, 1]) {
|
||||
bin.add(ctx.parts.rod(), ctx.materials.get("metalTrim"), {
|
||||
x: sx * (p.length / 2 - 0.12),
|
||||
y: 0.08,
|
||||
z: p.depth / 2 - 0.08,
|
||||
size: [0.035, p.height - 0.13, 0.035],
|
||||
});
|
||||
}
|
||||
return bin.build("kitchen.island");
|
||||
},
|
||||
});
|
||||
|
||||
type WardrobeParams = { width: number; depth: number; height: number; doors: number };
|
||||
|
||||
/** Full-height closed storage with a recessed plinth and quiet vertical pulls. */
|
||||
export const storageWardrobe = defineAsset<WardrobeParams>({
|
||||
id: "tera:storage.wardrobe",
|
||||
label: "Wardrobe",
|
||||
defaults: { width: 1.8, depth: 0.58, height: 2.18, doors: 3 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.width, depth: p.depth, height: p.height, clearance: 0.75 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const body = ctx.materials.get("shelf");
|
||||
const doors = tintable(ctx, "cabinet");
|
||||
const metal = ctx.materials.get("metalTrim");
|
||||
const count = Math.max(2, Math.min(5, Math.round(p.doors)));
|
||||
const doorW = (p.width - 0.05) / count;
|
||||
bin.add(ctx.parts.box(), ctx.materials.get("chairBase"), {
|
||||
x: 0,
|
||||
y: 0.02,
|
||||
z: -0.02,
|
||||
size: [p.width - 0.1, 0.08, p.depth - 0.1],
|
||||
});
|
||||
bin.add(ctx.parts.box(), body, { y: 0.08, size: [p.width, p.height - 0.08, p.depth] });
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const x = -p.width / 2 + 0.025 + doorW * (i + 0.5);
|
||||
bin.add(ctx.parts.box(), doors, {
|
||||
x,
|
||||
y: 0.11,
|
||||
z: p.depth / 2 + 0.006,
|
||||
size: [doorW - 0.014, p.height - 0.16, 0.018],
|
||||
});
|
||||
bin.add(ctx.parts.box(), metal, {
|
||||
x: x + doorW * 0.34,
|
||||
y: p.height * 0.46,
|
||||
z: p.depth / 2 + 0.02,
|
||||
size: [0.014, 0.28, 0.014],
|
||||
});
|
||||
}
|
||||
return bin.build("storage.wardrobe");
|
||||
},
|
||||
});
|
||||
|
||||
type StoolParams = { diameter: number; seatHeight: number; back: boolean };
|
||||
|
||||
/** A kitchen-height stool. It faces -Z so a seat address can share its yaw. */
|
||||
export const seatStool = defineAsset<StoolParams>({
|
||||
id: "tera:seat.stool",
|
||||
label: "Counter stool",
|
||||
defaults: { diameter: 0.4, seatHeight: 0.68, back: true },
|
||||
|
||||
footprint(p) {
|
||||
return { width: p.diameter + 0.1, depth: p.diameter + 0.12, height: p.back ? 0.96 : p.seatHeight, clearance: 0.35 };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const bin = new MeshBin();
|
||||
const metal = ctx.materials.get("chairBase");
|
||||
const textile = tintable(ctx, "chairFabric");
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const yaw = Math.PI / 4 + i * Math.PI / 2;
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: Math.sin(yaw) * p.diameter * 0.34,
|
||||
z: Math.cos(yaw) * p.diameter * 0.34,
|
||||
size: [0.025, p.seatHeight - 0.06, 0.025],
|
||||
});
|
||||
}
|
||||
bin.add(ctx.parts.cylinder(20), textile, {
|
||||
y: p.seatHeight - 0.06,
|
||||
size: [p.diameter, 0.1, p.diameter],
|
||||
});
|
||||
if (p.back) {
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: -p.diameter * 0.34,
|
||||
y: p.seatHeight - 0.02,
|
||||
z: p.diameter * 0.34,
|
||||
size: [0.022, 0.28, 0.022],
|
||||
});
|
||||
bin.add(ctx.parts.rod(), metal, {
|
||||
x: p.diameter * 0.34,
|
||||
y: p.seatHeight - 0.02,
|
||||
z: p.diameter * 0.34,
|
||||
size: [0.022, 0.28, 0.022],
|
||||
});
|
||||
// Keep the back and circular seat in the same indexed primitive family.
|
||||
// `roundedBox` is non-indexed; mixing it with the indexed cylinder would
|
||||
// make BufferGeometryUtils reject the material bin and drop every fabric
|
||||
// part from the finished stool.
|
||||
bin.add(ctx.parts.box(), textile, {
|
||||
y: p.seatHeight + 0.18,
|
||||
z: p.diameter * 0.34,
|
||||
size: [p.diameter * 0.82, 0.2, 0.06],
|
||||
});
|
||||
}
|
||||
return bin.build("seat.stool");
|
||||
},
|
||||
});
|
||||
|
||||
type FloorLightParams = { height: number; reach: number; shadeDiameter: number };
|
||||
|
||||
/** An arced floor lamp. Geometry glows; the office lighting rig remains the only light owner. */
|
||||
export const lightFloor = defineAsset<FloorLightParams>({
|
||||
id: "tera:light.floor",
|
||||
label: "Floor lamp",
|
||||
defaults: { height: 1.55, reach: 0.42, shadeDiameter: 0.32 },
|
||||
|
||||
footprint(p) {
|
||||
return { width: Math.max(0.42, p.shadeDiameter), depth: p.reach + p.shadeDiameter / 2, height: p.height };
|
||||
},
|
||||
|
||||
build(p, ctx) {
|
||||
const body = new MeshBin();
|
||||
const glow = new MeshBin();
|
||||
const metal = ctx.materials.get("lightHousing");
|
||||
body.add(ctx.parts.cylinder(20), ctx.materials.get("chairBase"), {
|
||||
size: [0.36, 0.035, 0.36],
|
||||
});
|
||||
body.add(ctx.parts.rod(), metal, { y: 0.035, size: [0.025, p.height - 0.18, 0.025] });
|
||||
body.add(ctx.parts.rod(), metal, {
|
||||
y: p.height - 0.18,
|
||||
z: -p.reach / 2,
|
||||
size: [0.025, 0.025, p.reach],
|
||||
});
|
||||
body.add(ctx.parts.cone(20), metal, {
|
||||
y: p.height - 0.4,
|
||||
z: -p.reach,
|
||||
size: [p.shadeDiameter, 0.24, p.shadeDiameter],
|
||||
pitch: Math.PI,
|
||||
});
|
||||
glow.add(ctx.parts.disc(20), ctx.materials.get("lightDiffuser"), {
|
||||
y: p.height - 0.395,
|
||||
z: -p.reach,
|
||||
size: [p.shadeDiameter * 0.86, 1, p.shadeDiameter * 0.86],
|
||||
pitch: Math.PI,
|
||||
});
|
||||
const group = new THREE.Group();
|
||||
group.name = "light.floor";
|
||||
group.add(body.build("light.floor:body"));
|
||||
group.add(glow.build("light.floor:glow", { castShadow: false, receiveShadow: false }));
|
||||
return group;
|
||||
},
|
||||
});
|
||||
@@ -17,7 +17,7 @@
|
||||
* somebody walks through a wall — leave the collider with no gap where the door
|
||||
* is.
|
||||
*
|
||||
* Seventeen assets is not a furniture catalogue and is not trying to be. It is
|
||||
* Twenty-five assets is not a furniture catalogue and is not trying to be. It is
|
||||
* the set that gets a real floor plate looking like an office: somewhere to
|
||||
* work, somewhere to sit, somewhere to meet, somewhere to put things, something
|
||||
* to look at, something alive, and light.
|
||||
@@ -26,6 +26,15 @@
|
||||
import { kit, type AnyAsset, type AssetRegistry } from "../kit.ts";
|
||||
import { deskPartition, deskPedestal, deskWorkstation } from "./desks.ts";
|
||||
import { plantPotted, plantTall } from "./greenery.ts";
|
||||
import {
|
||||
bedPlatform,
|
||||
kitchenIsland,
|
||||
kitchenRun,
|
||||
lightFloor,
|
||||
seatStool,
|
||||
sofaModular,
|
||||
storageWardrobe,
|
||||
} from "./habitat.ts";
|
||||
import { lightPendant, lightTroffer } from "./lighting.ts";
|
||||
import { robotOptimus } from "./optimus.ts";
|
||||
import { screenMonitor, screenWallDisplay } from "./screens.ts";
|
||||
@@ -53,6 +62,13 @@ export const OFFICE_ASSETS: readonly AnyAsset[] = [
|
||||
robotOptimus,
|
||||
rug,
|
||||
whiteboard,
|
||||
bedPlatform,
|
||||
sofaModular,
|
||||
kitchenRun,
|
||||
kitchenIsland,
|
||||
storageWardrobe,
|
||||
seatStool,
|
||||
lightFloor,
|
||||
];
|
||||
|
||||
/** Register the built-in catalogue into a registry. Defaults to the shared one. */
|
||||
@@ -81,4 +97,11 @@ export {
|
||||
tableMeeting,
|
||||
tableSide,
|
||||
whiteboard,
|
||||
bedPlatform,
|
||||
sofaModular,
|
||||
kitchenRun,
|
||||
kitchenIsland,
|
||||
storageWardrobe,
|
||||
seatStool,
|
||||
lightFloor,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user