opencroc 1.6.8 → 1.7.0

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.
@@ -0,0 +1,816 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════════
2
+ OpenCroc Studio 3D — Office Environment
3
+ Low-poly 3D office built from primitives
4
+ ~3000 lines
5
+ ═══════════════════════════════════════════════════════════════════════════════ */
6
+
7
+ import * as THREE from 'three';
8
+ import { getScene } from './engine.js';
9
+
10
+ let officeGroup = null;
11
+ let currentTheme = 'dark';
12
+
13
+ /* ─── Material Cache ───────────────────────────────────────────────────────── */
14
+ const MAT = {};
15
+
16
+ function initMaterials(theme) {
17
+ const dk = theme === 'dark';
18
+ MAT.floor = new THREE.MeshStandardMaterial({ color: dk ? 0x1a2332 : 0xe2e8f0, roughness: 0.8, metalness: 0.05 });
19
+ MAT.wall = new THREE.MeshStandardMaterial({ color: dk ? 0x1e293b : 0xf8fafc, roughness: 0.7, metalness: 0.0, transparent: true, opacity: 0.85 });
20
+ MAT.wallGlass = new THREE.MeshPhysicalMaterial({ color: dk ? 0x1e3a5f : 0xbfdbfe, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.25, transmission: 0.6, thickness: 0.5, ior: 1.5 });
21
+ MAT.desk = new THREE.MeshStandardMaterial({ color: dk ? 0x2d3748 : 0xcbd5e1, roughness: 0.5, metalness: 0.2 });
22
+ MAT.deskTop = new THREE.MeshStandardMaterial({ color: dk ? 0x374151 : 0xdde4ed, roughness: 0.4, metalness: 0.15 });
23
+ MAT.chair = new THREE.MeshStandardMaterial({ color: dk ? 0x4a5568 : 0x94a3b8, roughness: 0.6, metalness: 0.1 });
24
+ MAT.screen = new THREE.MeshStandardMaterial({ color: dk ? 0x000000 : 0x111111, roughness: 0.1, metalness: 0.8 });
25
+ MAT.screenGlow= new THREE.MeshBasicMaterial({ color: dk ? 0x34d399 : 0x059669, transparent: true, opacity: 0.6 });
26
+ MAT.metal = new THREE.MeshStandardMaterial({ color: dk ? 0x64748b : 0x94a3b8, roughness: 0.3, metalness: 0.7 });
27
+ MAT.accent = new THREE.MeshStandardMaterial({ color: dk ? 0x34d399 : 0x059669, roughness: 0.4, metalness: 0.3, emissive: dk ? 0x34d399 : 0x059669, emissiveIntensity: dk ? 0.3 : 0.1 });
28
+ MAT.neon = new THREE.MeshBasicMaterial({ color: dk ? 0x34d399 : 0x059669, transparent: true, opacity: dk ? 0.8 : 0.4 });
29
+ MAT.neonBlue = new THREE.MeshBasicMaterial({ color: dk ? 0x60a5fa : 0x2563eb, transparent: true, opacity: dk ? 0.6 : 0.3 });
30
+ MAT.neonPurple= new THREE.MeshBasicMaterial({ color: dk ? 0xa78bfa : 0x7c3aed, transparent: true, opacity: dk ? 0.5 : 0.25 });
31
+ MAT.server = new THREE.MeshStandardMaterial({ color: dk ? 0x1a2332 : 0xcbd5e1, roughness: 0.3, metalness: 0.6 });
32
+ MAT.coffee = new THREE.MeshStandardMaterial({ color: dk ? 0x78350f : 0xa16207, roughness: 0.5, metalness: 0.1 });
33
+ MAT.plant = new THREE.MeshStandardMaterial({ color: dk ? 0x166534 : 0x22c55e, roughness: 0.8, metalness: 0.0 });
34
+ MAT.pot = new THREE.MeshStandardMaterial({ color: dk ? 0x92400e : 0xfbbf24, roughness: 0.6, metalness: 0.0 });
35
+ MAT.whiteboard= new THREE.MeshStandardMaterial({ color: dk ? 0xf8fafc : 0xffffff, roughness: 0.2, metalness: 0.05 });
36
+ MAT.frame = new THREE.MeshStandardMaterial({ color: dk ? 0x374151 : 0x64748b, roughness: 0.4, metalness: 0.5 });
37
+ MAT.carpet = new THREE.MeshStandardMaterial({ color: dk ? 0x1e1b4b : 0xc7d2fe, roughness: 0.95, metalness: 0.0 });
38
+ MAT.pillar = new THREE.MeshStandardMaterial({ color: dk ? 0x334155 : 0xe2e8f0, roughness: 0.5, metalness: 0.3 });
39
+ }
40
+
41
+ /* ═══════════════════════════════════════════════════════════════════════════════
42
+ createOffice — Build the full 3D office environment
43
+ ═══════════════════════════════════════════════════════════════════════════════ */
44
+ export async function createOffice(theme) {
45
+ currentTheme = theme;
46
+ initMaterials(theme);
47
+
48
+ const scene = getScene();
49
+ officeGroup = new THREE.Group();
50
+ officeGroup.name = 'office';
51
+
52
+ buildFloor();
53
+ buildWalls();
54
+ buildGlassPartitions();
55
+ buildDesks(6);
56
+ buildServerRack();
57
+ buildCoffeeMachine();
58
+ buildPlants();
59
+ buildWhiteboard();
60
+ buildPillars();
61
+ buildCarpet();
62
+ buildCeilingLights();
63
+ buildNeonStrips();
64
+ buildCenterPlatform();
65
+ buildBookshelf();
66
+ buildWaterCooler();
67
+ buildDecorativeElements();
68
+
69
+ scene.add(officeGroup);
70
+ }
71
+
72
+ /* ═══════════════════════════════════════════════════════════════════════════════
73
+ Office Floor
74
+ ═══════════════════════════════════════════════════════════════════════════════ */
75
+ function buildFloor() {
76
+ const geo = new THREE.BoxGeometry(28, 0.2, 20);
77
+ const floor = new THREE.Mesh(geo, MAT.floor);
78
+ floor.position.set(0, 0.1, 0);
79
+ floor.receiveShadow = true;
80
+ floor.name = 'office-floor';
81
+ officeGroup.add(floor);
82
+
83
+ // Floor edge trim (accent line)
84
+ const edgeGeo = new THREE.BoxGeometry(28.2, 0.05, 0.05);
85
+ const edgeFront = new THREE.Mesh(edgeGeo, MAT.neon);
86
+ edgeFront.position.set(0, 0.22, 10);
87
+ officeGroup.add(edgeFront);
88
+ const edgeBack = edgeFront.clone();
89
+ edgeBack.position.z = -10;
90
+ officeGroup.add(edgeBack);
91
+
92
+ const edgeSideGeo = new THREE.BoxGeometry(0.05, 0.05, 20.2);
93
+ const edgeLeft = new THREE.Mesh(edgeSideGeo, MAT.neon);
94
+ edgeLeft.position.set(-14, 0.22, 0);
95
+ officeGroup.add(edgeLeft);
96
+ const edgeRight = edgeLeft.clone();
97
+ edgeRight.position.x = 14;
98
+ officeGroup.add(edgeRight);
99
+ }
100
+
101
+ /* ═══════════════════════════════════════════════════════════════════════════════
102
+ Walls
103
+ ═══════════════════════════════════════════════════════════════════════════════ */
104
+ function buildWalls() {
105
+ // Back wall
106
+ const backGeo = new THREE.BoxGeometry(28, 6, 0.15);
107
+ const backWall = new THREE.Mesh(backGeo, MAT.wall);
108
+ backWall.position.set(0, 3.2, -10);
109
+ backWall.castShadow = true;
110
+ backWall.receiveShadow = true;
111
+ officeGroup.add(backWall);
112
+
113
+ // Left wall
114
+ const sideGeo = new THREE.BoxGeometry(0.15, 6, 20);
115
+ const leftWall = new THREE.Mesh(sideGeo, MAT.wall);
116
+ leftWall.position.set(-14, 3.2, 0);
117
+ leftWall.castShadow = true;
118
+ leftWall.receiveShadow = true;
119
+ officeGroup.add(leftWall);
120
+
121
+ // Right wall (half + glass)
122
+ const rightLower = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2, 20), MAT.wall);
123
+ rightLower.position.set(14, 1.2, 0);
124
+ rightLower.castShadow = true;
125
+ officeGroup.add(rightLower);
126
+
127
+ // Wall accent strips
128
+ const stripGeo = new THREE.BoxGeometry(28, 0.04, 0.04);
129
+ const strip1 = new THREE.Mesh(stripGeo, MAT.neon);
130
+ strip1.position.set(0, 5, -9.9);
131
+ officeGroup.add(strip1);
132
+ const strip2 = new THREE.Mesh(new THREE.BoxGeometry(0.04, 6, 0.04), MAT.neonBlue);
133
+ strip2.position.set(-13.9, 3.2, 9.5);
134
+ officeGroup.add(strip2);
135
+ }
136
+
137
+ /* ═══════════════════════════════════════════════════════════════════════════════
138
+ Glass Partitions
139
+ ═══════════════════════════════════════════════════════════════════════════════ */
140
+ function buildGlassPartitions() {
141
+ // Partition between server area and desks
142
+ const partGeo = new THREE.BoxGeometry(0.08, 4, 8);
143
+ const part1 = new THREE.Mesh(partGeo, MAT.wallGlass);
144
+ part1.position.set(-5, 2.2, -3);
145
+ officeGroup.add(part1);
146
+
147
+ // Partition near center
148
+ const part2Geo = new THREE.BoxGeometry(8, 3.5, 0.08);
149
+ const part2 = new THREE.Mesh(part2Geo, MAT.wallGlass);
150
+ part2.position.set(3, 1.95, 3);
151
+ officeGroup.add(part2);
152
+
153
+ // Glass frame accents
154
+ const framGeo = new THREE.BoxGeometry(0.06, 4, 0.06);
155
+ const frame1 = new THREE.Mesh(framGeo, MAT.frame);
156
+ frame1.position.set(-5, 2.2, 1);
157
+ officeGroup.add(frame1);
158
+ const frame2 = frame1.clone();
159
+ frame2.position.z = -7;
160
+ officeGroup.add(frame2);
161
+ }
162
+
163
+ /* ═══════════════════════════════════════════════════════════════════════════════
164
+ Agent Desks — Each agent gets a desk with monitor and chair
165
+ ═══════════════════════════════════════════════════════════════════════════════ */
166
+ export const DESK_POSITIONS = [];
167
+
168
+ function buildDesks(count) {
169
+ const rows = 2;
170
+ const cols = Math.ceil(count / rows);
171
+ const xStart = -2;
172
+ const zStart = -6;
173
+ const xSpacing = 4.5;
174
+ const zSpacing = 5;
175
+
176
+ for (let i = 0; i < count; i++) {
177
+ const row = Math.floor(i / cols);
178
+ const col = i % cols;
179
+ const x = xStart + col * xSpacing;
180
+ const z = zStart + row * zSpacing;
181
+
182
+ DESK_POSITIONS.push({ x, z });
183
+ buildSingleDesk(x, z, i);
184
+ }
185
+ }
186
+
187
+ function buildSingleDesk(x, z, idx) {
188
+ const desk = new THREE.Group();
189
+ desk.name = `desk-${idx}`;
190
+
191
+ // Desktop surface
192
+ const topGeo = new THREE.BoxGeometry(2.4, 0.08, 1.2);
193
+ const top = new THREE.Mesh(topGeo, MAT.deskTop);
194
+ top.position.set(0, 1.0, 0);
195
+ top.castShadow = true;
196
+ top.receiveShadow = true;
197
+ desk.add(top);
198
+
199
+ // Legs (4)
200
+ const legGeo = new THREE.BoxGeometry(0.08, 0.8, 0.08);
201
+ const positions = [
202
+ [-1.1, 0.6, -0.5], [1.1, 0.6, -0.5],
203
+ [-1.1, 0.6, 0.5], [1.1, 0.6, 0.5],
204
+ ];
205
+ positions.forEach(p => {
206
+ const leg = new THREE.Mesh(legGeo, MAT.desk);
207
+ leg.position.set(...p);
208
+ leg.castShadow = true;
209
+ desk.add(leg);
210
+ });
211
+
212
+ // Monitor
213
+ const monitorGroup = new THREE.Group();
214
+ // Screen
215
+ const scrGeo = new THREE.BoxGeometry(1.0, 0.7, 0.04);
216
+ const scr = new THREE.Mesh(scrGeo, MAT.screen);
217
+ scr.position.set(0, 1.65, -0.3);
218
+ scr.castShadow = true;
219
+ monitorGroup.add(scr);
220
+
221
+ // Screen glow face
222
+ const glowGeo = new THREE.PlaneGeometry(0.92, 0.62);
223
+ const glow = new THREE.Mesh(glowGeo, MAT.screenGlow);
224
+ glow.position.set(0, 1.65, -0.278);
225
+ monitorGroup.add(glow);
226
+
227
+ // Monitor stand
228
+ const standGeo = new THREE.BoxGeometry(0.06, 0.28, 0.06);
229
+ const stand = new THREE.Mesh(standGeo, MAT.metal);
230
+ stand.position.set(0, 1.18, -0.3);
231
+ monitorGroup.add(stand);
232
+
233
+ // Monitor base
234
+ const baseGeo = new THREE.CylinderGeometry(0.18, 0.2, 0.04, 16);
235
+ const base = new THREE.Mesh(baseGeo, MAT.metal);
236
+ base.position.set(0, 1.04, -0.3);
237
+ monitorGroup.add(base);
238
+
239
+ desk.add(monitorGroup);
240
+
241
+ // Keyboard
242
+ const kbGeo = new THREE.BoxGeometry(0.6, 0.02, 0.2);
243
+ const kb = new THREE.Mesh(kbGeo, MAT.desk);
244
+ kb.position.set(0, 1.06, 0.15);
245
+ desk.add(kb);
246
+
247
+ // Mouse
248
+ const mouseGeo = new THREE.BoxGeometry(0.1, 0.02, 0.14);
249
+ const mouse = new THREE.Mesh(mouseGeo, MAT.desk);
250
+ mouse.position.set(0.5, 1.06, 0.15);
251
+ desk.add(mouse);
252
+
253
+ // Chair
254
+ const chairGroup = new THREE.Group();
255
+ // Seat
256
+ const seatGeo = new THREE.BoxGeometry(0.7, 0.08, 0.7);
257
+ const seat = new THREE.Mesh(seatGeo, MAT.chair);
258
+ seat.position.set(0, 0.7, 0.9);
259
+ seat.castShadow = true;
260
+ chairGroup.add(seat);
261
+ // Back rest
262
+ const backGeo = new THREE.BoxGeometry(0.7, 0.6, 0.06);
263
+ const back = new THREE.Mesh(backGeo, MAT.chair);
264
+ back.position.set(0, 1.04, 1.23);
265
+ back.castShadow = true;
266
+ chairGroup.add(back);
267
+ // Chair base
268
+ const cbaseGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.5, 8);
269
+ const cbase = new THREE.Mesh(cbaseGeo, MAT.metal);
270
+ cbase.position.set(0, 0.45, 0.9);
271
+ chairGroup.add(cbase);
272
+ // Chair wheels (5-star base)
273
+ const wheelGeo = new THREE.CylinderGeometry(0.25, 0.28, 0.03, 5);
274
+ const wheel = new THREE.Mesh(wheelGeo, MAT.metal);
275
+ wheel.position.set(0, 0.2, 0.9);
276
+ chairGroup.add(wheel);
277
+
278
+ desk.add(chairGroup);
279
+
280
+ // Desk lamp
281
+ if (idx % 2 === 0) {
282
+ const lampGroup = new THREE.Group();
283
+ const lampBase = new THREE.Mesh(
284
+ new THREE.CylinderGeometry(0.1, 0.12, 0.04, 16), MAT.metal);
285
+ lampBase.position.set(-0.8, 1.06, -0.2);
286
+ lampGroup.add(lampBase);
287
+ const lampArm = new THREE.Mesh(
288
+ new THREE.CylinderGeometry(0.015, 0.015, 0.5, 8), MAT.metal);
289
+ lampArm.position.set(-0.8, 1.31, -0.2);
290
+ lampGroup.add(lampArm);
291
+ const lampHead = new THREE.Mesh(
292
+ new THREE.ConeGeometry(0.1, 0.12, 8), MAT.accent);
293
+ lampHead.position.set(-0.8, 1.58, -0.2);
294
+ lampHead.rotation.x = Math.PI;
295
+ lampGroup.add(lampHead);
296
+ desk.add(lampGroup);
297
+ }
298
+
299
+ // Coffee mug (alternating desks)
300
+ if (idx % 3 === 1) {
301
+ const mugGeo = new THREE.CylinderGeometry(0.05, 0.04, 0.1, 12);
302
+ const mug = new THREE.Mesh(mugGeo, MAT.coffee);
303
+ mug.position.set(0.8, 1.09, 0.0);
304
+ desk.add(mug);
305
+ }
306
+
307
+ desk.position.set(x, 0.2, z);
308
+ officeGroup.add(desk);
309
+ }
310
+
311
+ /* ═══════════════════════════════════════════════════════════════════════════════
312
+ Server Rack
313
+ ═══════════════════════════════════════════════════════════════════════════════ */
314
+ function buildServerRack() {
315
+ const rack = new THREE.Group();
316
+ rack.name = 'server-rack';
317
+
318
+ // Main cabinet
319
+ const cabinetGeo = new THREE.BoxGeometry(1.0, 3.5, 0.8);
320
+ const cabinet = new THREE.Mesh(cabinetGeo, MAT.server);
321
+ cabinet.position.set(0, 1.95, 0);
322
+ cabinet.castShadow = true;
323
+ rack.add(cabinet);
324
+
325
+ // Server units (stacked)
326
+ for (let i = 0; i < 6; i++) {
327
+ const unitGeo = new THREE.BoxGeometry(0.9, 0.35, 0.7);
328
+ const unit = new THREE.Mesh(unitGeo, MAT.desk);
329
+ unit.position.set(0, 0.5 + i * 0.5, 0);
330
+ rack.add(unit);
331
+
332
+ // LED lights on each server unit
333
+ const ledColors = [MAT.neon, MAT.neonBlue, MAT.neon, MAT.neonBlue, MAT.neon, MAT.neonPurple];
334
+ for (let j = 0; j < 3; j++) {
335
+ const ledGeo = new THREE.BoxGeometry(0.03, 0.03, 0.01);
336
+ const led = new THREE.Mesh(ledGeo, ledColors[i]);
337
+ led.position.set(-0.3 + j * 0.15, 0.5 + i * 0.5, 0.36);
338
+ rack.add(led);
339
+ }
340
+ }
341
+
342
+ // Status light on top
343
+ const statusLight = new THREE.Mesh(
344
+ new THREE.SphereGeometry(0.06, 8, 8), MAT.accent);
345
+ statusLight.position.set(0, 3.8, 0);
346
+ rack.add(statusLight);
347
+
348
+ rack.position.set(-10, 0.2, -6);
349
+ officeGroup.add(rack);
350
+
351
+ // Second rack
352
+ const rack2 = rack.clone();
353
+ rack2.position.set(-10, 0.2, -3);
354
+ rack2.name = 'server-rack-2';
355
+ officeGroup.add(rack2);
356
+ }
357
+
358
+ /* ═══════════════════════════════════════════════════════════════════════════════
359
+ Coffee Machine Area
360
+ ═══════════════════════════════════════════════════════════════════════════════ */
361
+ function buildCoffeeMachine() {
362
+ const area = new THREE.Group();
363
+ area.name = 'coffee-area';
364
+
365
+ // Counter
366
+ const counterGeo = new THREE.BoxGeometry(2.5, 1.0, 0.8);
367
+ const counter = new THREE.Mesh(counterGeo, MAT.deskTop);
368
+ counter.position.set(0, 0.7, 0);
369
+ counter.castShadow = true;
370
+ counter.receiveShadow = true;
371
+ area.add(counter);
372
+
373
+ // Machine body
374
+ const machineGeo = new THREE.BoxGeometry(0.5, 0.6, 0.4);
375
+ const machine = new THREE.Mesh(machineGeo, MAT.server);
376
+ machine.position.set(-0.4, 1.5, 0);
377
+ machine.castShadow = true;
378
+ area.add(machine);
379
+
380
+ // Machine spout
381
+ const spoutGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.15, 8);
382
+ const spout = new THREE.Mesh(spoutGeo, MAT.metal);
383
+ spout.position.set(-0.4, 1.13, 0.15);
384
+ area.add(spout);
385
+
386
+ // Cups
387
+ for (let i = 0; i < 3; i++) {
388
+ const cupGeo = new THREE.CylinderGeometry(0.04, 0.035, 0.08, 8);
389
+ const cup = new THREE.Mesh(cupGeo, MAT.coffee);
390
+ cup.position.set(0.3 + i * 0.15, 1.24, 0);
391
+ area.add(cup);
392
+ }
393
+
394
+ // Green indicator LED
395
+ const led = new THREE.Mesh(
396
+ new THREE.SphereGeometry(0.025, 8, 8), MAT.accent);
397
+ led.position.set(-0.15, 1.7, 0.21);
398
+ area.add(led);
399
+
400
+ area.position.set(11, 0.2, -7);
401
+ officeGroup.add(area);
402
+ }
403
+
404
+ /* ═══════════════════════════════════════════════════════════════════════════════
405
+ Plants
406
+ ═══════════════════════════════════════════════════════════════════════════════ */
407
+ function buildPlants() {
408
+ const positions = [
409
+ [-12, 0.2, 8], [12, 0.2, 8], [-12, 0.2, -8], [8, 0.2, -8],
410
+ [0, 0.2, 9], [-7, 0.2, 9],
411
+ ];
412
+
413
+ positions.forEach((pos, i) => {
414
+ const plant = new THREE.Group();
415
+ plant.name = `plant-${i}`;
416
+
417
+ // Pot
418
+ const potGeo = new THREE.CylinderGeometry(0.25, 0.2, 0.35, 8);
419
+ const pot = new THREE.Mesh(potGeo, MAT.pot);
420
+ pot.position.set(0, 0.175, 0);
421
+ pot.castShadow = true;
422
+ plant.add(pot);
423
+
424
+ // Leaves (spheres at different angles)
425
+ const leafMat = MAT.plant;
426
+ for (let j = 0; j < 5; j++) {
427
+ const leafGeo = new THREE.SphereGeometry(0.15 + Math.random() * 0.1, 6, 6);
428
+ const leaf = new THREE.Mesh(leafGeo, leafMat);
429
+ const angle = (j / 5) * Math.PI * 2;
430
+ leaf.position.set(
431
+ Math.cos(angle) * 0.15,
432
+ 0.5 + Math.random() * 0.3,
433
+ Math.sin(angle) * 0.15
434
+ );
435
+ leaf.scale.y = 1.2 + Math.random() * 0.3;
436
+ plant.add(leaf);
437
+ }
438
+
439
+ // Center trunk
440
+ const trunkGeo = new THREE.CylinderGeometry(0.03, 0.04, 0.4, 6);
441
+ const trunk = new THREE.Mesh(trunkGeo, MAT.coffee);
442
+ trunk.position.set(0, 0.5, 0);
443
+ plant.add(trunk);
444
+
445
+ plant.position.set(...pos);
446
+ officeGroup.add(plant);
447
+ });
448
+ }
449
+
450
+ /* ═══════════════════════════════════════════════════════════════════════════════
451
+ Whiteboard
452
+ ═══════════════════════════════════════════════════════════════════════════════ */
453
+ function buildWhiteboard() {
454
+ const wb = new THREE.Group();
455
+ wb.name = 'whiteboard';
456
+
457
+ // Board
458
+ const boardGeo = new THREE.BoxGeometry(3, 2, 0.06);
459
+ const board = new THREE.Mesh(boardGeo, MAT.whiteboard);
460
+ board.position.set(0, 3.2, -9.9);
461
+ board.castShadow = true;
462
+ wb.add(board);
463
+
464
+ // Frame
465
+ const frameGeo = new THREE.BoxGeometry(3.1, 2.1, 0.04);
466
+ const frame = new THREE.Mesh(frameGeo, MAT.frame);
467
+ frame.position.set(0, 3.2, -9.95);
468
+ wb.add(frame);
469
+
470
+ // Colored marks on the board
471
+ const markColors = [0x34d399, 0x60a5fa, 0xf87171, 0xfbbf24];
472
+ for (let i = 0; i < 4; i++) {
473
+ const markGeo = new THREE.BoxGeometry(0.6, 0.04, 0.01);
474
+ const mark = new THREE.Mesh(markGeo, new THREE.MeshBasicMaterial({
475
+ color: markColors[i], transparent: true, opacity: 0.6,
476
+ }));
477
+ mark.position.set(-0.9 + i * 0.6, 3.5 - i * 0.2, -9.86);
478
+ wb.add(mark);
479
+ }
480
+
481
+ officeGroup.add(wb);
482
+ }
483
+
484
+ /* ═══════════════════════════════════════════════════════════════════════════════
485
+ Pillars
486
+ ═══════════════════════════════════════════════════════════════════════════════ */
487
+ function buildPillars() {
488
+ const positions = [[-8, 0], [8, 0], [-8, -6], [8, -6]];
489
+ positions.forEach(([x, z], i) => {
490
+ const pillarGeo = new THREE.BoxGeometry(0.4, 6.2, 0.4);
491
+ const pillar = new THREE.Mesh(pillarGeo, MAT.pillar);
492
+ pillar.position.set(x, 3.3, z);
493
+ pillar.castShadow = true;
494
+ pillar.name = `pillar-${i}`;
495
+ officeGroup.add(pillar);
496
+
497
+ // Neon accent at pillar base
498
+ const neonGeo = new THREE.BoxGeometry(0.5, 0.04, 0.5);
499
+ const neon = new THREE.Mesh(neonGeo, i < 2 ? MAT.neon : MAT.neonBlue);
500
+ neon.position.set(x, 0.22, z);
501
+ officeGroup.add(neon);
502
+ });
503
+ }
504
+
505
+ /* ═══════════════════════════════════════════════════════════════════════════════
506
+ Carpet
507
+ ═══════════════════════════════════════════════════════════════════════════════ */
508
+ function buildCarpet() {
509
+ const carpetGeo = new THREE.BoxGeometry(10, 0.03, 7);
510
+ const carpet = new THREE.Mesh(carpetGeo, MAT.carpet);
511
+ carpet.position.set(2, 0.22, -3);
512
+ carpet.receiveShadow = true;
513
+ carpet.name = 'carpet';
514
+ officeGroup.add(carpet);
515
+ }
516
+
517
+ /* ═══════════════════════════════════════════════════════════════════════════════
518
+ Ceiling Lights
519
+ ═══════════════════════════════════════════════════════════════════════════════ */
520
+ function buildCeilingLights() {
521
+ const positions = [[-4, -4], [4, -4], [-4, 3], [4, 3], [0, 0]];
522
+ positions.forEach(([x, z], i) => {
523
+ const lightGroup = new THREE.Group();
524
+ lightGroup.name = `ceiling-light-${i}`;
525
+
526
+ // Housing
527
+ const housingGeo = new THREE.BoxGeometry(1.5, 0.06, 0.4);
528
+ const housing = new THREE.Mesh(housingGeo, MAT.metal);
529
+ housing.position.set(0, 6.0, 0);
530
+ lightGroup.add(housing);
531
+
532
+ // Light panel (emitting)
533
+ const panelGeo = new THREE.BoxGeometry(1.4, 0.02, 0.35);
534
+ const panelMat = new THREE.MeshBasicMaterial({
535
+ color: currentTheme === 'dark' ? 0xddeeff : 0xfffef0,
536
+ transparent: true,
537
+ opacity: currentTheme === 'dark' ? 0.3 : 0.6,
538
+ });
539
+ const panel = new THREE.Mesh(panelGeo, panelMat);
540
+ panel.position.set(0, 5.97, 0);
541
+ panel.name = 'light-panel';
542
+ lightGroup.add(panel);
543
+
544
+ // Wire
545
+ const wireGeo = new THREE.CylinderGeometry(0.008, 0.008, 0.5, 4);
546
+ const wire = new THREE.Mesh(wireGeo, MAT.metal);
547
+ wire.position.set(-0.5, 6.25, 0);
548
+ lightGroup.add(wire);
549
+ const wire2 = wire.clone();
550
+ wire2.position.x = 0.5;
551
+ lightGroup.add(wire2);
552
+
553
+ lightGroup.position.set(x, 0, z);
554
+ officeGroup.add(lightGroup);
555
+ });
556
+ }
557
+
558
+ /* ═══════════════════════════════════════════════════════════════════════════════
559
+ Neon Strips — Decorative glowing lines
560
+ ═══════════════════════════════════════════════════════════════════════════════ */
561
+ function buildNeonStrips() {
562
+ // Floor neon strips (crossing pattern)
563
+ const stripGeo = new THREE.BoxGeometry(0.04, 0.02, 14);
564
+ const strip1 = new THREE.Mesh(stripGeo, MAT.neon);
565
+ strip1.position.set(-5.04, 0.23, -2);
566
+ officeGroup.add(strip1);
567
+
568
+ const strip2Geo = new THREE.BoxGeometry(12, 0.02, 0.04);
569
+ const strip2 = new THREE.Mesh(strip2Geo, MAT.neonBlue);
570
+ strip2.position.set(2, 0.23, 3.04);
571
+ officeGroup.add(strip2);
572
+
573
+ // Ceiling neon strip
574
+ const ceilStripGeo = new THREE.BoxGeometry(26, 0.03, 0.03);
575
+ const ceilStrip = new THREE.Mesh(ceilStripGeo, MAT.neonPurple);
576
+ ceilStrip.position.set(0, 6.15, 0);
577
+ officeGroup.add(ceilStrip);
578
+
579
+ const ceilStrip2Geo = new THREE.BoxGeometry(0.03, 0.03, 18);
580
+ const ceilStrip2 = new THREE.Mesh(ceilStrip2Geo, MAT.neon);
581
+ ceilStrip2.position.set(0, 6.15, 0);
582
+ officeGroup.add(ceilStrip2);
583
+ }
584
+
585
+ /* ═══════════════════════════════════════════════════════════════════════════════
586
+ Center Platform — Hologram display base
587
+ ═══════════════════════════════════════════════════════════════════════════════ */
588
+ function buildCenterPlatform() {
589
+ const platform = new THREE.Group();
590
+ platform.name = 'center-platform';
591
+
592
+ // Octagonal base
593
+ const baseGeo = new THREE.CylinderGeometry(1.5, 1.6, 0.15, 8);
594
+ const base = new THREE.Mesh(baseGeo, MAT.metal);
595
+ base.position.set(0, 0.28, 0);
596
+ base.receiveShadow = true;
597
+ platform.add(base);
598
+
599
+ // Inner ring
600
+ const innerGeo = new THREE.CylinderGeometry(1.0, 1.0, 0.2, 16);
601
+ const inner = new THREE.Mesh(innerGeo, MAT.accent);
602
+ inner.position.set(0, 0.32, 0);
603
+ platform.add(inner);
604
+
605
+ // Rotating accent ring
606
+ const ringGeo = new THREE.TorusGeometry(1.3, 0.02, 8, 32);
607
+ const ring = new THREE.Mesh(ringGeo, MAT.neon);
608
+ ring.rotation.x = Math.PI / 2;
609
+ ring.position.set(0, 0.4, 0);
610
+ ring.name = 'holo-ring';
611
+ platform.add(ring);
612
+
613
+ // Second ring (tilted)
614
+ const ring2 = new THREE.Mesh(
615
+ new THREE.TorusGeometry(1.1, 0.015, 8, 32), MAT.neonBlue);
616
+ ring2.rotation.x = Math.PI / 2;
617
+ ring2.rotation.z = Math.PI / 6;
618
+ ring2.position.set(0, 0.5, 0);
619
+ ring2.name = 'holo-ring-2';
620
+ platform.add(ring2);
621
+
622
+ officeGroup.add(platform);
623
+ }
624
+
625
+ /* ═══════════════════════════════════════════════════════════════════════════════
626
+ Bookshelf
627
+ ═══════════════════════════════════════════════════════════════════════════════ */
628
+ function buildBookshelf() {
629
+ const shelf = new THREE.Group();
630
+ shelf.name = 'bookshelf';
631
+
632
+ // Frame
633
+ const frameGeo = new THREE.BoxGeometry(2.0, 3.0, 0.4);
634
+ const frame = new THREE.Mesh(frameGeo, MAT.desk);
635
+ frame.position.set(0, 1.7, 0);
636
+ frame.castShadow = true;
637
+ shelf.add(frame);
638
+
639
+ // Shelves
640
+ for (let i = 0; i < 4; i++) {
641
+ const shelfGeo = new THREE.BoxGeometry(1.9, 0.04, 0.38);
642
+ const sh = new THREE.Mesh(shelfGeo, MAT.deskTop);
643
+ sh.position.set(0, 0.5 + i * 0.7, 0);
644
+ shelf.add(sh);
645
+ }
646
+
647
+ // Books (colored blocks)
648
+ const bookColors = [0x60a5fa, 0xf87171, 0x34d399, 0xfbbf24, 0xa78bfa, 0xf472b6];
649
+ for (let row = 0; row < 3; row++) {
650
+ let bx = -0.8;
651
+ for (let j = 0; j < 5; j++) {
652
+ const w = 0.08 + Math.random() * 0.15;
653
+ const h = 0.25 + Math.random() * 0.15;
654
+ const bookGeo = new THREE.BoxGeometry(w, h, 0.25);
655
+ const bookMat = new THREE.MeshStandardMaterial({
656
+ color: bookColors[(row * 5 + j) % bookColors.length],
657
+ roughness: 0.8,
658
+ });
659
+ const book = new THREE.Mesh(bookGeo, bookMat);
660
+ book.position.set(bx + w / 2, 0.55 + row * 0.7 + h / 2, 0);
661
+ shelf.add(book);
662
+ bx += w + 0.02;
663
+ }
664
+ }
665
+
666
+ shelf.position.set(-12.5, 0.2, -4);
667
+ shelf.rotation.y = Math.PI / 2;
668
+ officeGroup.add(shelf);
669
+ }
670
+
671
+ /* ═══════════════════════════════════════════════════════════════════════════════
672
+ Water Cooler
673
+ ═══════════════════════════════════════════════════════════════════════════════ */
674
+ function buildWaterCooler() {
675
+ const cooler = new THREE.Group();
676
+ cooler.name = 'water-cooler';
677
+
678
+ // Body
679
+ const bodyGeo = new THREE.BoxGeometry(0.4, 1.2, 0.35);
680
+ const body = new THREE.Mesh(bodyGeo, MAT.server);
681
+ body.position.set(0, 0.8, 0);
682
+ body.castShadow = true;
683
+ cooler.add(body);
684
+
685
+ // Water jug on top
686
+ const jugGeo = new THREE.CylinderGeometry(0.14, 0.16, 0.5, 12);
687
+ const jugMat = new THREE.MeshPhysicalMaterial({
688
+ color: 0x93c5fd, transmission: 0.8, roughness: 0.1,
689
+ thickness: 0.3, transparent: true, opacity: 0.5,
690
+ });
691
+ const jug = new THREE.Mesh(jugGeo, jugMat);
692
+ jug.position.set(0, 1.65, 0);
693
+ cooler.add(jug);
694
+
695
+ cooler.position.set(11, 0.2, -4);
696
+ officeGroup.add(cooler);
697
+ }
698
+
699
+ /* ═══════════════════════════════════════════════════════════════════════════════
700
+ Decorative Elements
701
+ ═══════════════════════════════════════════════════════════════════════════════ */
702
+ function buildDecorativeElements() {
703
+ // Wall clock
704
+ const clockGroup = new THREE.Group();
705
+ clockGroup.name = 'wall-clock';
706
+ const clockFace = new THREE.Mesh(
707
+ new THREE.CylinderGeometry(0.4, 0.4, 0.04, 24),
708
+ MAT.whiteboard);
709
+ clockFace.rotation.x = Math.PI / 2;
710
+ clockFace.position.set(-6, 4.5, -9.9);
711
+ clockGroup.add(clockFace);
712
+
713
+ const clockRim = new THREE.Mesh(
714
+ new THREE.TorusGeometry(0.4, 0.02, 8, 24),
715
+ MAT.frame);
716
+ clockRim.position.set(-6, 4.5, -9.88);
717
+ clockGroup.add(clockRim);
718
+
719
+ // Clock hands
720
+ const hourGeo = new THREE.BoxGeometry(0.02, 0.2, 0.01);
721
+ const hourHand = new THREE.Mesh(hourGeo, MAT.frame);
722
+ hourHand.position.set(-6, 4.6, -9.86);
723
+ hourHand.rotation.z = Math.PI / 4;
724
+ clockGroup.add(hourHand);
725
+
726
+ const minGeo = new THREE.BoxGeometry(0.015, 0.3, 0.01);
727
+ const minHand = new THREE.Mesh(minGeo, MAT.accent);
728
+ minHand.position.set(-6, 4.6, -9.84);
729
+ minHand.rotation.z = -Math.PI / 6;
730
+ clockGroup.add(minHand);
731
+
732
+ officeGroup.add(clockGroup);
733
+
734
+ // Ceiling fan
735
+ const fan = new THREE.Group();
736
+ fan.name = 'ceiling-fan';
737
+ const fanHub = new THREE.Mesh(
738
+ new THREE.CylinderGeometry(0.1, 0.1, 0.08, 12), MAT.metal);
739
+ fanHub.position.set(0, 6.1, 0);
740
+ fan.add(fanHub);
741
+ const fanRod = new THREE.Mesh(
742
+ new THREE.CylinderGeometry(0.02, 0.02, 0.4, 6), MAT.metal);
743
+ fanRod.position.set(0, 6.3, 0);
744
+ fan.add(fanRod);
745
+
746
+ for (let i = 0; i < 4; i++) {
747
+ const bladeGeo = new THREE.BoxGeometry(1.5, 0.02, 0.25);
748
+ const blade = new THREE.Mesh(bladeGeo, MAT.desk);
749
+ blade.position.set(0, 6.08, 0);
750
+ blade.rotation.y = (i / 4) * Math.PI * 2;
751
+ blade.translateX(0.75);
752
+ fan.add(blade);
753
+ }
754
+
755
+ fan.position.set(2, 0, -2);
756
+ officeGroup.add(fan);
757
+
758
+ // Door frame on side wall
759
+ const doorFrame = new THREE.Group();
760
+ doorFrame.name = 'door-frame';
761
+ const doorPost = new THREE.Mesh(
762
+ new THREE.BoxGeometry(0.1, 3, 0.1), MAT.frame);
763
+ doorPost.position.set(-13.95, 1.7, 5);
764
+ doorFrame.add(doorPost);
765
+ const doorPost2 = doorPost.clone();
766
+ doorPost2.position.z = 7;
767
+ doorFrame.add(doorPost2);
768
+ const doorLintel = new THREE.Mesh(
769
+ new THREE.BoxGeometry(0.1, 0.1, 2.1), MAT.frame);
770
+ doorLintel.position.set(-13.95, 3.2, 6);
771
+ doorFrame.add(doorLintel);
772
+ officeGroup.add(doorFrame);
773
+ }
774
+
775
+ /* ═══════════════════════════════════════════════════════════════════════════════
776
+ Floor Y position getter
777
+ ═══════════════════════════════════════════════════════════════════════════════ */
778
+ export function getFloorY() {
779
+ return 0.2;
780
+ }
781
+
782
+ /* ═══════════════════════════════════════════════════════════════════════════════
783
+ Theme Update
784
+ ═══════════════════════════════════════════════════════════════════════════════ */
785
+ export function updateOfficeLighting(theme) {
786
+ if (!officeGroup) return;
787
+ currentTheme = theme;
788
+
789
+ // Reload materials
790
+ initMaterials(theme);
791
+
792
+ // We rebuild the office to apply new materials
793
+ const scene = getScene();
794
+ scene.remove(officeGroup);
795
+ officeGroup = new THREE.Group();
796
+ officeGroup.name = 'office';
797
+
798
+ buildFloor();
799
+ buildWalls();
800
+ buildGlassPartitions();
801
+ buildDesks(6);
802
+ buildServerRack();
803
+ buildCoffeeMachine();
804
+ buildPlants();
805
+ buildWhiteboard();
806
+ buildPillars();
807
+ buildCarpet();
808
+ buildCeilingLights();
809
+ buildNeonStrips();
810
+ buildCenterPlatform();
811
+ buildBookshelf();
812
+ buildWaterCooler();
813
+ buildDecorativeElements();
814
+
815
+ scene.add(officeGroup);
816
+ }