@agent-os-lab/agent-game-sdk 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,13 @@ const OFFICE_FLOOR_CENTER_X = 16;
10
10
  const OFFICE_FLOOR_WIDTH = 68 * OFFICE_LAYOUT_SCALE;
11
11
  const OFFICE_FLOOR_DEPTH = 24 * OFFICE_LAYOUT_SCALE;
12
12
  const OFFICE_FLOOR_TILE_SIZE = 3;
13
+ const GLASS_FACADE_DEFAULT_HEIGHT = 17.2;
14
+ const GLASS_FACADE_OUTWARD_OFFSET = 0.42;
15
+ const GLASS_FACADE_PANEL_THICKNESS = 0.08;
16
+ const GLASS_FACADE_VERTICAL_OVERLAP = 0.35;
17
+ const GLASS_ROOF_THICKNESS = 0.08;
18
+ const GLASS_FACADE_DETAIL_OFFSET = 0.07;
19
+ const GLASS_FACADE_FRAME_THICKNESS = 0.12;
13
20
 
14
21
  export function scaleOfficeX(x: number): number {
15
22
  return OFFICE_FLOOR_CENTER_X + (x - OFFICE_FLOOR_CENTER_X) * OFFICE_LAYOUT_SCALE;
@@ -162,20 +169,272 @@ export function addLights(scene: THREE.Scene): void {
162
169
  }
163
170
 
164
171
  export function buildOfficeScene(scene: THREE.Scene, layout: ResolvedOfficeLayout): void {
165
- buildOfficeBaseFloors(scene, layout);
166
- buildOfficeWalls(scene, layout);
167
-
168
- const batcher = new BoxInstanceBatcher(scene);
169
- const transparentCache = new TransparentBoxCache();
170
- activeBoxBatcher = batcher;
171
- activeTransparentBoxCache = transparentCache;
172
- try {
173
- layout.components.forEach((component) => renderOfficeComponent(scene, component));
174
- } finally {
175
- activeBoxBatcher = undefined;
176
- activeTransparentBoxCache = undefined;
172
+ buildOfficeGround(scene, layout);
173
+ buildBuildingBase(scene, layout);
174
+ buildBuildingStructuralColumns(scene, layout);
175
+ buildBuildingConnectors(scene, layout);
176
+ const floorElevations = layout.building.floors.map((floor) => floor.elevation);
177
+ const firstFloorElevation = Math.min(...floorElevations);
178
+ const topFloorElevation = Math.max(...floorElevations);
179
+
180
+ for (const floor of layout.building.floors) {
181
+ const floorGroup = new THREE.Group();
182
+ floorGroup.name = `officeFloor:${floor.id}`;
183
+ floorGroup.position.y = floor.elevation;
184
+ const floorTarget = floorGroup as unknown as THREE.Scene;
185
+ buildOfficeBaseFloors(floorTarget, floor.floorTiles);
186
+ buildOfficeWalls(floorTarget, floor.walls);
187
+ const higherFloorElevations = floorElevations.filter((elevation) => elevation > floor.elevation);
188
+ const glassHeight = higherFloorElevations.length > 0
189
+ ? Math.min(...higherFloorElevations) - floor.elevation + GLASS_FACADE_VERTICAL_OVERLAP
190
+ : GLASS_FACADE_DEFAULT_HEIGHT;
191
+ buildOfficeGlassFacade(floorTarget, floor.floorTiles, glassHeight, {
192
+ detail: floor.elevation === firstFloorElevation ? "entrance" : undefined,
193
+ includeRoof: floor.elevation === topFloorElevation,
194
+ });
195
+
196
+ const batcher = new BoxInstanceBatcher(floorTarget);
197
+ const transparentCache = new TransparentBoxCache();
198
+ activeBoxBatcher = batcher;
199
+ activeTransparentBoxCache = transparentCache;
200
+ try {
201
+ floor.components.forEach((component) => renderOfficeComponent(floorTarget, component));
202
+ } finally {
203
+ activeBoxBatcher = undefined;
204
+ activeTransparentBoxCache = undefined;
205
+ }
206
+ batcher.flush();
207
+ scene.add(floorGroup);
208
+ }
209
+ }
210
+
211
+ function buildBuildingBase(scene: THREE.Scene, layout: ResolvedOfficeLayout): void {
212
+ const floors = [...layout.building.floors].sort((a, b) => a.elevation - b.elevation);
213
+ const groundFloor = floors[0];
214
+ if (!groundFloor) {
215
+ return;
216
+ }
217
+ const bounds = getFloorTileEdges(groundFloor.floorTiles);
218
+ if (!bounds) {
219
+ return;
220
+ }
221
+
222
+ const group = new THREE.Group();
223
+ group.name = "officeBuildingBase";
224
+ const batcher = new ColoredBoxBatcher(group, "officeBuildingBaseMesh");
225
+ const width = bounds.maxX - bounds.minX;
226
+ const depth = bounds.maxZ - bounds.minZ;
227
+ const x = (bounds.minX + bounds.maxX) / 2;
228
+ const z = (bounds.minZ + bounds.maxZ) / 2;
229
+
230
+ addBaseRing(batcher, 0x9f9a8b, width, depth, x, z, 1.6, 0.48, -0.24);
231
+ addBaseRing(batcher, 0x858173, width + 1.6, depth + 1.6, x, z, 1.2, 0.16, -0.56);
232
+ batcher.flush();
233
+ scene.add(group);
234
+ }
235
+
236
+ function addBaseRing(
237
+ batcher: ColoredBoxBatcher,
238
+ color: number,
239
+ innerWidth: number,
240
+ innerDepth: number,
241
+ x: number,
242
+ z: number,
243
+ thickness: number,
244
+ height: number,
245
+ y: number,
246
+ ): void {
247
+ batcher.addBox(color, innerWidth + thickness * 2, height, thickness, x, y, z - innerDepth / 2 - thickness / 2, 0);
248
+ batcher.addBox(color, innerWidth + thickness * 2, height, thickness, x, y, z + innerDepth / 2 + thickness / 2, 0);
249
+ batcher.addBox(color, thickness, height, innerDepth, x - innerWidth / 2 - thickness / 2, y, z, 0);
250
+ batcher.addBox(color, thickness, height, innerDepth, x + innerWidth / 2 + thickness / 2, y, z, 0);
251
+ }
252
+
253
+ function buildBuildingStructuralColumns(scene: THREE.Scene, layout: ResolvedOfficeLayout): void {
254
+ const floors = [...layout.building.floors].sort((a, b) => a.elevation - b.elevation);
255
+ if (floors.length < 2) {
256
+ return;
257
+ }
258
+
259
+ const group = new THREE.Group();
260
+ group.name = "officeStructuralColumns";
261
+ const batcher = new BoxInstanceBatcher(group);
262
+
263
+ for (let floorIndex = 1; floorIndex < floors.length; floorIndex += 1) {
264
+ const lowerFloor = floors[floorIndex - 1]!;
265
+ const upperFloor = floors[floorIndex]!;
266
+ const bounds = getFloorTileEdges(upperFloor.floorTiles);
267
+ if (!bounds) {
268
+ continue;
269
+ }
270
+
271
+ const columnHeight = Math.max(0.1, upperFloor.elevation - lowerFloor.elevation - 0.2);
272
+ const columnY = lowerFloor.elevation + 0.1 + columnHeight / 2;
273
+ getStructuralColumnPositions(bounds).forEach((position) => {
274
+ batcher.addBox(0x9a9688, 0.58, columnHeight, 0.58, position.x, columnY, position.z, 0);
275
+ });
177
276
  }
277
+
178
278
  batcher.flush();
279
+ scene.add(group);
280
+ }
281
+
282
+ function getFloorTileEdges(floorTiles: ResolvedOfficeLayout["scene"]["floorTiles"]) {
283
+ if (floorTiles.length === 0) {
284
+ return null;
285
+ }
286
+ return floorTiles.reduce(
287
+ (bounds, tile) => ({
288
+ minX: Math.min(bounds.minX, tile.x - tile.width / 2),
289
+ maxX: Math.max(bounds.maxX, tile.x + tile.width / 2),
290
+ minZ: Math.min(bounds.minZ, tile.z - tile.depth / 2),
291
+ maxZ: Math.max(bounds.maxZ, tile.z + tile.depth / 2),
292
+ }),
293
+ {
294
+ minX: Number.POSITIVE_INFINITY,
295
+ maxX: Number.NEGATIVE_INFINITY,
296
+ minZ: Number.POSITIVE_INFINITY,
297
+ maxZ: Number.NEGATIVE_INFINITY,
298
+ },
299
+ );
300
+ }
301
+
302
+ function getStructuralColumnPositions(bounds: { minX: number; maxX: number; minZ: number; maxZ: number }) {
303
+ const inset = 1.25;
304
+ const minX = bounds.minX + inset;
305
+ const maxX = bounds.maxX - inset;
306
+ const minZ = bounds.minZ + inset;
307
+ const maxZ = bounds.maxZ - inset;
308
+ return [
309
+ { x: minX, z: minZ },
310
+ { x: maxX, z: minZ },
311
+ { x: minX, z: maxZ },
312
+ { x: maxX, z: maxZ },
313
+ ];
314
+ }
315
+
316
+ function buildOfficeGround(scene: THREE.Scene, layout: ResolvedOfficeLayout): void {
317
+ const group = new THREE.Group();
318
+ group.name = "officeGroundEnvironment";
319
+
320
+ const campusWidth = layout.scene.width + 110;
321
+ const campusDepth = layout.scene.depth + 92;
322
+ const centerX = layout.scene.center.x;
323
+ const centerZ = layout.scene.center.z;
324
+ const westX = centerX - layout.scene.width / 2 - 18;
325
+ const southZ = centerZ + layout.scene.depth / 2 + 18;
326
+ const parkingX = westX - 12;
327
+ const parkingZ = southZ - 14;
328
+ const treeGroup = new THREE.Group();
329
+ treeGroup.name = "officeTrees";
330
+ const vehicleGroup = new THREE.Group();
331
+ vehicleGroup.name = "officeVehicles";
332
+ const batcher = new ColoredBoxBatcher(group, "officeGroundEnvironmentMesh");
333
+
334
+ addEnvironmentMarker(group, "officeGroundPlane");
335
+ addEnvironmentMarker(group, "officeRoad:main");
336
+ addEnvironmentMarker(group, "officeRoad:service");
337
+ addEnvironmentMarker(group, "officeSidewalk:south");
338
+ addEnvironmentMarker(group, "officeSidewalk:west");
339
+ addEnvironmentMarker(group, "officeParking:visitor");
340
+ batcher.addBox(0xb9c0b7, campusWidth, 0.08, campusDepth, centerX, -0.16, centerZ, 0);
341
+ batcher.addBox(0x4b5563, campusWidth - 20, 0.04, 9, centerX, -0.09, southZ, 0);
342
+ batcher.addBox(0x4b5563, 8, 0.04, campusDepth - 18, westX, -0.09, centerZ, 0);
343
+ batcher.addBox(0xd7d3c8, campusWidth - 28, 0.04, 3.2, centerX, -0.06, southZ - 6.2, 0);
344
+ batcher.addBox(0xd7d3c8, 3.2, 0.04, campusDepth - 26, westX + 5.8, -0.06, centerZ, 0);
345
+ batcher.addBox(0x8d948b, 14, 0.035, 10, parkingX, -0.055, parkingZ, 0);
346
+
347
+ for (const xOffset of [-42, -30, -18, -6, 8, 20, 32, 44]) {
348
+ addTree(batcher, centerX + xOffset, southZ + 7.5);
349
+ }
350
+ for (const zOffset of [-28, -16, -4, 8]) {
351
+ addTree(batcher, westX - 8, centerZ + zOffset);
352
+ }
353
+
354
+ addCar(batcher, centerX - 28, southZ + 0.6, 0xe11d48, 0);
355
+ addCar(batcher, centerX - 12, southZ - 1.2, 0x2563eb, Math.PI);
356
+ addCar(batcher, centerX + 18, southZ + 0.7, 0xf59e0b, 0);
357
+ addCar(batcher, westX, centerZ - 18, 0x14b8a6, Math.PI / 2);
358
+ addCar(batcher, parkingX, parkingZ, 0xf8fafc, Math.PI / 2);
359
+ batcher.flush();
360
+ group.add(treeGroup, vehicleGroup);
361
+
362
+ scene.add(group);
363
+ }
364
+
365
+ function addEnvironmentMarker(scene: THREE.Object3D, name: string): void {
366
+ const marker = new THREE.Group();
367
+ marker.name = name;
368
+ scene.add(marker);
369
+ }
370
+
371
+ function addTree(batcher: ColoredBoxBatcher, x: number, z: number): void {
372
+ batcher.addBox(0x7c4a28, 0.36, 1.4, 0.36, x, 0.56, z, 0);
373
+ batcher.addBox(0x2f6b3f, 1.65, 1.35, 1.65, x, 1.62, z, Math.PI / 4);
374
+ batcher.addBox(0x3f8550, 1.25, 1.05, 1.25, x, 2.35, z, Math.PI / 4);
375
+ }
376
+
377
+ function addCar(batcher: ColoredBoxBatcher, x: number, z: number, color: number, rotationY: number): void {
378
+ batcher.addBox(color, 3.4, 0.7, 1.55, x, 0.28, z, rotationY);
379
+ batcher.addBox(
380
+ 0xc7d2fe,
381
+ 1.45,
382
+ 0.48,
383
+ 1.18,
384
+ x + Math.cos(rotationY) * 0.28,
385
+ 0.86,
386
+ z - Math.sin(rotationY) * 0.28,
387
+ rotationY,
388
+ );
389
+ for (const wheelX of [-1.05, 1.05]) {
390
+ for (const wheelZ of [-0.72, 0.72]) {
391
+ addLocalEnvironmentBox(batcher, 0x111827, 0.42, 0.42, 0.18, x, z, wheelX, 0.16, wheelZ, rotationY);
392
+ }
393
+ }
394
+ }
395
+
396
+ function addLocalEnvironmentBox(
397
+ batcher: ColoredBoxBatcher,
398
+ color: number,
399
+ width: number,
400
+ height: number,
401
+ depth: number,
402
+ originX: number,
403
+ originZ: number,
404
+ localX: number,
405
+ y: number,
406
+ localZ: number,
407
+ rotationY: number,
408
+ ) {
409
+ const cos = Math.cos(rotationY);
410
+ const sin = Math.sin(rotationY);
411
+ batcher.addBox(
412
+ color,
413
+ width,
414
+ height,
415
+ depth,
416
+ originX + localX * cos + localZ * sin,
417
+ y,
418
+ originZ - localX * sin + localZ * cos,
419
+ rotationY,
420
+ );
421
+ }
422
+
423
+ export const OFFICE_FLOOR_FOCUS_DIM_OPACITY = 0.18;
424
+
425
+ export function applyOfficeFloorFocus(
426
+ scene: THREE.Scene,
427
+ layout: ResolvedOfficeLayout,
428
+ focusedFloorId: string | null,
429
+ ): void {
430
+ const floorIds = new Set(layout.building.floors.map((floor) => floor.id));
431
+ scene.children.forEach((child) => {
432
+ const floorId = parseOfficeFloorGroupId(child.name);
433
+ if (!floorId || !floorIds.has(floorId)) {
434
+ return;
435
+ }
436
+ setObjectOpacity(child, !focusedFloorId || floorId === focusedFloorId ? 1 : OFFICE_FLOOR_FOCUS_DIM_OPACITY);
437
+ });
179
438
  }
180
439
 
181
440
  export function disposeObject(object: THREE.Object3D): void {
@@ -193,13 +452,48 @@ export function disposeObject(object: THREE.Object3D): void {
193
452
  });
194
453
  }
195
454
 
455
+ function parseOfficeFloorGroupId(name: string): string | null {
456
+ return name.startsWith("officeFloor:") ? name.slice("officeFloor:".length) : null;
457
+ }
458
+
459
+ export function setObjectOpacity(object: THREE.Object3D, opacity: number): void {
460
+ object.traverse((child) => {
461
+ const mesh = child as THREE.Mesh;
462
+ if (!mesh.material) {
463
+ return;
464
+ }
465
+ if (Array.isArray(mesh.material)) {
466
+ mesh.material = mesh.material.map((material) => prepareFocusMaterial(material, opacity));
467
+ } else {
468
+ mesh.material = prepareFocusMaterial(mesh.material, opacity);
469
+ }
470
+ });
471
+ }
472
+
473
+ function prepareFocusMaterial<TMaterial extends THREE.Material>(material: TMaterial, opacity: number): TMaterial {
474
+ const focusMaterial = (material.userData.officeFocusMaterialCloned ? material : material.clone()) as TMaterial;
475
+ const baseOpacity = typeof material.userData.officeBaseOpacity === "number" ? material.userData.officeBaseOpacity : material.opacity;
476
+ const baseTransparent = typeof material.userData.officeBaseTransparent === "boolean" ? material.userData.officeBaseTransparent : material.transparent;
477
+ const baseDepthWrite = typeof material.userData.officeBaseDepthWrite === "boolean" ? material.userData.officeBaseDepthWrite : material.depthWrite;
478
+ const resolvedOpacity = baseOpacity * opacity;
479
+ focusMaterial.userData.officeFocusMaterialCloned = true;
480
+ focusMaterial.userData.officeBaseOpacity = baseOpacity;
481
+ focusMaterial.userData.officeBaseTransparent = baseTransparent;
482
+ focusMaterial.userData.officeBaseDepthWrite = baseDepthWrite;
483
+ focusMaterial.transparent = baseTransparent || resolvedOpacity < 1;
484
+ focusMaterial.opacity = resolvedOpacity;
485
+ focusMaterial.depthWrite = baseDepthWrite && resolvedOpacity >= 1;
486
+ focusMaterial.needsUpdate = true;
487
+ return focusMaterial;
488
+ }
489
+
196
490
  class BoxInstanceBatcher {
197
- private readonly scene: THREE.Scene;
491
+ private readonly scene: THREE.Object3D;
198
492
  private readonly geometryCache = new Map<string, THREE.BoxGeometry>();
199
493
  private readonly materialCache = new Map<number, THREE.MeshLambertMaterial>();
200
494
  private readonly batches = new Map<number, THREE.BufferGeometry[]>();
201
495
 
202
- constructor(scene: THREE.Scene) {
496
+ constructor(scene: THREE.Object3D) {
203
497
  this.scene = scene;
204
498
  }
205
499
 
@@ -257,6 +551,63 @@ class BoxInstanceBatcher {
257
551
  }
258
552
  }
259
553
 
554
+ class ColoredBoxBatcher {
555
+ private readonly scene: THREE.Object3D;
556
+ private readonly name: string;
557
+ private readonly geometries: THREE.BufferGeometry[] = [];
558
+
559
+ constructor(scene: THREE.Object3D, name: string) {
560
+ this.scene = scene;
561
+ this.name = name;
562
+ }
563
+
564
+ addBox(
565
+ color: number,
566
+ width: number,
567
+ height: number,
568
+ depth: number,
569
+ x: number,
570
+ y: number,
571
+ z: number,
572
+ rotationY: number,
573
+ ) {
574
+ const geometry = new THREE.BoxGeometry(width, height, depth);
575
+ matrixHelper.position.set(x, y, z);
576
+ matrixHelper.rotation.set(0, rotationY, 0);
577
+ matrixHelper.scale.set(1, 1, 1);
578
+ matrixHelper.updateMatrix();
579
+ geometry.applyMatrix4(matrixHelper.matrix);
580
+
581
+ const vertexColor = new THREE.Color(color);
582
+ const position = geometry.getAttribute("position");
583
+ const colors = new Float32Array(position.count * 3);
584
+ for (let index = 0; index < position.count; index += 1) {
585
+ colors[index * 3] = vertexColor.r;
586
+ colors[index * 3 + 1] = vertexColor.g;
587
+ colors[index * 3 + 2] = vertexColor.b;
588
+ }
589
+ geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
590
+ this.geometries.push(geometry);
591
+ }
592
+
593
+ flush() {
594
+ const mergedGeometry = mergeGeometries(this.geometries, false);
595
+ this.geometries.forEach((geometry) => geometry.dispose());
596
+ this.geometries.length = 0;
597
+ if (!mergedGeometry) {
598
+ return;
599
+ }
600
+ const mesh = new THREE.Mesh(
601
+ mergedGeometry,
602
+ new THREE.MeshLambertMaterial({ vertexColors: true }),
603
+ );
604
+ mesh.castShadow = false;
605
+ mesh.receiveShadow = true;
606
+ mesh.name = this.name;
607
+ this.scene.add(mesh);
608
+ }
609
+ }
610
+
260
611
  let activeBoxBatcher: BoxInstanceBatcher | undefined;
261
612
 
262
613
  class TransparentBoxCache {
@@ -294,8 +645,8 @@ class TransparentBoxCache {
294
645
 
295
646
  let activeTransparentBoxCache: TransparentBoxCache | undefined;
296
647
 
297
- function buildOfficeBaseFloors(scene: THREE.Scene, layout: ResolvedOfficeLayout) {
298
- layout.scene.floorTiles.forEach((tile) => {
648
+ function buildOfficeBaseFloors(scene: THREE.Scene, floorTiles: ResolvedOfficeLayout["scene"]["floorTiles"]) {
649
+ floorTiles.forEach((tile) => {
299
650
  addBaseFloor(scene, tile.color, tile.x, tile.z, tile.width, tile.depth);
300
651
  });
301
652
  }
@@ -335,9 +686,9 @@ function addGridFloorLines(scene: THREE.Scene, x: number, z: number, width: numb
335
686
  scene.add(lines);
336
687
  }
337
688
 
338
- function buildOfficeWalls(scene: THREE.Scene, layout: ResolvedOfficeLayout) {
689
+ function buildOfficeWalls(scene: THREE.Scene, walls: ResolvedOfficeLayout["scene"]["walls"]) {
339
690
  const materialCache = new Map<number, THREE.MeshLambertMaterial>();
340
- layout.scene.walls.forEach((wall) => {
691
+ walls.forEach((wall) => {
341
692
  const material = materialCache.get(wall.color) ?? (() => {
342
693
  const created = new THREE.MeshLambertMaterial({ color: wall.color });
343
694
  materialCache.set(wall.color, created);
@@ -356,6 +707,164 @@ function buildOfficeWalls(scene: THREE.Scene, layout: ResolvedOfficeLayout) {
356
707
  });
357
708
  }
358
709
 
710
+ function buildOfficeGlassFacade(
711
+ scene: THREE.Scene,
712
+ floorTiles: ResolvedOfficeLayout["scene"]["floorTiles"],
713
+ height = GLASS_FACADE_DEFAULT_HEIGHT,
714
+ options: { detail?: "entrance" | "windows"; includeRoof?: boolean } = {},
715
+ ) {
716
+ const bounds = getFloorTileEdges(floorTiles);
717
+ if (!bounds) {
718
+ return;
719
+ }
720
+ const geometries: THREE.BufferGeometry[] = [];
721
+ const group = new THREE.Group();
722
+ group.name = "officeGlassFacade";
723
+ const width = bounds.maxX - bounds.minX;
724
+ const depth = bounds.maxZ - bounds.minZ;
725
+ const closedWidth = width + GLASS_FACADE_OUTWARD_OFFSET * 2 + GLASS_FACADE_PANEL_THICKNESS;
726
+ const closedDepth = depth + GLASS_FACADE_OUTWARD_OFFSET * 2 + GLASS_FACADE_PANEL_THICKNESS;
727
+ const centerX = (bounds.minX + bounds.maxX) / 2;
728
+ const centerZ = (bounds.minZ + bounds.maxZ) / 2;
729
+ const panelY = height / 2;
730
+
731
+ [
732
+ { id: "north", width: closedWidth, depth: GLASS_FACADE_PANEL_THICKNESS, x: centerX, z: bounds.minZ - GLASS_FACADE_OUTWARD_OFFSET },
733
+ { id: "south", width: closedWidth, depth: GLASS_FACADE_PANEL_THICKNESS, x: centerX, z: bounds.maxZ + GLASS_FACADE_OUTWARD_OFFSET },
734
+ { id: "west", width: GLASS_FACADE_PANEL_THICKNESS, depth: closedDepth, x: bounds.minX - GLASS_FACADE_OUTWARD_OFFSET, z: centerZ },
735
+ { id: "east", width: GLASS_FACADE_PANEL_THICKNESS, depth: closedDepth, x: bounds.maxX + GLASS_FACADE_OUTWARD_OFFSET, z: centerZ },
736
+ ].forEach((panel) => {
737
+ const geometry = new THREE.BoxGeometry(panel.width, height, panel.depth);
738
+ matrixHelper.position.set(panel.x, panelY, panel.z);
739
+ matrixHelper.rotation.set(0, 0, 0);
740
+ matrixHelper.scale.set(1, 1, 1);
741
+ matrixHelper.updateMatrix();
742
+ geometry.applyMatrix4(matrixHelper.matrix);
743
+ geometries.push(geometry);
744
+
745
+ const marker = new THREE.Object3D();
746
+ marker.name = `officeGlassFacade:${panel.id}`;
747
+ marker.position.set(panel.x, panelY, panel.z);
748
+ marker.userData.officeGlassFacadePanel = { width: panel.width, height, depth: panel.depth };
749
+ group.add(marker);
750
+ });
751
+
752
+ if (options.includeRoof) {
753
+ const geometry = new THREE.BoxGeometry(closedWidth, GLASS_ROOF_THICKNESS, closedDepth);
754
+ matrixHelper.position.set(centerX, height + GLASS_ROOF_THICKNESS / 2, centerZ);
755
+ matrixHelper.rotation.set(0, 0, 0);
756
+ matrixHelper.scale.set(1, 1, 1);
757
+ matrixHelper.updateMatrix();
758
+ geometry.applyMatrix4(matrixHelper.matrix);
759
+ geometries.push(geometry);
760
+
761
+ const marker = new THREE.Object3D();
762
+ marker.name = "officeGlassRoof";
763
+ marker.position.set(centerX, height, centerZ);
764
+ marker.userData.officeGlassRoof = { width: closedWidth, height: GLASS_ROOF_THICKNESS, depth: closedDepth };
765
+ group.add(marker);
766
+ }
767
+
768
+ const mergedGeometry = mergeGeometries(geometries, false);
769
+ geometries.forEach((geometry) => geometry.dispose());
770
+ if (!mergedGeometry) {
771
+ return;
772
+ }
773
+ const mesh = new THREE.Mesh(
774
+ mergedGeometry,
775
+ new THREE.MeshLambertMaterial({ color: 0x94a3b8, transparent: true, opacity: 0.18, depthWrite: false }),
776
+ );
777
+ mesh.name = "officeGlassFacadeMesh";
778
+ mesh.castShadow = false;
779
+ mesh.receiveShadow = false;
780
+ group.add(mesh);
781
+ if (options.detail === "entrance") {
782
+ addGlassFacadeEntrance(group, bounds.maxZ + GLASS_FACADE_OUTWARD_OFFSET + GLASS_FACADE_DETAIL_OFFSET, centerX);
783
+ }
784
+ scene.add(group);
785
+ }
786
+
787
+ function addGlassFacadeEntrance(group: THREE.Group, southZ: number, centerX: number) {
788
+ const panes: THREE.BufferGeometry[] = [];
789
+ const frames: THREE.BufferGeometry[] = [];
790
+ const handles: THREE.BufferGeometry[] = [];
791
+ const doorWidth = 7.2;
792
+ const doorHeight = 6.4;
793
+ const doorY = doorHeight / 2;
794
+ pushBoxGeometry(panes, doorWidth / 2 - 0.22, doorHeight, 0.05, centerX - doorWidth / 4, doorY, southZ);
795
+ pushBoxGeometry(panes, doorWidth / 2 - 0.22, doorHeight, 0.05, centerX + doorWidth / 4, doorY, southZ);
796
+ pushBoxGeometry(frames, GLASS_FACADE_FRAME_THICKNESS * 1.35, doorHeight + 0.45, 0.1, centerX - doorWidth / 2, doorY, southZ + 0.02);
797
+ pushBoxGeometry(frames, GLASS_FACADE_FRAME_THICKNESS * 1.35, doorHeight + 0.45, 0.1, centerX + doorWidth / 2, doorY, southZ + 0.02);
798
+ pushBoxGeometry(frames, doorWidth + 0.45, GLASS_FACADE_FRAME_THICKNESS * 1.35, 0.1, centerX, doorHeight + 0.12, southZ + 0.02);
799
+ pushBoxGeometry(frames, doorWidth + 1.4, 0.28, 0.9, centerX, doorHeight + 0.55, southZ + 0.35);
800
+ pushBoxGeometry(frames, GLASS_FACADE_FRAME_THICKNESS, doorHeight, 0.1, centerX, doorY, southZ + 0.03);
801
+ pushBoxGeometry(handles, 0.12, 1.2, 0.12, centerX - 0.38, 2.65, southZ + 0.12);
802
+ pushBoxGeometry(handles, 0.12, 1.2, 0.12, centerX + 0.38, 2.65, southZ + 0.12);
803
+ addMergedMesh(group, panes, new THREE.MeshLambertMaterial({ color: 0xa5f3fc, transparent: true, opacity: 0.26, depthWrite: false }), "officeGlassFacadeEntrance:panes");
804
+ addMergedMesh(group, frames, new THREE.MeshLambertMaterial({ color: 0x1f2937 }), "officeGlassFacadeEntrance:frame");
805
+ addMergedMesh(group, handles, new THREE.MeshLambertMaterial({ color: 0xe5e7eb }), "officeGlassFacadeEntrance:handles");
806
+ const marker = new THREE.Object3D();
807
+ marker.name = "officeGlassFacadeEntrance";
808
+ marker.position.set(centerX, doorY, southZ);
809
+ group.add(marker);
810
+ }
811
+
812
+ function pushBoxGeometry(
813
+ geometries: THREE.BufferGeometry[],
814
+ width: number,
815
+ height: number,
816
+ depth: number,
817
+ x: number,
818
+ y: number,
819
+ z: number,
820
+ ) {
821
+ const geometry = new THREE.BoxGeometry(width, height, depth);
822
+ matrixHelper.position.set(x, y, z);
823
+ matrixHelper.rotation.set(0, 0, 0);
824
+ matrixHelper.scale.set(1, 1, 1);
825
+ matrixHelper.updateMatrix();
826
+ geometry.applyMatrix4(matrixHelper.matrix);
827
+ geometries.push(geometry);
828
+ }
829
+
830
+ function addMergedMesh(group: THREE.Group, geometries: THREE.BufferGeometry[], material: THREE.Material, name: string) {
831
+ const geometry = mergeGeometries(geometries, false);
832
+ geometries.forEach((item) => item.dispose());
833
+ if (!geometry) {
834
+ material.dispose();
835
+ return;
836
+ }
837
+ const mesh = new THREE.Mesh(geometry, material);
838
+ mesh.name = name;
839
+ mesh.castShadow = false;
840
+ mesh.receiveShadow = false;
841
+ group.add(mesh);
842
+ }
843
+
844
+ function buildBuildingConnectors(scene: THREE.Scene, layout: ResolvedOfficeLayout) {
845
+ layout.building.connectors.forEach((connector) => {
846
+ if (connector.stops.length < 2) {
847
+ return;
848
+ }
849
+ const firstStop = connector.stops[0]!;
850
+ const minY = Math.min(...connector.stops.map((stop) => stop.cabinPosition.y));
851
+ const maxY = Math.max(...connector.stops.map((stop) => stop.cabinPosition.y));
852
+ const height = Math.max(0.1, maxY - minY + 2.8);
853
+ addTransparentBox(
854
+ scene,
855
+ 0x94a3b8,
856
+ 2.4,
857
+ height,
858
+ 1.25,
859
+ firstStop.cabinPosition.x,
860
+ minY + height / 2 - 1.4,
861
+ firstStop.cabinPosition.z,
862
+ 0.18,
863
+ `elevatorShaft:${connector.id}`,
864
+ );
865
+ });
866
+ }
867
+
359
868
  function addOfficeWallBox(
360
869
  scene: THREE.Scene,
361
870
  width: number,
@@ -510,6 +1019,36 @@ function renderOfficeComponent(scene: THREE.Scene, component: ResolvedOfficeComp
510
1019
  component.props?.depth,
511
1020
  );
512
1021
  return;
1022
+ case "diningTable":
1023
+ addDiningTable(scene, x, z);
1024
+ return;
1025
+ case "cafeCounter":
1026
+ addCafeCounter(scene, x, z);
1027
+ return;
1028
+ case "cafeTable":
1029
+ addCafeTable(scene, x, z);
1030
+ return;
1031
+ case "kitchenCounter":
1032
+ addKitchenCounter(scene, x, z);
1033
+ return;
1034
+ case "kitchenStove":
1035
+ addKitchenStove(scene, x, z);
1036
+ return;
1037
+ case "kitchenFridge":
1038
+ addKitchenFridge(scene, x, z);
1039
+ return;
1040
+ case "gardenPlanter":
1041
+ addGardenPlanter(scene, x, z, rotation);
1042
+ return;
1043
+ case "gardenTree":
1044
+ addGardenTree(scene, x, z);
1045
+ return;
1046
+ case "gardenBench":
1047
+ addGardenBench(scene, x, z, rotation);
1048
+ return;
1049
+ case "gardenPath":
1050
+ addGardenPath(scene, x, z, component.props?.width, component.props?.depth, rotation);
1051
+ return;
513
1052
  case "meetingGlassRoom":
514
1053
  addMeetingGlassRoom(
515
1054
  scene,
@@ -581,6 +1120,15 @@ function renderOfficeComponent(scene: THREE.Scene, component: ResolvedOfficeComp
581
1120
  case "gymMirror":
582
1121
  addGymMirror(scene, x, z, rotation, component.props?.width, component.props?.faceDirection ?? 1);
583
1122
  return;
1123
+ case "elevatorDoor":
1124
+ addElevatorDoor(scene, x, z, component.props?.width, component.props?.height);
1125
+ return;
1126
+ case "elevatorFrame":
1127
+ addElevatorFrame(scene, x, z, component.props?.width, component.props?.depth, component.props?.height);
1128
+ return;
1129
+ case "elevatorIndicator":
1130
+ addElevatorIndicator(scene, x, component.position.y ?? 2.6, z, component.props?.width, component.props?.height);
1131
+ return;
584
1132
  case "glassWall":
585
1133
  case "glassDoor":
586
1134
  return;
@@ -643,6 +1191,76 @@ function addMeetingTable(
643
1191
  addBox(scene, legColor, 0.2, 1.5, 0.2, x + 2.2, 0.75, z + legZ);
644
1192
  }
645
1193
 
1194
+ function addDiningTable(scene: THREE.Scene, x: number, z: number) {
1195
+ addBox(scene, 0xc98b32, 2.6, 0.14, 1.8, x, 1.05, z);
1196
+ addBox(scene, 0x7c3f12, 0.14, 1.05, 0.14, x - 1.08, 0.52, z - 0.68);
1197
+ addBox(scene, 0x7c3f12, 0.14, 1.05, 0.14, x + 1.08, 0.52, z - 0.68);
1198
+ addBox(scene, 0x7c3f12, 0.14, 1.05, 0.14, x - 1.08, 0.52, z + 0.68);
1199
+ addBox(scene, 0x7c3f12, 0.14, 1.05, 0.14, x + 1.08, 0.52, z + 0.68);
1200
+ }
1201
+
1202
+ function addCafeCounter(scene: THREE.Scene, x: number, z: number) {
1203
+ addBox(scene, 0x7c2d12, 4.2, 0.78, 0.72, x, 0.39, z);
1204
+ addBox(scene, 0xd6c4a8, 4.38, 0.12, 0.9, x, 0.84, z);
1205
+ addBox(scene, 0x1f2937, 0.72, 0.42, 0.34, x - 1.15, 1.12, z - 0.18);
1206
+ addBox(scene, 0x94a3b8, 0.62, 0.06, 0.26, x - 1.15, 1.36, z - 0.34);
1207
+ addBox(scene, 0x334155, 0.34, 0.36, 0.28, x - 0.38, 1.05, z + 0.08);
1208
+ addBox(scene, 0xf8fafc, 0.18, 0.2, 0.18, x + 0.45, 1.03, z - 0.12);
1209
+ addBox(scene, 0xf8fafc, 0.18, 0.2, 0.18, x + 0.78, 1.03, z - 0.12);
1210
+ addBox(scene, 0xf59e0b, 0.5, 0.18, 0.36, x + 1.35, 1.02, z + 0.1);
1211
+ }
1212
+
1213
+ function addCafeTable(scene: THREE.Scene, x: number, z: number) {
1214
+ addBox(scene, 0xb7791f, 1.65, 0.12, 1.65, x, 0.9, z);
1215
+ addBox(scene, 0x7c3f12, 0.18, 0.9, 0.18, x, 0.45, z);
1216
+ addBox(scene, 0x7c3f12, 0.9, 0.08, 0.9, x, 0.08, z);
1217
+ }
1218
+
1219
+ function addKitchenCounter(scene: THREE.Scene, x: number, z: number) {
1220
+ addBox(scene, 0x92400e, 4.8, 0.8, 0.72, x, 0.4, z);
1221
+ addBox(scene, 0xe5e7eb, 4.95, 0.12, 0.9, x, 0.86, z);
1222
+ addBox(scene, 0x64748b, 0.84, 0.08, 0.5, x - 1.35, 0.95, z - 0.02);
1223
+ addBox(scene, 0x94a3b8, 0.52, 0.05, 0.32, x + 1.35, 0.96, z - 0.04);
1224
+ }
1225
+
1226
+ function addKitchenStove(scene: THREE.Scene, x: number, z: number) {
1227
+ addBox(scene, 0x334155, 1.5, 0.82, 0.82, x, 0.42, z);
1228
+ addBox(scene, 0x111827, 1.2, 0.08, 0.62, x, 0.88, z);
1229
+ addBox(scene, 0xf97316, 0.22, 0.04, 0.22, x - 0.32, 0.94, z - 0.14);
1230
+ addBox(scene, 0xf97316, 0.22, 0.04, 0.22, x + 0.32, 0.94, z + 0.14);
1231
+ }
1232
+
1233
+ function addKitchenFridge(scene: THREE.Scene, x: number, z: number) {
1234
+ addBox(scene, 0xe2e8f0, 1.1, 2.0, 0.78, x, 1.0, z);
1235
+ addBox(scene, 0xcbd5e1, 1.02, 0.05, 0.8, x, 1.42, z + 0.02);
1236
+ addBox(scene, 0x64748b, 0.06, 0.58, 0.06, x + 0.42, 1.1, z + 0.42);
1237
+ }
1238
+
1239
+ function addGardenPath(scene: THREE.Scene, x: number, z: number, width = 4, depth = 1, rotation = 0) {
1240
+ addLocalBox(scene, 0xb9b39f, width, 0.05, depth, x, z, 0, 0.05, 0, rotation);
1241
+ }
1242
+
1243
+ function addGardenPlanter(scene: THREE.Scene, x: number, z: number, rotation = 0) {
1244
+ addLocalBox(scene, 0x8b5e3c, 4.4, 0.5, 1.15, x, z, 0, 0.25, 0, rotation);
1245
+ addLocalBox(scene, 0x4d7c3f, 4.05, 0.18, 0.84, x, z, 0, 0.58, 0, rotation);
1246
+ for (const offset of [-1.35, -0.45, 0.45, 1.35]) {
1247
+ addLocalBox(scene, 0x2f6b3f, 0.52, 0.46, 0.52, x, z, offset, 0.84, 0, rotation + Math.PI / 4);
1248
+ }
1249
+ }
1250
+
1251
+ function addGardenTree(scene: THREE.Scene, x: number, z: number) {
1252
+ addBox(scene, 0x7c4a28, 0.34, 1.15, 0.34, x, 0.58, z);
1253
+ addBox(scene, 0x2f6b3f, 1.45, 1.15, 1.45, x, 1.35, z, Math.PI / 4);
1254
+ addBox(scene, 0x3f8550, 1.05, 0.88, 1.05, x, 2.02, z, Math.PI / 4);
1255
+ }
1256
+
1257
+ function addGardenBench(scene: THREE.Scene, x: number, z: number, rotation = 0) {
1258
+ addLocalBox(scene, 0x8b5e3c, 3.1, 0.2, 0.58, x, z, 0, 0.58, 0, rotation);
1259
+ addLocalBox(scene, 0x6b4428, 3.1, 0.72, 0.18, x, z, 0, 0.9, 0.28, rotation);
1260
+ addLocalBox(scene, 0x475569, 0.12, 0.55, 0.12, x, z, -1.25, 0.28, -0.18, rotation);
1261
+ addLocalBox(scene, 0x475569, 0.12, 0.55, 0.12, x, z, 1.25, 0.28, -0.18, rotation);
1262
+ }
1263
+
646
1264
  function addMeetingGlassRoom(
647
1265
  scene: THREE.Scene,
648
1266
  roomIndex: number,
@@ -876,6 +1494,22 @@ function addGymMirror(scene: THREE.Scene, x: number, z: number, rotation = 0, wi
876
1494
  addLocalBox(scene, 0xe0f2fe, 0.08, 1.42, 0.05, x, z, width * 0.18, 1.45, 0.14 * faceDirection, rotation);
877
1495
  }
878
1496
 
1497
+ function addElevatorDoor(scene: THREE.Scene, x: number, z: number, width = 1.5, height = 2.4) {
1498
+ addBox(scene, 0x475569, width, height, 0.08, x, height / 2, z);
1499
+ addBox(scene, 0xcbd5e1, 0.04, height * 0.84, 0.1, x, height / 2, z + 0.05);
1500
+ }
1501
+
1502
+ function addElevatorFrame(scene: THREE.Scene, x: number, z: number, width = 2.2, depth = 1.1, height = 2.8) {
1503
+ addBox(scene, 0x334155, width, 0.16, depth, x, height, z);
1504
+ addBox(scene, 0x334155, 0.16, height, depth, x - width / 2, height / 2, z);
1505
+ addBox(scene, 0x334155, 0.16, height, depth, x + width / 2, height / 2, z);
1506
+ }
1507
+
1508
+ function addElevatorIndicator(scene: THREE.Scene, x: number, y: number, z: number, width = 0.45, height = 0.3) {
1509
+ addBox(scene, 0x0f172a, width, height, 0.08, x, y, z + 0.08);
1510
+ addBox(scene, 0x38bdf8, width * 0.62, height * 0.46, 0.09, x, y, z + 0.13);
1511
+ }
1512
+
879
1513
  function addBox(
880
1514
  scene: THREE.Scene,
881
1515
  color: number,