watercooler 0.0.4 → 0.0.6
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.
- package/package.json +1 -1
- package/public/app.js +853 -228
- package/public/index.html +29 -9
- package/server.ts +64 -2
package/public/app.js
CHANGED
|
@@ -2,19 +2,20 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
3
3
|
|
|
4
4
|
// State
|
|
5
|
-
let config = { user: '', mailbox: '' };
|
|
5
|
+
let config = { user: '', mailbox: '', avatar: null };
|
|
6
6
|
let messages = []; // Messages TO user (for main panel)
|
|
7
|
-
let allMessages = []; // All messages involving user (for
|
|
7
|
+
let allMessages = []; // All messages involving user (for desk dialogs)
|
|
8
8
|
let recipients = [];
|
|
9
|
+
let avatarStates = {}; // Map of name -> {tool_name, timestamp}
|
|
9
10
|
let scene, camera, renderer, controls;
|
|
10
11
|
let agentMeshes = new Map();
|
|
11
12
|
let connectionLines = [];
|
|
12
13
|
let raycaster, mouse;
|
|
13
14
|
|
|
14
|
-
// Color palette for agents
|
|
15
|
+
// Color palette for agents - modern muted tones
|
|
15
16
|
const agentColors = [
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
0x5EEAD4, 0x6EE7B7, 0x7DD3FC, 0xA78BFA, 0xFBBF24,
|
|
18
|
+
0xF9A8D4, 0x86EFAC, 0x93C5FD, 0xC4B5FD, 0x67E8F9
|
|
18
19
|
];
|
|
19
20
|
|
|
20
21
|
function getAgentColor(name) {
|
|
@@ -25,81 +26,107 @@ function getAgentColor(name) {
|
|
|
25
26
|
return agentColors[Math.abs(hash) % agentColors.length];
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// Platform dimensions
|
|
30
|
+
const PLATFORM_SIZE = 60;
|
|
31
|
+
const PLATFORM_HEIGHT = 2;
|
|
32
|
+
const WALL_HEIGHT = 18;
|
|
33
|
+
|
|
34
|
+
// Animated objects
|
|
35
|
+
let holoSphere = null;
|
|
36
|
+
let holoParticles = null;
|
|
37
|
+
let glowLights = [];
|
|
38
|
+
let floatingParticles = [];
|
|
39
|
+
|
|
28
40
|
// Initialize Three.js
|
|
29
41
|
function init() {
|
|
30
42
|
const container = document.getElementById('canvas-container');
|
|
31
43
|
|
|
32
44
|
scene = new THREE.Scene();
|
|
33
|
-
scene.background = new THREE.Color(
|
|
45
|
+
scene.background = new THREE.Color(0x1a3a3a);
|
|
46
|
+
scene.fog = new THREE.FogExp2(0x1a3a3a, 0.003);
|
|
34
47
|
|
|
35
|
-
camera = new THREE.PerspectiveCamera(
|
|
36
|
-
camera.position.set(
|
|
48
|
+
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
49
|
+
camera.position.set(55, 45, 55);
|
|
37
50
|
|
|
38
51
|
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
39
52
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
53
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
40
54
|
renderer.shadowMap.enabled = true;
|
|
41
55
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
56
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
57
|
+
renderer.toneMappingExposure = 1.2;
|
|
42
58
|
container.appendChild(renderer.domElement);
|
|
43
59
|
|
|
44
60
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
45
61
|
controls.enableDamping = true;
|
|
46
62
|
controls.dampingFactor = 0.05;
|
|
47
|
-
controls.maxPolarAngle = Math.PI / 2 - 0.
|
|
48
|
-
controls.minDistance =
|
|
49
|
-
controls.maxDistance =
|
|
63
|
+
controls.maxPolarAngle = Math.PI / 2 - 0.05;
|
|
64
|
+
controls.minDistance = 25;
|
|
65
|
+
controls.maxDistance = 120;
|
|
50
66
|
controls.enableZoom = true;
|
|
51
67
|
controls.zoomSpeed = 0.8;
|
|
52
|
-
controls.enablePan = false;
|
|
68
|
+
controls.enablePan = false;
|
|
69
|
+
controls.target.set(0, 5, 0);
|
|
53
70
|
controls.touches = {
|
|
54
71
|
ONE: THREE.TOUCH.ROTATE,
|
|
55
72
|
TWO: THREE.TOUCH.DOLLY_PAN
|
|
56
73
|
};
|
|
57
74
|
|
|
58
|
-
// Lighting
|
|
59
|
-
|
|
75
|
+
// === Lighting ===
|
|
76
|
+
// Soft ambient
|
|
77
|
+
const ambientLight = new THREE.AmbientLight(0x2d5a5a, 0.8);
|
|
60
78
|
scene.add(ambientLight);
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
dirLight.
|
|
80
|
+
// Main directional light (warm)
|
|
81
|
+
const dirLight = new THREE.DirectionalLight(0xfff5e6, 0.6);
|
|
82
|
+
dirLight.position.set(40, 80, 30);
|
|
64
83
|
dirLight.castShadow = true;
|
|
65
|
-
dirLight.shadow.camera.left = -
|
|
66
|
-
dirLight.shadow.camera.right =
|
|
67
|
-
dirLight.shadow.camera.top =
|
|
68
|
-
dirLight.shadow.camera.bottom = -
|
|
84
|
+
dirLight.shadow.camera.left = -40;
|
|
85
|
+
dirLight.shadow.camera.right = 40;
|
|
86
|
+
dirLight.shadow.camera.top = 40;
|
|
87
|
+
dirLight.shadow.camera.bottom = -40;
|
|
69
88
|
dirLight.shadow.mapSize.width = 2048;
|
|
70
89
|
dirLight.shadow.mapSize.height = 2048;
|
|
90
|
+
dirLight.shadow.bias = -0.001;
|
|
71
91
|
scene.add(dirLight);
|
|
72
92
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
// Fill light from below (teal tint)
|
|
94
|
+
const fillLight = new THREE.DirectionalLight(0x4fd1c5, 0.3);
|
|
95
|
+
fillLight.position.set(-20, 5, -20);
|
|
96
|
+
scene.add(fillLight);
|
|
97
|
+
|
|
98
|
+
// Hemisphere light for natural ambient
|
|
99
|
+
const hemiLight = new THREE.HemisphereLight(0x4fd1c5, 0x1a3a3a, 0.4);
|
|
100
|
+
scene.add(hemiLight);
|
|
101
|
+
|
|
102
|
+
// === Platform ===
|
|
103
|
+
createPlatform();
|
|
104
|
+
|
|
105
|
+
// === Glass Walls ===
|
|
106
|
+
createGlassWalls();
|
|
107
|
+
|
|
108
|
+
// === Decorative Plants ===
|
|
109
|
+
createPlants();
|
|
83
110
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
grid.material.opacity = 0.2;
|
|
87
|
-
grid.material.transparent = true;
|
|
88
|
-
scene.add(grid);
|
|
111
|
+
// === Holographic Sphere ===
|
|
112
|
+
createHolographicSphere();
|
|
89
113
|
|
|
90
|
-
//
|
|
91
|
-
|
|
114
|
+
// === Ambient Glow Lights ===
|
|
115
|
+
createGlowLights();
|
|
116
|
+
|
|
117
|
+
// === Floating Particles ===
|
|
118
|
+
createFloatingParticles();
|
|
92
119
|
|
|
93
120
|
window.addEventListener('resize', onWindowResize);
|
|
94
121
|
|
|
95
|
-
// Raycaster for
|
|
122
|
+
// Raycaster for desk clicks
|
|
96
123
|
raycaster = new THREE.Raycaster();
|
|
97
124
|
mouse = new THREE.Vector2();
|
|
98
|
-
renderer.domElement.addEventListener('click',
|
|
125
|
+
renderer.domElement.addEventListener('click', onDeskClick);
|
|
99
126
|
|
|
100
127
|
// Add touch support for mobile
|
|
101
|
-
renderer.domElement.addEventListener('touchstart',
|
|
102
|
-
renderer.domElement.addEventListener('touchend',
|
|
128
|
+
renderer.domElement.addEventListener('touchstart', onDeskTouchStart, { passive: false });
|
|
129
|
+
renderer.domElement.addEventListener('touchend', onDeskTouchEnd, { passive: false });
|
|
103
130
|
|
|
104
131
|
// Disable context menu on mobile for better UX
|
|
105
132
|
renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
@@ -112,147 +139,648 @@ function init() {
|
|
|
112
139
|
animate();
|
|
113
140
|
}
|
|
114
141
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
142
|
+
function createPlatform() {
|
|
143
|
+
// Main platform - dark concrete slab
|
|
144
|
+
const platformGeo = new THREE.BoxGeometry(PLATFORM_SIZE, PLATFORM_HEIGHT, PLATFORM_SIZE);
|
|
145
|
+
const platformMat = new THREE.MeshStandardMaterial({
|
|
146
|
+
color: 0x3a3a3a,
|
|
147
|
+
roughness: 0.4,
|
|
148
|
+
metalness: 0.1
|
|
149
|
+
});
|
|
150
|
+
const platform = new THREE.Mesh(platformGeo, platformMat);
|
|
151
|
+
platform.position.y = PLATFORM_HEIGHT / 2;
|
|
152
|
+
platform.receiveShadow = true;
|
|
153
|
+
platform.castShadow = true;
|
|
154
|
+
scene.add(platform);
|
|
155
|
+
|
|
156
|
+
// Edge trim - lighter accent
|
|
157
|
+
const trimGeo = new THREE.BoxGeometry(PLATFORM_SIZE + 0.5, 0.3, PLATFORM_SIZE + 0.5);
|
|
158
|
+
const trimMat = new THREE.MeshStandardMaterial({
|
|
159
|
+
color: 0x5a5a5a,
|
|
160
|
+
roughness: 0.3,
|
|
161
|
+
metalness: 0.3
|
|
162
|
+
});
|
|
163
|
+
const trim = new THREE.Mesh(trimGeo, trimMat);
|
|
164
|
+
trim.position.y = PLATFORM_HEIGHT + 0.15;
|
|
165
|
+
scene.add(trim);
|
|
166
|
+
|
|
167
|
+
// Floor surface - polished concrete with subtle grid
|
|
168
|
+
const floorGeo = new THREE.PlaneGeometry(PLATFORM_SIZE - 2, PLATFORM_SIZE - 2);
|
|
169
|
+
const floorMat = new THREE.MeshStandardMaterial({
|
|
170
|
+
color: 0x4a4a4a,
|
|
171
|
+
roughness: 0.2,
|
|
172
|
+
metalness: 0.15
|
|
173
|
+
});
|
|
174
|
+
const floor = new THREE.Mesh(floorGeo, floorMat);
|
|
175
|
+
floor.rotation.x = -Math.PI / 2;
|
|
176
|
+
floor.position.y = PLATFORM_HEIGHT + 0.02;
|
|
177
|
+
floor.receiveShadow = true;
|
|
178
|
+
scene.add(floor);
|
|
179
|
+
|
|
180
|
+
// Subtle grid on floor
|
|
181
|
+
const gridHelper = new THREE.GridHelper(PLATFORM_SIZE - 4, 20, 0x555555, 0x444444);
|
|
182
|
+
gridHelper.position.y = PLATFORM_HEIGHT + 0.05;
|
|
183
|
+
gridHelper.material.opacity = 0.15;
|
|
184
|
+
gridHelper.material.transparent = true;
|
|
185
|
+
scene.add(gridHelper);
|
|
186
|
+
|
|
187
|
+
// Ground below platform (dark reflection surface)
|
|
188
|
+
const groundGeo = new THREE.PlaneGeometry(300, 300);
|
|
189
|
+
const groundMat = new THREE.MeshStandardMaterial({
|
|
190
|
+
color: 0x1a3a3a,
|
|
191
|
+
roughness: 0.6,
|
|
192
|
+
metalness: 0.2
|
|
193
|
+
});
|
|
194
|
+
const ground = new THREE.Mesh(groundGeo, groundMat);
|
|
195
|
+
ground.rotation.x = -Math.PI / 2;
|
|
196
|
+
ground.position.y = -0.1;
|
|
197
|
+
ground.receiveShadow = true;
|
|
198
|
+
scene.add(ground);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createGlassWalls() {
|
|
202
|
+
const glassMat = new THREE.MeshPhysicalMaterial({
|
|
203
|
+
color: 0x88cccc,
|
|
204
|
+
transparent: true,
|
|
205
|
+
opacity: 0.08,
|
|
206
|
+
roughness: 0.05,
|
|
207
|
+
metalness: 0.0,
|
|
208
|
+
transmission: 0.95,
|
|
209
|
+
thickness: 0.5,
|
|
210
|
+
side: THREE.DoubleSide
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const wallHeight = WALL_HEIGHT;
|
|
214
|
+
const wallY = PLATFORM_HEIGHT + wallHeight / 2;
|
|
215
|
+
const halfSize = PLATFORM_SIZE / 2;
|
|
216
|
+
|
|
217
|
+
// Back wall
|
|
218
|
+
const backWall = new THREE.Mesh(
|
|
219
|
+
new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
|
|
220
|
+
glassMat
|
|
221
|
+
);
|
|
222
|
+
backWall.position.set(0, wallY, -halfSize);
|
|
223
|
+
scene.add(backWall);
|
|
224
|
+
|
|
225
|
+
// Left wall
|
|
226
|
+
const leftWall = new THREE.Mesh(
|
|
227
|
+
new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
|
|
228
|
+
glassMat
|
|
229
|
+
);
|
|
230
|
+
leftWall.position.set(-halfSize, wallY, 0);
|
|
231
|
+
leftWall.rotation.y = Math.PI / 2;
|
|
232
|
+
scene.add(leftWall);
|
|
233
|
+
|
|
234
|
+
// Right wall (partial, for openness)
|
|
235
|
+
const rightWall = new THREE.Mesh(
|
|
236
|
+
new THREE.PlaneGeometry(PLATFORM_SIZE, wallHeight),
|
|
237
|
+
glassMat
|
|
238
|
+
);
|
|
239
|
+
rightWall.position.set(halfSize, wallY, 0);
|
|
240
|
+
rightWall.rotation.y = -Math.PI / 2;
|
|
241
|
+
scene.add(rightWall);
|
|
242
|
+
|
|
243
|
+
// Glass edge frames (vertical pillars at corners)
|
|
244
|
+
const pillarGeo = new THREE.BoxGeometry(0.5, wallHeight, 0.5);
|
|
245
|
+
const pillarMat = new THREE.MeshStandardMaterial({
|
|
246
|
+
color: 0x777777,
|
|
247
|
+
roughness: 0.2,
|
|
248
|
+
metalness: 0.6
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const corners = [
|
|
252
|
+
[-halfSize, wallY, -halfSize],
|
|
253
|
+
[halfSize, wallY, -halfSize],
|
|
254
|
+
[-halfSize, wallY, halfSize],
|
|
255
|
+
[halfSize, wallY, halfSize]
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
corners.forEach(pos => {
|
|
259
|
+
const pillar = new THREE.Mesh(pillarGeo, pillarMat);
|
|
260
|
+
pillar.position.set(...pos);
|
|
261
|
+
pillar.castShadow = true;
|
|
262
|
+
scene.add(pillar);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Top edge frame
|
|
266
|
+
const topFrameMat = new THREE.MeshStandardMaterial({
|
|
267
|
+
color: 0x666666,
|
|
268
|
+
roughness: 0.2,
|
|
269
|
+
metalness: 0.5
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const frameY = PLATFORM_HEIGHT + wallHeight;
|
|
273
|
+
|
|
274
|
+
// Back top frame
|
|
275
|
+
const backFrame = new THREE.Mesh(new THREE.BoxGeometry(PLATFORM_SIZE, 0.3, 0.3), topFrameMat);
|
|
276
|
+
backFrame.position.set(0, frameY, -halfSize);
|
|
277
|
+
scene.add(backFrame);
|
|
278
|
+
|
|
279
|
+
// Left top frame
|
|
280
|
+
const leftFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
|
|
281
|
+
leftFrame.position.set(-halfSize, frameY, 0);
|
|
282
|
+
scene.add(leftFrame);
|
|
283
|
+
|
|
284
|
+
// Right top frame
|
|
285
|
+
const rightFrame = new THREE.Mesh(new THREE.BoxGeometry(0.3, 0.3, PLATFORM_SIZE), topFrameMat);
|
|
286
|
+
rightFrame.position.set(halfSize, frameY, 0);
|
|
287
|
+
scene.add(rightFrame);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createPlants() {
|
|
291
|
+
const plantPositions = [
|
|
292
|
+
// Corner clusters
|
|
293
|
+
[-25, PLATFORM_HEIGHT, -25],
|
|
294
|
+
[25, PLATFORM_HEIGHT, -25],
|
|
295
|
+
[-25, PLATFORM_HEIGHT, 25],
|
|
296
|
+
[25, PLATFORM_HEIGHT, 25],
|
|
297
|
+
// Edge accents
|
|
298
|
+
[-20, PLATFORM_HEIGHT, 27],
|
|
299
|
+
[20, PLATFORM_HEIGHT, 27],
|
|
300
|
+
[-27, PLATFORM_HEIGHT, 0],
|
|
301
|
+
[27, PLATFORM_HEIGHT, -15],
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
plantPositions.forEach(pos => {
|
|
305
|
+
createPlantCluster(pos[0], pos[1], pos[2]);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function createPlantCluster(x, y, z) {
|
|
310
|
+
const group = new THREE.Group();
|
|
311
|
+
|
|
312
|
+
// Planter box
|
|
313
|
+
const planterGeo = new THREE.BoxGeometry(3, 1.5, 3);
|
|
314
|
+
const planterMat = new THREE.MeshStandardMaterial({
|
|
315
|
+
color: 0x2a2a2a,
|
|
316
|
+
roughness: 0.6,
|
|
317
|
+
metalness: 0.1
|
|
318
|
+
});
|
|
319
|
+
const planter = new THREE.Mesh(planterGeo, planterMat);
|
|
320
|
+
planter.position.y = 0.75;
|
|
321
|
+
planter.castShadow = true;
|
|
322
|
+
planter.receiveShadow = true;
|
|
323
|
+
group.add(planter);
|
|
324
|
+
|
|
325
|
+
// Soil
|
|
326
|
+
const soilGeo = new THREE.BoxGeometry(2.6, 0.2, 2.6);
|
|
327
|
+
const soilMat = new THREE.MeshStandardMaterial({ color: 0x3d2817 });
|
|
328
|
+
const soil = new THREE.Mesh(soilGeo, soilMat);
|
|
329
|
+
soil.position.y = 1.5;
|
|
330
|
+
group.add(soil);
|
|
331
|
+
|
|
332
|
+
// Foliage - multiple spheres for bush look
|
|
333
|
+
const leafColors = [0x1a6b3a, 0x228B22, 0x2d8b4e, 0x1f7a3f];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < 5; i++) {
|
|
336
|
+
const size = 0.6 + Math.random() * 0.8;
|
|
337
|
+
const leafGeo = new THREE.SphereGeometry(size, 8, 8);
|
|
338
|
+
const leafMat = new THREE.MeshStandardMaterial({
|
|
339
|
+
color: leafColors[Math.floor(Math.random() * leafColors.length)],
|
|
340
|
+
roughness: 0.8
|
|
341
|
+
});
|
|
342
|
+
const leaf = new THREE.Mesh(leafGeo, leafMat);
|
|
343
|
+
leaf.position.set(
|
|
344
|
+
(Math.random() - 0.5) * 1.5,
|
|
345
|
+
1.8 + Math.random() * 1.5,
|
|
346
|
+
(Math.random() - 0.5) * 1.5
|
|
347
|
+
);
|
|
348
|
+
leaf.castShadow = true;
|
|
349
|
+
group.add(leaf);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Tall fern-like elements (cone shapes)
|
|
353
|
+
for (let i = 0; i < 3; i++) {
|
|
354
|
+
const fernGeo = new THREE.ConeGeometry(0.3, 2 + Math.random() * 2, 6);
|
|
355
|
+
const fernMat = new THREE.MeshStandardMaterial({
|
|
356
|
+
color: 0x1a5c2e,
|
|
357
|
+
roughness: 0.7
|
|
358
|
+
});
|
|
359
|
+
const fern = new THREE.Mesh(fernGeo, fernMat);
|
|
360
|
+
fern.position.set(
|
|
361
|
+
(Math.random() - 0.5) * 1.5,
|
|
362
|
+
2.5 + Math.random() * 1.5,
|
|
363
|
+
(Math.random() - 0.5) * 1.5
|
|
364
|
+
);
|
|
365
|
+
fern.castShadow = true;
|
|
366
|
+
group.add(fern);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
group.position.set(x, y, z);
|
|
370
|
+
scene.add(group);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createHolographicSphere() {
|
|
374
|
+
// Wireframe sphere
|
|
375
|
+
const sphereGeo = new THREE.IcosahedronGeometry(6, 3);
|
|
376
|
+
const sphereMat = new THREE.MeshBasicMaterial({
|
|
377
|
+
color: 0x4fd1c5,
|
|
378
|
+
wireframe: true,
|
|
379
|
+
transparent: true,
|
|
380
|
+
opacity: 0.3
|
|
381
|
+
});
|
|
382
|
+
holoSphere = new THREE.Mesh(sphereGeo, sphereMat);
|
|
383
|
+
holoSphere.position.set(0, PLATFORM_HEIGHT + 12, 0);
|
|
384
|
+
scene.add(holoSphere);
|
|
385
|
+
|
|
386
|
+
// Inner glow sphere
|
|
387
|
+
const innerGeo = new THREE.SphereGeometry(4, 32, 32);
|
|
388
|
+
const innerMat = new THREE.MeshBasicMaterial({
|
|
389
|
+
color: 0x4fd1c5,
|
|
390
|
+
transparent: true,
|
|
391
|
+
opacity: 0.05
|
|
392
|
+
});
|
|
393
|
+
const innerSphere = new THREE.Mesh(innerGeo, innerMat);
|
|
394
|
+
holoSphere.add(innerSphere);
|
|
395
|
+
|
|
396
|
+
// Point cloud on sphere surface
|
|
397
|
+
const particleCount = 300;
|
|
398
|
+
const positions = new Float32Array(particleCount * 3);
|
|
399
|
+
for (let i = 0; i < particleCount; i++) {
|
|
400
|
+
const theta = Math.random() * Math.PI * 2;
|
|
401
|
+
const phi = Math.acos(2 * Math.random() - 1);
|
|
402
|
+
const r = 5.5 + Math.random() * 0.5;
|
|
403
|
+
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
|
404
|
+
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
|
|
405
|
+
positions[i * 3 + 2] = r * Math.cos(phi);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const particleGeo = new THREE.BufferGeometry();
|
|
409
|
+
particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
410
|
+
const particleMat = new THREE.PointsMaterial({
|
|
411
|
+
color: 0x88ffee,
|
|
412
|
+
size: 0.15,
|
|
413
|
+
transparent: true,
|
|
414
|
+
opacity: 0.6,
|
|
415
|
+
blending: THREE.AdditiveBlending
|
|
416
|
+
});
|
|
417
|
+
holoParticles = new THREE.Points(particleGeo, particleMat);
|
|
418
|
+
holoSphere.add(holoParticles);
|
|
419
|
+
|
|
420
|
+
// Point light from the sphere
|
|
421
|
+
const sphereLight = new THREE.PointLight(0x4fd1c5, 0.8, 35);
|
|
422
|
+
sphereLight.position.copy(holoSphere.position);
|
|
423
|
+
scene.add(sphereLight);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createGlowLights() {
|
|
427
|
+
// Floor-standing lamp posts
|
|
428
|
+
const lampPositions = [
|
|
429
|
+
[20, PLATFORM_HEIGHT, 15],
|
|
430
|
+
[-20, PLATFORM_HEIGHT, 15],
|
|
431
|
+
[20, PLATFORM_HEIGHT, -20],
|
|
432
|
+
[-20, PLATFORM_HEIGHT, -20],
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
lampPositions.forEach(pos => {
|
|
436
|
+
// Lamp post
|
|
437
|
+
const postGeo = new THREE.CylinderGeometry(0.15, 0.15, 6, 8);
|
|
438
|
+
const postMat = new THREE.MeshStandardMaterial({
|
|
439
|
+
color: 0x555555,
|
|
440
|
+
roughness: 0.3,
|
|
441
|
+
metalness: 0.7
|
|
442
|
+
});
|
|
443
|
+
const post = new THREE.Mesh(postGeo, postMat);
|
|
444
|
+
post.position.set(pos[0], pos[1] + 3, pos[2]);
|
|
445
|
+
post.castShadow = true;
|
|
446
|
+
scene.add(post);
|
|
128
447
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
448
|
+
// Lamp bulb
|
|
449
|
+
const bulbGeo = new THREE.SphereGeometry(0.4, 16, 16);
|
|
450
|
+
const bulbMat = new THREE.MeshBasicMaterial({
|
|
451
|
+
color: 0xffcc66,
|
|
452
|
+
transparent: true,
|
|
453
|
+
opacity: 0.9
|
|
454
|
+
});
|
|
455
|
+
const bulb = new THREE.Mesh(bulbGeo, bulbMat);
|
|
456
|
+
bulb.position.set(pos[0], pos[1] + 6.2, pos[2]);
|
|
457
|
+
scene.add(bulb);
|
|
134
458
|
|
|
135
|
-
|
|
136
|
-
|
|
459
|
+
// Point light
|
|
460
|
+
const light = new THREE.PointLight(0xffcc66, 0.5, 18);
|
|
461
|
+
light.position.set(pos[0], pos[1] + 6.2, pos[2]);
|
|
462
|
+
light.castShadow = false;
|
|
463
|
+
scene.add(light);
|
|
464
|
+
glowLights.push({ bulb, light, baseIntensity: 0.5 });
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function createFloatingParticles() {
|
|
469
|
+
const particleCount = 80;
|
|
470
|
+
const positions = new Float32Array(particleCount * 3);
|
|
471
|
+
|
|
472
|
+
for (let i = 0; i < particleCount; i++) {
|
|
473
|
+
positions[i * 3] = (Math.random() - 0.5) * PLATFORM_SIZE;
|
|
474
|
+
positions[i * 3 + 1] = PLATFORM_HEIGHT + 2 + Math.random() * WALL_HEIGHT;
|
|
475
|
+
positions[i * 3 + 2] = (Math.random() - 0.5) * PLATFORM_SIZE;
|
|
137
476
|
}
|
|
477
|
+
|
|
478
|
+
const geometry = new THREE.BufferGeometry();
|
|
479
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
480
|
+
|
|
481
|
+
const material = new THREE.PointsMaterial({
|
|
482
|
+
color: 0x88ffdd,
|
|
483
|
+
size: 0.12,
|
|
484
|
+
transparent: true,
|
|
485
|
+
opacity: 0.4,
|
|
486
|
+
blending: THREE.AdditiveBlending
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const particles = new THREE.Points(geometry, material);
|
|
490
|
+
scene.add(particles);
|
|
491
|
+
floatingParticles.push(particles);
|
|
138
492
|
}
|
|
139
493
|
|
|
140
|
-
function
|
|
494
|
+
function createAgentDesk(name, position, toolName = null) {
|
|
141
495
|
const color = getAgentColor(name);
|
|
142
496
|
const group = new THREE.Group();
|
|
143
497
|
group.position.copy(position);
|
|
498
|
+
group.position.y = PLATFORM_HEIGHT;
|
|
499
|
+
|
|
500
|
+
// Modern desk - white top with thin legs
|
|
501
|
+
const deskTopGeo = new THREE.BoxGeometry(5, 0.2, 3);
|
|
502
|
+
const deskMat = new THREE.MeshStandardMaterial({
|
|
503
|
+
color: 0xe8e8e8,
|
|
504
|
+
roughness: 0.3,
|
|
505
|
+
metalness: 0.1
|
|
506
|
+
});
|
|
507
|
+
const deskTop = new THREE.Mesh(deskTopGeo, deskMat);
|
|
508
|
+
deskTop.position.y = 2.5;
|
|
509
|
+
deskTop.castShadow = true;
|
|
510
|
+
deskTop.receiveShadow = true;
|
|
511
|
+
group.add(deskTop);
|
|
512
|
+
|
|
513
|
+
// Desk legs - thin metal
|
|
514
|
+
const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 2.4, 8);
|
|
515
|
+
const legMat = new THREE.MeshStandardMaterial({
|
|
516
|
+
color: 0x999999,
|
|
517
|
+
roughness: 0.2,
|
|
518
|
+
metalness: 0.7
|
|
519
|
+
});
|
|
520
|
+
const legPositions = [
|
|
521
|
+
[-2.2, 1.2, -1.2],
|
|
522
|
+
[2.2, 1.2, -1.2],
|
|
523
|
+
[-2.2, 1.2, 1.2],
|
|
524
|
+
[2.2, 1.2, 1.2]
|
|
525
|
+
];
|
|
526
|
+
legPositions.forEach(pos => {
|
|
527
|
+
const leg = new THREE.Mesh(legGeo, legMat);
|
|
528
|
+
leg.position.set(...pos);
|
|
529
|
+
group.add(leg);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Modern chair - sleek
|
|
533
|
+
const chairSeatGeo = new THREE.BoxGeometry(1.8, 0.15, 1.8);
|
|
534
|
+
const chairMat = new THREE.MeshStandardMaterial({
|
|
535
|
+
color: 0x2a2a2a,
|
|
536
|
+
roughness: 0.5,
|
|
537
|
+
metalness: 0.2
|
|
538
|
+
});
|
|
539
|
+
const chairSeat = new THREE.Mesh(chairSeatGeo, chairMat);
|
|
540
|
+
chairSeat.position.set(0, 1.6, 3.2);
|
|
541
|
+
chairSeat.castShadow = true;
|
|
542
|
+
group.add(chairSeat);
|
|
543
|
+
|
|
544
|
+
// Chair back - curved look (box approximation)
|
|
545
|
+
const chairBackGeo = new THREE.BoxGeometry(1.8, 2.2, 0.15);
|
|
546
|
+
const chairBack = new THREE.Mesh(chairBackGeo, chairMat);
|
|
547
|
+
chairBack.position.set(0, 2.7, 4.1);
|
|
548
|
+
chairBack.castShadow = true;
|
|
549
|
+
group.add(chairBack);
|
|
550
|
+
|
|
551
|
+
// Chair post
|
|
552
|
+
const chairPostGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.2, 8);
|
|
553
|
+
const chairPost = new THREE.Mesh(chairPostGeo, legMat);
|
|
554
|
+
chairPost.position.set(0, 0.9, 3.2);
|
|
555
|
+
group.add(chairPost);
|
|
556
|
+
|
|
557
|
+
// Chair base star
|
|
558
|
+
for (let i = 0; i < 5; i++) {
|
|
559
|
+
const armGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.2, 6);
|
|
560
|
+
const arm = new THREE.Mesh(armGeo, legMat);
|
|
561
|
+
const angle = (i / 5) * Math.PI * 2;
|
|
562
|
+
arm.rotation.z = Math.PI / 2;
|
|
563
|
+
arm.position.set(
|
|
564
|
+
Math.cos(angle) * 0.5,
|
|
565
|
+
0.3,
|
|
566
|
+
3.2 + Math.sin(angle) * 0.5
|
|
567
|
+
);
|
|
568
|
+
arm.rotation.y = angle;
|
|
569
|
+
group.add(arm);
|
|
570
|
+
}
|
|
144
571
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
572
|
+
// Person - Body (sitting, modern look)
|
|
573
|
+
const bodyGeo = new THREE.CylinderGeometry(0.6, 0.5, 2, 8);
|
|
574
|
+
const bodyMat = new THREE.MeshStandardMaterial({
|
|
575
|
+
color: color,
|
|
576
|
+
roughness: 0.6,
|
|
577
|
+
metalness: 0.05
|
|
578
|
+
});
|
|
579
|
+
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|
580
|
+
body.position.set(0, 2.7, 3.2);
|
|
581
|
+
body.castShadow = true;
|
|
582
|
+
group.add(body);
|
|
583
|
+
|
|
584
|
+
// Person - Head
|
|
585
|
+
const headGeo = new THREE.SphereGeometry(0.5, 16, 16);
|
|
586
|
+
const headMat = new THREE.MeshStandardMaterial({
|
|
587
|
+
color: 0xf5d0b0,
|
|
588
|
+
roughness: 0.7
|
|
589
|
+
});
|
|
590
|
+
const head = new THREE.Mesh(headGeo, headMat);
|
|
591
|
+
head.position.set(0, 4.0, 3.2);
|
|
592
|
+
head.castShadow = true;
|
|
593
|
+
group.add(head);
|
|
594
|
+
|
|
595
|
+
// Person - Arms on desk
|
|
596
|
+
const armObjGeo = new THREE.CylinderGeometry(0.12, 0.12, 1.8, 6);
|
|
597
|
+
const armMat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.6 });
|
|
598
|
+
|
|
599
|
+
const leftArm = new THREE.Mesh(armObjGeo, armMat);
|
|
600
|
+
leftArm.rotation.z = Math.PI / 2;
|
|
601
|
+
leftArm.rotation.y = 0.3;
|
|
602
|
+
leftArm.position.set(-0.8, 2.8, 2);
|
|
603
|
+
group.add(leftArm);
|
|
604
|
+
|
|
605
|
+
const rightArm = new THREE.Mesh(armObjGeo, armMat);
|
|
606
|
+
rightArm.rotation.z = Math.PI / 2;
|
|
607
|
+
rightArm.rotation.y = -0.3;
|
|
608
|
+
rightArm.position.set(0.8, 2.8, 2);
|
|
609
|
+
group.add(rightArm);
|
|
610
|
+
|
|
611
|
+
// Monitor (modern flat screen)
|
|
612
|
+
const monitorStandGeo = new THREE.CylinderGeometry(0.5, 0.6, 0.1, 16);
|
|
613
|
+
const monitorMat = new THREE.MeshStandardMaterial({
|
|
614
|
+
color: 0x333333,
|
|
615
|
+
roughness: 0.3,
|
|
616
|
+
metalness: 0.5
|
|
617
|
+
});
|
|
618
|
+
const monitorStand = new THREE.Mesh(monitorStandGeo, monitorMat);
|
|
619
|
+
monitorStand.position.set(0, 2.65, 0.8);
|
|
620
|
+
group.add(monitorStand);
|
|
621
|
+
|
|
622
|
+
const monitorNeckGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.2, 8);
|
|
623
|
+
const monitorNeck = new THREE.Mesh(monitorNeckGeo, monitorMat);
|
|
624
|
+
monitorNeck.position.set(0, 3.2, 0.8);
|
|
625
|
+
group.add(monitorNeck);
|
|
626
|
+
|
|
627
|
+
// Screen
|
|
628
|
+
const screenFrameGeo = new THREE.BoxGeometry(3, 1.8, 0.12);
|
|
629
|
+
const screenFrame = new THREE.Mesh(screenFrameGeo, monitorMat);
|
|
630
|
+
screenFrame.position.set(0, 4.0, 0.8);
|
|
631
|
+
screenFrame.castShadow = true;
|
|
632
|
+
group.add(screenFrame);
|
|
633
|
+
|
|
634
|
+
// Screen display (glowing)
|
|
635
|
+
const screenDisplayGeo = new THREE.PlaneGeometry(2.7, 1.5);
|
|
636
|
+
const screenDisplayMat = new THREE.MeshBasicMaterial({
|
|
637
|
+
color: 0x2a6b5e,
|
|
638
|
+
});
|
|
639
|
+
const screenDisplay = new THREE.Mesh(screenDisplayGeo, screenDisplayMat);
|
|
640
|
+
screenDisplay.position.set(0, 4.0, 0.87);
|
|
641
|
+
group.add(screenDisplay);
|
|
642
|
+
|
|
643
|
+
// Screen glow light
|
|
644
|
+
const screenLight = new THREE.PointLight(0x4fd1c5, 0.3, 6);
|
|
645
|
+
screenLight.position.set(0, 4.0, 1.5);
|
|
646
|
+
group.add(screenLight);
|
|
647
|
+
|
|
648
|
+
// Keyboard
|
|
649
|
+
const kbGeo = new THREE.BoxGeometry(1.6, 0.05, 0.5);
|
|
650
|
+
const kbMat = new THREE.MeshStandardMaterial({
|
|
651
|
+
color: 0x444444,
|
|
652
|
+
roughness: 0.5,
|
|
653
|
+
metalness: 0.3
|
|
654
|
+
});
|
|
655
|
+
const keyboard = new THREE.Mesh(kbGeo, kbMat);
|
|
656
|
+
keyboard.position.set(0, 2.63, 2);
|
|
657
|
+
group.add(keyboard);
|
|
185
658
|
|
|
186
659
|
// Name label sprite
|
|
187
660
|
const canvas = document.createElement('canvas');
|
|
188
661
|
const context = canvas.getContext('2d');
|
|
189
|
-
// High DPI canvas for crisp text
|
|
190
662
|
const scale = 2;
|
|
191
663
|
canvas.width = 512;
|
|
192
|
-
canvas.height = 128;
|
|
664
|
+
canvas.height = toolName ? 160 : 128;
|
|
193
665
|
context.scale(scale, scale);
|
|
194
|
-
|
|
195
|
-
|
|
666
|
+
|
|
667
|
+
// Frosted glass background
|
|
668
|
+
context.fillStyle = 'rgba(20, 60, 60, 0.85)';
|
|
669
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
196
670
|
context.fill();
|
|
197
|
-
|
|
198
|
-
|
|
671
|
+
|
|
672
|
+
// Subtle border
|
|
673
|
+
context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
|
|
674
|
+
context.lineWidth = 1;
|
|
675
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
676
|
+
context.stroke();
|
|
677
|
+
|
|
678
|
+
context.font = 'bold 22px Arial';
|
|
679
|
+
context.fillStyle = '#e0f5f0';
|
|
199
680
|
context.textAlign = 'center';
|
|
200
681
|
context.textBaseline = 'middle';
|
|
201
|
-
context.fillText(name, 128,
|
|
682
|
+
context.fillText(name, 128, 24);
|
|
683
|
+
|
|
684
|
+
if (toolName) {
|
|
685
|
+
context.font = 'italic 14px Arial';
|
|
686
|
+
context.fillStyle = '#4fd1c5';
|
|
687
|
+
context.fillText(toolName, 128, 56);
|
|
688
|
+
}
|
|
202
689
|
|
|
203
690
|
const texture = new THREE.CanvasTexture(canvas);
|
|
204
691
|
texture.minFilter = THREE.LinearFilter;
|
|
205
692
|
texture.magFilter = THREE.LinearFilter;
|
|
206
693
|
const spriteMat = new THREE.SpriteMaterial({ map: texture });
|
|
207
694
|
const sprite = new THREE.Sprite(spriteMat);
|
|
208
|
-
sprite.position.set(0,
|
|
209
|
-
sprite.scale.set(
|
|
695
|
+
sprite.position.set(0, 6.5, 2);
|
|
696
|
+
sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
|
|
697
|
+
sprite.name = 'label';
|
|
210
698
|
group.add(sprite);
|
|
211
699
|
|
|
212
|
-
// Path to house
|
|
213
|
-
const pathGeo = new THREE.PlaneGeometry(2, 8);
|
|
214
|
-
const pathMat = new THREE.MeshStandardMaterial({ color: 0xD2B48C });
|
|
215
|
-
const path = new THREE.Mesh(pathGeo, pathMat);
|
|
216
|
-
path.rotation.x = -Math.PI / 2;
|
|
217
|
-
path.position.set(0, 0.02, 7);
|
|
218
|
-
group.add(path);
|
|
219
|
-
|
|
220
700
|
scene.add(group);
|
|
221
701
|
agentMeshes.set(name, group);
|
|
222
702
|
|
|
223
703
|
return group;
|
|
224
704
|
}
|
|
225
705
|
|
|
706
|
+
function updateDeskLabel(desk, name, toolName = null) {
|
|
707
|
+
const sprite = desk.getObjectByName('label');
|
|
708
|
+
if (!sprite) return;
|
|
709
|
+
|
|
710
|
+
const canvas = document.createElement('canvas');
|
|
711
|
+
const context = canvas.getContext('2d');
|
|
712
|
+
const scale = 2;
|
|
713
|
+
canvas.width = 512;
|
|
714
|
+
canvas.height = toolName ? 160 : 128;
|
|
715
|
+
context.scale(scale, scale);
|
|
716
|
+
|
|
717
|
+
context.fillStyle = 'rgba(20, 60, 60, 0.85)';
|
|
718
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
719
|
+
context.fill();
|
|
720
|
+
|
|
721
|
+
context.strokeStyle = 'rgba(79, 209, 197, 0.4)';
|
|
722
|
+
context.lineWidth = 1;
|
|
723
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
724
|
+
context.stroke();
|
|
725
|
+
|
|
726
|
+
context.font = 'bold 22px Arial';
|
|
727
|
+
context.fillStyle = '#e0f5f0';
|
|
728
|
+
context.textAlign = 'center';
|
|
729
|
+
context.textBaseline = 'middle';
|
|
730
|
+
context.fillText(name, 128, 24);
|
|
731
|
+
|
|
732
|
+
if (toolName) {
|
|
733
|
+
context.font = 'italic 14px Arial';
|
|
734
|
+
context.fillStyle = '#4fd1c5';
|
|
735
|
+
context.fillText(toolName, 128, 56);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
739
|
+
texture.minFilter = THREE.LinearFilter;
|
|
740
|
+
texture.magFilter = THREE.LinearFilter;
|
|
741
|
+
sprite.material.map = texture;
|
|
742
|
+
sprite.material.needsUpdate = true;
|
|
743
|
+
|
|
744
|
+
sprite.position.set(0, 6.5, 2);
|
|
745
|
+
sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
|
|
746
|
+
}
|
|
747
|
+
|
|
226
748
|
function createMessageParticle(fromPos, toPos) {
|
|
227
|
-
const particleGeo = new THREE.SphereGeometry(0.
|
|
228
|
-
const particleMat = new THREE.
|
|
229
|
-
color:
|
|
230
|
-
|
|
231
|
-
|
|
749
|
+
const particleGeo = new THREE.SphereGeometry(0.35, 12, 12);
|
|
750
|
+
const particleMat = new THREE.MeshBasicMaterial({
|
|
751
|
+
color: 0xff6b6b,
|
|
752
|
+
transparent: true,
|
|
753
|
+
opacity: 0.95
|
|
232
754
|
});
|
|
233
755
|
const particle = new THREE.Mesh(particleGeo, particleMat);
|
|
234
756
|
|
|
235
757
|
particle.position.copy(fromPos);
|
|
236
|
-
particle.position.y +=
|
|
758
|
+
particle.position.y += 5;
|
|
759
|
+
|
|
760
|
+
// Add a glow point light that follows particle
|
|
761
|
+
const glow = new THREE.PointLight(0xff6b6b, 1.0, 10);
|
|
762
|
+
particle.add(glow);
|
|
237
763
|
|
|
238
764
|
scene.add(particle);
|
|
239
765
|
|
|
240
|
-
// Animate particle
|
|
241
766
|
const startTime = Date.now();
|
|
242
|
-
const duration =
|
|
767
|
+
const duration = 1500;
|
|
243
768
|
|
|
244
769
|
function animateParticle() {
|
|
245
770
|
const elapsed = Date.now() - startTime;
|
|
246
771
|
const progress = Math.min(elapsed / duration, 1);
|
|
247
772
|
|
|
248
773
|
particle.position.lerpVectors(
|
|
249
|
-
new THREE.Vector3(fromPos.x, fromPos.y +
|
|
250
|
-
new THREE.Vector3(toPos.x, toPos.y +
|
|
774
|
+
new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z),
|
|
775
|
+
new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z),
|
|
251
776
|
progress
|
|
252
777
|
);
|
|
253
778
|
|
|
254
|
-
|
|
255
|
-
|
|
779
|
+
particle.position.y += Math.sin(progress * Math.PI) * 2;
|
|
780
|
+
|
|
781
|
+
// Pulse size
|
|
782
|
+
const pulse = Math.sin(progress * Math.PI) * 0.2 + 1;
|
|
783
|
+
particle.scale.setScalar(pulse);
|
|
256
784
|
|
|
257
785
|
if (progress < 1) {
|
|
258
786
|
requestAnimationFrame(animateParticle);
|
|
@@ -268,48 +796,58 @@ function createConnectionLine(fromPos, toPos) {
|
|
|
268
796
|
const startPos = new THREE.Vector3(fromPos.x, fromPos.y + 5, fromPos.z);
|
|
269
797
|
const endPos = new THREE.Vector3(toPos.x, toPos.y + 5, toPos.z);
|
|
270
798
|
|
|
799
|
+
// Create curved line with points
|
|
800
|
+
const mid = new THREE.Vector3().addVectors(startPos, endPos).multiplyScalar(0.5);
|
|
801
|
+
mid.y += 2;
|
|
802
|
+
|
|
803
|
+
const curve = new THREE.QuadraticBezierCurve3(startPos, mid, endPos);
|
|
804
|
+
const points = curve.getPoints(50);
|
|
805
|
+
|
|
806
|
+
// Main line - thicker, glowing red
|
|
271
807
|
const material = new THREE.LineBasicMaterial({
|
|
272
808
|
color: 0xff6b6b,
|
|
273
|
-
opacity: 0.
|
|
809
|
+
opacity: 0.7,
|
|
274
810
|
transparent: true,
|
|
275
811
|
linewidth: 3
|
|
276
812
|
});
|
|
277
813
|
|
|
278
|
-
const points = [startPos, endPos];
|
|
279
|
-
|
|
280
814
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
281
815
|
const line = new THREE.Line(geometry, material);
|
|
282
816
|
|
|
283
817
|
scene.add(line);
|
|
284
818
|
connectionLines.push(line);
|
|
285
819
|
|
|
286
|
-
// Add
|
|
820
|
+
// Add small chevron markers along the path to show direction
|
|
287
821
|
const direction = new THREE.Vector3().subVectors(endPos, startPos).normalize();
|
|
288
|
-
const arrowPos = endPos.clone().sub(direction.clone().multiplyScalar(5));
|
|
289
|
-
|
|
290
|
-
const arrowGeometry = new THREE.ConeGeometry(0.5, 1.5, 8);
|
|
291
|
-
const arrowMaterial = new THREE.MeshStandardMaterial({
|
|
292
|
-
color: 0xff6b6b,
|
|
293
|
-
emissive: 0xff6b6b,
|
|
294
|
-
emissiveIntensity: 0.3
|
|
295
|
-
});
|
|
296
|
-
const arrowhead = new THREE.Mesh(arrowGeometry, arrowMaterial);
|
|
297
|
-
|
|
298
|
-
arrowhead.position.copy(arrowPos);
|
|
299
|
-
|
|
300
|
-
// Orient arrow to point along the line
|
|
301
822
|
const up = new THREE.Vector3(0, 1, 0);
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
823
|
+
const numMarkers = 4;
|
|
824
|
+
for (let i = 0; i < numMarkers; i++) {
|
|
825
|
+
const t = (i + 1) / (numMarkers + 1);
|
|
826
|
+
const markerPos = curve.getPoint(t);
|
|
827
|
+
|
|
828
|
+
const markerGeo = new THREE.ConeGeometry(0.25, 0.7, 8);
|
|
829
|
+
const markerMat = new THREE.MeshBasicMaterial({
|
|
830
|
+
color: 0xff6b6b,
|
|
831
|
+
transparent: true,
|
|
832
|
+
opacity: 0.5
|
|
833
|
+
});
|
|
834
|
+
const marker = new THREE.Mesh(markerGeo, markerMat);
|
|
835
|
+
|
|
836
|
+
// Get tangent at this point for direction
|
|
837
|
+
const tangent = curve.getTangent(t);
|
|
838
|
+
marker.position.copy(markerPos);
|
|
839
|
+
|
|
840
|
+
const markerQuat = new THREE.Quaternion();
|
|
841
|
+
markerQuat.setFromUnitVectors(up, tangent);
|
|
842
|
+
marker.setRotationFromQuaternion(markerQuat);
|
|
843
|
+
|
|
844
|
+
scene.add(marker);
|
|
845
|
+
connectionLines.push(marker);
|
|
846
|
+
}
|
|
308
847
|
|
|
309
|
-
// Send particle
|
|
310
848
|
setTimeout(() => {
|
|
311
849
|
createMessageParticle(fromPos, toPos);
|
|
312
|
-
},
|
|
850
|
+
}, 50);
|
|
313
851
|
}
|
|
314
852
|
|
|
315
853
|
function clearConnections() {
|
|
@@ -321,53 +859,107 @@ function updateVillage() {
|
|
|
321
859
|
clearConnections();
|
|
322
860
|
|
|
323
861
|
// Use recipients (from coworkers.db) as the authoritative list of agents
|
|
324
|
-
// This ensures we only show houses for registered coworkers
|
|
325
862
|
const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
|
|
326
863
|
|
|
327
|
-
// Also add message participants
|
|
864
|
+
// Also add message participants
|
|
328
865
|
messages.forEach(m => {
|
|
329
866
|
allAgents.add(m.sender.toLowerCase());
|
|
330
867
|
allAgents.add(m.recipient.toLowerCase());
|
|
331
868
|
});
|
|
332
869
|
|
|
333
|
-
// Arrange agents in a circle
|
|
870
|
+
// Arrange agents in a circle on the platform
|
|
334
871
|
const agents = Array.from(allAgents);
|
|
335
|
-
const radius =
|
|
872
|
+
const radius = Math.min(20, Math.max(10, agents.length * 3));
|
|
336
873
|
|
|
337
874
|
agents.forEach((agent, index) => {
|
|
338
|
-
const angle = (index / agents.length) * Math.PI * 2;
|
|
875
|
+
const angle = (index / agents.length) * Math.PI * 2 - Math.PI / 2;
|
|
339
876
|
const x = Math.cos(angle) * radius;
|
|
340
877
|
const z = Math.sin(angle) * radius;
|
|
341
878
|
const position = new THREE.Vector3(x, 0, z);
|
|
342
879
|
|
|
880
|
+
const avatarState = avatarStates[agent.toLowerCase()];
|
|
881
|
+
const toolName = avatarState?.tool_name || null;
|
|
882
|
+
|
|
343
883
|
if (!agentMeshes.has(agent)) {
|
|
344
|
-
|
|
884
|
+
const group = createAgentDesk(agent, position, toolName);
|
|
885
|
+
// Face desk toward center
|
|
886
|
+
group.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
|
|
345
887
|
} else {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
888
|
+
const desk = agentMeshes.get(agent);
|
|
889
|
+
desk.position.set(x, PLATFORM_HEIGHT, z);
|
|
890
|
+
desk.lookAt(new THREE.Vector3(0, PLATFORM_HEIGHT, 0));
|
|
891
|
+
updateDeskLabel(desk, agent, toolName);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Remove desks for coworkers that no longer exist
|
|
896
|
+
agentMeshes.forEach((desk, name) => {
|
|
897
|
+
if (!allAgents.has(name)) {
|
|
898
|
+
// Remove from scene
|
|
899
|
+
scene.remove(desk);
|
|
900
|
+
|
|
901
|
+
// Dispose of geometries and materials to prevent memory leaks
|
|
902
|
+
desk.traverse((child) => {
|
|
903
|
+
if (child.isMesh) {
|
|
904
|
+
child.geometry.dispose();
|
|
905
|
+
if (child.material) {
|
|
906
|
+
if (Array.isArray(child.material)) {
|
|
907
|
+
child.material.forEach(m => m.dispose());
|
|
908
|
+
} else {
|
|
909
|
+
child.material.dispose();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// Remove from Map
|
|
916
|
+
agentMeshes.delete(name);
|
|
349
917
|
}
|
|
350
918
|
});
|
|
351
919
|
|
|
352
920
|
// Create connections for unread messages only
|
|
353
921
|
allMessages.forEach(msg => {
|
|
354
|
-
const
|
|
355
|
-
const
|
|
922
|
+
const fromDesk = agentMeshes.get(msg.sender.toLowerCase());
|
|
923
|
+
const toDesk = agentMeshes.get(msg.recipient.toLowerCase());
|
|
356
924
|
|
|
357
|
-
if (
|
|
925
|
+
if (fromDesk && toDesk && !msg.read) {
|
|
358
926
|
createConnectionLine(
|
|
359
|
-
|
|
360
|
-
|
|
927
|
+
fromDesk.position,
|
|
928
|
+
toDesk.position
|
|
361
929
|
);
|
|
362
930
|
}
|
|
363
931
|
});
|
|
364
932
|
|
|
365
|
-
// Update
|
|
366
|
-
|
|
933
|
+
// Update desk labels with unread indicators
|
|
934
|
+
updateDeskLabels();
|
|
367
935
|
}
|
|
368
936
|
|
|
369
937
|
function animate() {
|
|
370
938
|
requestAnimationFrame(animate);
|
|
939
|
+
|
|
940
|
+
const time = Date.now() * 0.001;
|
|
941
|
+
|
|
942
|
+
// Rotate holographic sphere
|
|
943
|
+
if (holoSphere) {
|
|
944
|
+
holoSphere.rotation.y = time * 0.15;
|
|
945
|
+
holoSphere.rotation.x = Math.sin(time * 0.1) * 0.1;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Animate floating particles
|
|
949
|
+
floatingParticles.forEach(particles => {
|
|
950
|
+
const positions = particles.geometry.attributes.position.array;
|
|
951
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
952
|
+
positions[i + 1] += Math.sin(time + positions[i] * 0.1) * 0.003;
|
|
953
|
+
}
|
|
954
|
+
particles.geometry.attributes.position.needsUpdate = true;
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Subtle glow pulse on lamps
|
|
958
|
+
glowLights.forEach((item, i) => {
|
|
959
|
+
const pulse = Math.sin(time * 1.5 + i) * 0.15 + 1;
|
|
960
|
+
item.light.intensity = item.baseIntensity * pulse;
|
|
961
|
+
});
|
|
962
|
+
|
|
371
963
|
controls.update();
|
|
372
964
|
renderer.render(scene, camera);
|
|
373
965
|
}
|
|
@@ -379,14 +971,12 @@ function onWindowResize() {
|
|
|
379
971
|
camera.updateProjectionMatrix();
|
|
380
972
|
renderer.setSize(width, height);
|
|
381
973
|
|
|
382
|
-
// Adjust camera position for better mobile view
|
|
383
974
|
if (width < 768) {
|
|
384
|
-
|
|
385
|
-
camera.position.
|
|
386
|
-
|
|
387
|
-
controls.minDistance = 30; // Prevent zooming too close on mobile
|
|
975
|
+
camera.position.y = Math.max(camera.position.y, 40);
|
|
976
|
+
camera.position.z = Math.max(camera.position.z, 50);
|
|
977
|
+
controls.minDistance = 35;
|
|
388
978
|
} else {
|
|
389
|
-
controls.minDistance =
|
|
979
|
+
controls.minDistance = 25;
|
|
390
980
|
}
|
|
391
981
|
}
|
|
392
982
|
|
|
@@ -410,6 +1000,17 @@ async function loadData() {
|
|
|
410
1000
|
recipients = Array.isArray(recipientsData) ? recipientsData : [];
|
|
411
1001
|
allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
|
|
412
1002
|
|
|
1003
|
+
// Load avatar states if avatar DB is configured
|
|
1004
|
+
if (config.avatar) {
|
|
1005
|
+
try {
|
|
1006
|
+
const avatarsRes = await fetch('/api/avatars');
|
|
1007
|
+
avatarStates = await avatarsRes.json();
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.error('Error loading avatar states:', err);
|
|
1010
|
+
avatarStates = {};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
413
1014
|
updateUI();
|
|
414
1015
|
updateVillage();
|
|
415
1016
|
} catch (err) {
|
|
@@ -510,9 +1111,9 @@ function updateUI() {
|
|
|
510
1111
|
});
|
|
511
1112
|
}
|
|
512
1113
|
|
|
513
|
-
// Update
|
|
1114
|
+
// Update desk dialog if it's open
|
|
514
1115
|
if (document.getElementById('house-dialog').classList.contains('active')) {
|
|
515
|
-
|
|
1116
|
+
updateDeskDialogContent();
|
|
516
1117
|
}
|
|
517
1118
|
}
|
|
518
1119
|
|
|
@@ -525,6 +1126,18 @@ async function markAsRead(id) {
|
|
|
525
1126
|
}
|
|
526
1127
|
}
|
|
527
1128
|
|
|
1129
|
+
window.markAllAsRead = async function() {
|
|
1130
|
+
const unreadMessages = messages.filter(m => !m.read && m.recipient.toLowerCase() === config.user.toLowerCase());
|
|
1131
|
+
if (unreadMessages.length === 0) return;
|
|
1132
|
+
|
|
1133
|
+
try {
|
|
1134
|
+
await Promise.all(unreadMessages.map(m => fetch(`/api/messages/${m.id}/read`, { method: 'POST' })));
|
|
1135
|
+
loadData();
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
console.error('Error marking all as read:', err);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
|
|
528
1141
|
async function sendMessage() {
|
|
529
1142
|
const to = document.getElementById('recipient-select').value;
|
|
530
1143
|
const message = document.getElementById('message-input').value.trim();
|
|
@@ -561,23 +1174,23 @@ async function sendMessage() {
|
|
|
561
1174
|
}
|
|
562
1175
|
}
|
|
563
1176
|
|
|
564
|
-
//
|
|
565
|
-
function
|
|
566
|
-
|
|
1177
|
+
// Desk click handler
|
|
1178
|
+
function onDeskClick(event) {
|
|
1179
|
+
handleDeskInteraction(event.clientX, event.clientY);
|
|
567
1180
|
}
|
|
568
1181
|
|
|
569
1182
|
// Touch handlers for mobile
|
|
570
1183
|
let touchStartX = 0;
|
|
571
1184
|
let touchStartY = 0;
|
|
572
1185
|
|
|
573
|
-
function
|
|
1186
|
+
function onDeskTouchStart(event) {
|
|
574
1187
|
if (event.touches.length === 1) {
|
|
575
1188
|
touchStartX = event.touches[0].clientX;
|
|
576
1189
|
touchStartY = event.touches[0].clientY;
|
|
577
1190
|
}
|
|
578
1191
|
}
|
|
579
1192
|
|
|
580
|
-
function
|
|
1193
|
+
function onDeskTouchEnd(event) {
|
|
581
1194
|
if (event.changedTouches.length === 1) {
|
|
582
1195
|
const touchEndX = event.changedTouches[0].clientX;
|
|
583
1196
|
const touchEndY = event.changedTouches[0].clientY;
|
|
@@ -590,13 +1203,13 @@ function onHouseTouchEnd(event) {
|
|
|
590
1203
|
|
|
591
1204
|
// Only trigger if touch didn't move much (tap vs swipe)
|
|
592
1205
|
if (moveDistance < 20) {
|
|
593
|
-
|
|
1206
|
+
handleDeskInteraction(touchEndX, touchEndY);
|
|
594
1207
|
}
|
|
595
1208
|
}
|
|
596
1209
|
}
|
|
597
1210
|
|
|
598
|
-
// Common
|
|
599
|
-
function
|
|
1211
|
+
// Common desk interaction handler
|
|
1212
|
+
function handleDeskInteraction(clientX, clientY) {
|
|
600
1213
|
// Calculate normalized device coordinates
|
|
601
1214
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
602
1215
|
const x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
|
@@ -607,34 +1220,34 @@ function handleHouseInteraction(clientX, clientY) {
|
|
|
607
1220
|
|
|
608
1221
|
raycaster.setFromCamera(mouse, camera);
|
|
609
1222
|
|
|
610
|
-
// Get all
|
|
611
|
-
const
|
|
1223
|
+
// Get all desk meshes
|
|
1224
|
+
const deskMeshes = [];
|
|
612
1225
|
agentMeshes.forEach((group, name) => {
|
|
613
1226
|
group.children.forEach(child => {
|
|
614
1227
|
if (child.isMesh && !child.userData.isBubble && !child.userData.isCup) {
|
|
615
1228
|
child.userData.agentName = name;
|
|
616
|
-
|
|
1229
|
+
deskMeshes.push(child);
|
|
617
1230
|
}
|
|
618
1231
|
});
|
|
619
1232
|
});
|
|
620
1233
|
|
|
621
|
-
const intersects = raycaster.intersectObjects(
|
|
1234
|
+
const intersects = raycaster.intersectObjects(deskMeshes);
|
|
622
1235
|
|
|
623
1236
|
if (intersects.length > 0) {
|
|
624
1237
|
const agentName = intersects[0].object.userData.agentName;
|
|
625
1238
|
if (agentName) {
|
|
626
|
-
|
|
1239
|
+
showDeskDialog(agentName);
|
|
627
1240
|
}
|
|
628
1241
|
}
|
|
629
1242
|
}
|
|
630
1243
|
|
|
631
|
-
// Global variable to track current agent for
|
|
632
|
-
let
|
|
1244
|
+
// Global variable to track current agent for desk dialog
|
|
1245
|
+
let currentDeskAgent = null;
|
|
633
1246
|
let currentTab = 'received';
|
|
634
1247
|
|
|
635
1248
|
// Show dialog with messages for a specific agent
|
|
636
|
-
async function
|
|
637
|
-
|
|
1249
|
+
async function showDeskDialog(agentName) {
|
|
1250
|
+
currentDeskAgent = agentName.toLowerCase();
|
|
638
1251
|
currentTab = 'received'; // Default to received tab
|
|
639
1252
|
|
|
640
1253
|
const dialog = document.getElementById('house-dialog');
|
|
@@ -674,35 +1287,35 @@ window.switchTab = function(tab) {
|
|
|
674
1287
|
document.getElementById('tab-sent').classList.toggle('active', tab === 'sent');
|
|
675
1288
|
|
|
676
1289
|
// Update content
|
|
677
|
-
|
|
1290
|
+
updateDeskDialogContent();
|
|
678
1291
|
};
|
|
679
1292
|
|
|
680
|
-
function
|
|
1293
|
+
function updateDeskDialogContent() {
|
|
681
1294
|
const content = document.getElementById('house-dialog-content');
|
|
682
1295
|
|
|
683
|
-
if (!
|
|
1296
|
+
if (!currentDeskAgent) return;
|
|
684
1297
|
|
|
685
1298
|
// Filter messages based on current tab - FROM THE AGENT'S PERSPECTIVE
|
|
686
1299
|
let filteredMessages;
|
|
687
1300
|
if (currentTab === 'received') {
|
|
688
1301
|
// Messages RECEIVED BY the agent (sent TO the agent)
|
|
689
1302
|
filteredMessages = allMessages.filter(m =>
|
|
690
|
-
m.recipient.toLowerCase() ===
|
|
1303
|
+
m.recipient.toLowerCase() === currentDeskAgent
|
|
691
1304
|
);
|
|
692
1305
|
} else {
|
|
693
1306
|
// Messages SENT BY the agent
|
|
694
1307
|
filteredMessages = allMessages.filter(m =>
|
|
695
|
-
m.sender.toLowerCase() ===
|
|
1308
|
+
m.sender.toLowerCase() === currentDeskAgent
|
|
696
1309
|
);
|
|
697
1310
|
}
|
|
698
1311
|
|
|
699
1312
|
// Update count badges
|
|
700
1313
|
const receivedCount = allMessages.filter(m =>
|
|
701
|
-
m.recipient.toLowerCase() ===
|
|
1314
|
+
m.recipient.toLowerCase() === currentDeskAgent
|
|
702
1315
|
).length;
|
|
703
1316
|
|
|
704
1317
|
const sentCount = allMessages.filter(m =>
|
|
705
|
-
m.sender.toLowerCase() ===
|
|
1318
|
+
m.sender.toLowerCase() === currentDeskAgent
|
|
706
1319
|
).length;
|
|
707
1320
|
|
|
708
1321
|
const receivedBadge = document.getElementById('received-count');
|
|
@@ -732,66 +1345,75 @@ function updateHouseDialogContent() {
|
|
|
732
1345
|
}
|
|
733
1346
|
}
|
|
734
1347
|
|
|
735
|
-
window.
|
|
1348
|
+
window.closeDeskDialog = function() {
|
|
736
1349
|
document.getElementById('house-dialog').classList.remove('active');
|
|
737
1350
|
};
|
|
738
1351
|
|
|
739
|
-
// Update
|
|
740
|
-
function
|
|
1352
|
+
// Update desk labels to show unread indicators and tool names
|
|
1353
|
+
function updateDeskLabels() {
|
|
741
1354
|
agentMeshes.forEach((group, name) => {
|
|
742
|
-
// Check for unread messages SENT TO this agent (messages they haven't read)
|
|
743
1355
|
const unreadCount = allMessages.filter(m =>
|
|
744
1356
|
m.recipient.toLowerCase() === name.toLowerCase() &&
|
|
745
1357
|
m.sender.toLowerCase() === config.user.toLowerCase() &&
|
|
746
1358
|
!m.read
|
|
747
1359
|
).length;
|
|
748
1360
|
|
|
749
|
-
// Also check if this agent has sent unread messages TO user
|
|
750
1361
|
const unreadFromAgent = allMessages.filter(m =>
|
|
751
1362
|
m.sender.toLowerCase() === name.toLowerCase() &&
|
|
752
1363
|
m.recipient.toLowerCase() === config.user.toLowerCase() &&
|
|
753
1364
|
!m.read
|
|
754
1365
|
).length;
|
|
755
1366
|
|
|
756
|
-
|
|
1367
|
+
const avatarState = avatarStates[name.toLowerCase()];
|
|
1368
|
+
const toolName = avatarState?.tool_name || null;
|
|
1369
|
+
|
|
757
1370
|
const sprite = group.children.find(c => c.isSprite);
|
|
758
1371
|
if (sprite) {
|
|
759
|
-
// Update the canvas texture - high DPI for crisp text
|
|
760
1372
|
const canvas = document.createElement('canvas');
|
|
761
1373
|
const context = canvas.getContext('2d');
|
|
762
1374
|
const scale = 2;
|
|
763
1375
|
canvas.width = 700;
|
|
764
|
-
canvas.height = 128;
|
|
1376
|
+
canvas.height = toolName ? 160 : 128;
|
|
765
1377
|
context.scale(scale, scale);
|
|
766
1378
|
|
|
767
|
-
// Background -
|
|
1379
|
+
// Background - themed colors for state
|
|
768
1380
|
if (unreadFromAgent > 0) {
|
|
769
|
-
|
|
770
|
-
context.fillStyle = 'rgba(220, 53, 69, 0.9)';
|
|
1381
|
+
context.fillStyle = 'rgba(220, 80, 80, 0.85)';
|
|
771
1382
|
} else if (unreadCount > 0) {
|
|
772
|
-
|
|
773
|
-
context.fillStyle = 'rgba(0, 123, 255, 0.9)';
|
|
1383
|
+
context.fillStyle = 'rgba(59, 130, 180, 0.85)';
|
|
774
1384
|
} else {
|
|
775
|
-
|
|
776
|
-
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
1385
|
+
context.fillStyle = 'rgba(20, 60, 60, 0.85)';
|
|
777
1386
|
}
|
|
778
|
-
context.roundRect(0, 0, 350, 64, 16);
|
|
1387
|
+
context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
|
|
779
1388
|
context.fill();
|
|
780
1389
|
|
|
781
|
-
//
|
|
782
|
-
context.
|
|
783
|
-
|
|
1390
|
+
// Border
|
|
1391
|
+
context.strokeStyle = unreadFromAgent > 0
|
|
1392
|
+
? 'rgba(255, 120, 120, 0.6)'
|
|
1393
|
+
: unreadCount > 0
|
|
1394
|
+
? 'rgba(100, 180, 255, 0.6)'
|
|
1395
|
+
: 'rgba(79, 209, 197, 0.3)';
|
|
1396
|
+
context.lineWidth = 1;
|
|
1397
|
+
context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
|
|
1398
|
+
context.stroke();
|
|
1399
|
+
|
|
1400
|
+
context.font = 'bold 22px Arial';
|
|
1401
|
+
context.fillStyle = '#e0f5f0';
|
|
784
1402
|
context.textAlign = 'center';
|
|
785
1403
|
context.textBaseline = 'middle';
|
|
786
1404
|
|
|
787
1405
|
if (unreadFromAgent > 0) {
|
|
788
|
-
|
|
789
|
-
context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175, 32);
|
|
1406
|
+
context.fillText(`${name} ${unreadFromAgent}`, 175, 24);
|
|
790
1407
|
} else if (unreadCount > 0) {
|
|
791
|
-
|
|
792
|
-
context.fillText(`${name} 📤 ${unreadCount}`, 175, 32);
|
|
1408
|
+
context.fillText(`${name} ${unreadCount}`, 175, 24);
|
|
793
1409
|
} else {
|
|
794
|
-
context.fillText(name, 175,
|
|
1410
|
+
context.fillText(name, 175, 24);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (toolName) {
|
|
1414
|
+
context.font = 'italic 14px Arial';
|
|
1415
|
+
context.fillStyle = '#4fd1c5';
|
|
1416
|
+
context.fillText(toolName, 175, 56);
|
|
795
1417
|
}
|
|
796
1418
|
|
|
797
1419
|
const texture = new THREE.CanvasTexture(canvas);
|
|
@@ -799,6 +1421,9 @@ function updateHouseLabels() {
|
|
|
799
1421
|
texture.magFilter = THREE.LinearFilter;
|
|
800
1422
|
sprite.material.map = texture;
|
|
801
1423
|
sprite.material.needsUpdate = true;
|
|
1424
|
+
|
|
1425
|
+
sprite.position.set(0, 6.5, 2);
|
|
1426
|
+
sprite.scale.set(7, toolName ? 2.2 : 1.8, 1);
|
|
802
1427
|
}
|
|
803
1428
|
});
|
|
804
1429
|
}
|