opencroc 1.8.1 → 1.8.2

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.
Files changed (71) hide show
  1. package/dist/cli/index.js +755 -8
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/index.d.ts +128 -1
  4. package/dist/index.js +548 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/web/dist/assets/main-Ccg3eDNK.js +1 -0
  7. package/dist/web/dist/assets/office-runtime-B3iNctxE.css +1 -0
  8. package/dist/web/dist/assets/office-runtime-BsCh82Pj.js +183 -0
  9. package/dist/web/dist/assets/pixel-page-3BYGm7dH.js +470 -0
  10. package/dist/web/dist/assets/react-vendor-C8RhVn0h.js +49 -0
  11. package/dist/web/dist/assets/studio-page-BInoyoV2.css +1 -0
  12. package/dist/web/dist/assets/studio-page-o3SCvE_v.js +351 -0
  13. package/dist/web/dist/assets/three-addons-BdrPp04O.js +470 -0
  14. package/dist/web/dist/assets/three-core-CsxM1PCY.js +4057 -0
  15. package/dist/web/dist/index.html +15 -0
  16. package/dist/web/index.html +11 -572
  17. package/dist/web/public/botreview/char_0.png +0 -0
  18. package/dist/web/public/botreview/char_1.png +0 -0
  19. package/dist/web/public/botreview/char_2.png +0 -0
  20. package/dist/web/public/botreview/coffee-machine.gif +0 -0
  21. package/dist/web/public/botreview/server.gif +0 -0
  22. package/dist/web/public/botreview/walls.png +0 -0
  23. package/dist/web/public/star/desk-v3.webp +0 -0
  24. package/dist/web/public/star/office_bg_small.webp +0 -0
  25. package/dist/web/public/star/star-idle-v5.png +0 -0
  26. package/dist/web/public/star/star-working-spritesheet-grid.webp +0 -0
  27. package/dist/web/src/app/AppLayout.tsx +34 -0
  28. package/dist/web/src/app/AppRouter.tsx +46 -0
  29. package/dist/web/src/app/bootstrap.tsx +22 -0
  30. package/dist/web/src/app/routes.tsx +52 -0
  31. package/dist/web/src/features/office/runtime/index.ts +1 -0
  32. package/dist/web/src/features/office/runtime/mount.ts +809 -0
  33. package/dist/web/src/features/pixel/runtime/index.ts +1 -0
  34. package/dist/web/src/features/pixel/runtime/mount.ts +728 -0
  35. package/dist/web/src/features/studio/runtime/index.ts +1 -0
  36. package/dist/web/src/features/studio/runtime/mount.ts +664 -0
  37. package/dist/web/src/features/three/engine/index.ts +1 -0
  38. package/dist/web/src/main.tsx +7 -0
  39. package/dist/web/src/pages/office/index.ts +1 -0
  40. package/dist/web/src/pages/office/page.tsx +283 -0
  41. package/dist/web/src/pages/pixel/index.ts +1 -0
  42. package/dist/web/src/pages/pixel/page.tsx +564 -0
  43. package/dist/web/src/pages/studio/index.ts +1 -0
  44. package/dist/web/src/pages/studio/page.tsx +446 -0
  45. package/dist/web/{js/agents.js → src/runtime/agents.ts} +304 -31
  46. package/dist/web/{js/camera.js → src/runtime/camera.ts} +12 -5
  47. package/dist/web/{js/dataviz.js → src/runtime/dataviz.ts} +38 -14
  48. package/dist/web/{js/effects.js → src/runtime/effects.ts} +139 -2
  49. package/dist/web/{js/engine.js → src/runtime/engine.ts} +45 -6
  50. package/dist/web/{js/office.js → src/runtime/office.ts} +136 -20
  51. package/dist/web/{js/ui.js → src/runtime/ui.ts} +11 -7
  52. package/dist/web/src/shared/assets.ts +4 -0
  53. package/dist/web/src/shared/navigation.ts +47 -0
  54. package/dist/web/src/styles/app-layout.css +19 -0
  55. package/dist/web/src/styles/office.css +268 -0
  56. package/dist/web/tsconfig.json +28 -0
  57. package/dist/web/vite.config.ts +93 -0
  58. package/package.json +11 -2
  59. package/dist/web/index-studio.html +0 -1644
  60. package/dist/web/index-v2-pixel.html +0 -1571
  61. /package/dist/web/{assets → dist}/botreview/char_0.png +0 -0
  62. /package/dist/web/{assets → dist}/botreview/char_1.png +0 -0
  63. /package/dist/web/{assets → dist}/botreview/char_2.png +0 -0
  64. /package/dist/web/{assets → dist}/botreview/coffee-machine.gif +0 -0
  65. /package/dist/web/{assets → dist}/botreview/server.gif +0 -0
  66. /package/dist/web/{assets → dist}/botreview/walls.png +0 -0
  67. /package/dist/web/{assets → dist}/star/desk-v3.webp +0 -0
  68. /package/dist/web/{assets → dist}/star/office_bg_small.webp +0 -0
  69. /package/dist/web/{assets → dist}/star/star-idle-v5.png +0 -0
  70. /package/dist/web/{assets → dist}/star/star-working-spritesheet-grid.webp +0 -0
  71. /package/dist/web/{js/state.js → src/runtime/state.ts} +0 -0
@@ -6,12 +6,24 @@
6
6
 
7
7
  import * as THREE from 'three';
8
8
  import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
9
- import { DESK_POSITIONS } from './office.js';
9
+ import { DESK_POSITIONS, POND_POSITIONS, setDeskOccupied } from './office';
10
10
 
11
11
  /* ─── Module state ─────────────────────────────────────────────────────────── */
12
12
  const agents = new Map(); // name → { group, parts, label, bubble, anim }
13
13
  let scene = null;
14
- let css2dRenderer = null;
14
+ let css2dRenderer = null;
15
+ let css2dResizeHandler = null;
16
+
17
+ function disposeObject3D(object) {
18
+ object?.traverse?.((child) => {
19
+ child.geometry?.dispose?.();
20
+ if (Array.isArray(child.material)) {
21
+ child.material.forEach((material) => material?.dispose?.());
22
+ } else {
23
+ child.material?.dispose?.();
24
+ }
25
+ });
26
+ }
15
27
 
16
28
  /* ─── Robot Colors per Role ────────────────────────────────────────────────── */
17
29
  const ROLE_COLORS = {
@@ -37,6 +49,17 @@ const STATUS_ANIM = {
37
49
  passed: { speed: 1.0, bobAmp: 0.05, rotSpeed: 0.2 },
38
50
  };
39
51
 
52
+ const ACTIVE_STATUSES = new Set([
53
+ 'working',
54
+ 'testing',
55
+ 'thinking',
56
+ 'scanning',
57
+ 'navigating',
58
+ 'interacting',
59
+ 'asserting',
60
+ 'reporting',
61
+ ]);
62
+
40
63
  /* ═══════════════════════════════════════════════════════════════════════════════
41
64
  AgentManager — Creates, updates, removes 3D robot agents
42
65
  ═══════════════════════════════════════════════════════════════════════════════ */
@@ -45,12 +68,17 @@ export class AgentManager {
45
68
  scene = sceneRef;
46
69
  this._time = 0;
47
70
  this._bubbleTimers = new Map();
71
+ this._deskAssignments = new Map();
72
+ this._eventAssignments = new Map();
48
73
  this._initCSS2D();
49
74
  }
50
75
 
51
- /** Initialize CSS2D renderer for labels and bubbles */
52
- _initCSS2D() {
53
- css2dRenderer = new CSS2DRenderer();
76
+ /** Initialize CSS2D renderer for labels and bubbles */
77
+ _initCSS2D() {
78
+ if (css2dRenderer?.domElement?.parentNode) {
79
+ css2dRenderer.domElement.parentNode.removeChild(css2dRenderer.domElement);
80
+ }
81
+ css2dRenderer = new CSS2DRenderer();
54
82
  css2dRenderer.setSize(window.innerWidth, window.innerHeight);
55
83
  css2dRenderer.domElement.style.position = 'fixed';
56
84
  css2dRenderer.domElement.style.top = '0';
@@ -59,14 +87,16 @@ export class AgentManager {
59
87
  css2dRenderer.domElement.style.zIndex = '5';
60
88
  document.body.appendChild(css2dRenderer.domElement);
61
89
 
62
- window.addEventListener('resize', () => {
63
- css2dRenderer.setSize(window.innerWidth, window.innerHeight);
64
- });
65
- }
90
+ css2dResizeHandler = () => {
91
+ css2dRenderer.setSize(window.innerWidth, window.innerHeight);
92
+ };
93
+ window.addEventListener('resize', css2dResizeHandler);
94
+ }
66
95
 
67
96
  /** Sync agents from backend data */
68
97
  sync(agentData) {
69
98
  const current = new Set();
99
+ const active = new Set();
70
100
 
71
101
  agentData.forEach((a, i) => {
72
102
  current.add(a.name);
@@ -74,11 +104,51 @@ export class AgentManager {
74
104
  this._createRobot(a, i, agentData.length);
75
105
  }
76
106
  this._updateStatus(a.name, a.status);
107
+ const eventActive = this._eventAssignments.get(a.name);
108
+ const isActive = typeof eventActive === 'boolean' ? eventActive : this._isActiveStatus(a.status);
109
+ if (isActive) active.add(a.name);
110
+ });
111
+
112
+ // Drop stale event-assignment flags for removed agents.
113
+ for (const name of this._eventAssignments.keys()) {
114
+ if (!current.has(name)) this._eventAssignments.delete(name);
115
+ }
116
+
117
+ // Release desks for idle/done agents.
118
+ for (const [name, deskIdx] of this._deskAssignments) {
119
+ if (!current.has(name) || !active.has(name)) {
120
+ this._deskAssignments.delete(name);
121
+ }
122
+ }
123
+
124
+ // Assign desks to active agents that don't have one yet.
125
+ active.forEach((name) => {
126
+ if (!this._deskAssignments.has(name)) {
127
+ const deskIdx = this._nextFreeDesk();
128
+ if (deskIdx >= 0) this._deskAssignments.set(name, deskIdx);
129
+ }
130
+ });
131
+
132
+ this._syncDeskOccupancy();
133
+
134
+ // Update movement targets by current allocation.
135
+ current.forEach((name) => {
136
+ const agent = agents.get(name);
137
+ if (!agent) return;
138
+ const deskIdx = this._deskAssignments.get(name);
139
+ if (typeof deskIdx === 'number' && DESK_POSITIONS[deskIdx]) {
140
+ const desk = DESK_POSITIONS[deskIdx];
141
+ this._setTarget(agent, desk.x, desk.z + 1.2, 'desk', desk);
142
+ } else {
143
+ const pond = POND_POSITIONS[agent.pondSlot % Math.max(1, POND_POSITIONS.length)] || { x: -9, z: 6.2 };
144
+ this._setTarget(agent, pond.x, pond.z, 'pond');
145
+ }
77
146
  });
78
147
 
79
148
  // Remove stale agents
80
149
  for (const [name] of agents) {
81
150
  if (!current.has(name)) {
151
+ this._deskAssignments.delete(name);
82
152
  this._removeRobot(name);
83
153
  }
84
154
  }
@@ -87,6 +157,89 @@ export class AgentManager {
87
157
  this._scheduleBubbles(agentData);
88
158
  }
89
159
 
160
+ applyAssignmentEvent(payload) {
161
+ const name = payload?.name;
162
+ if (!name || !agents.has(name)) return null;
163
+
164
+ this._eventAssignments.set(name, true);
165
+ if (!this._deskAssignments.has(name)) {
166
+ const deskIdx = this._nextFreeDesk();
167
+ if (deskIdx >= 0) this._deskAssignments.set(name, deskIdx);
168
+ }
169
+
170
+ const agent = agents.get(name);
171
+ const deskIdx = this._deskAssignments.get(name);
172
+ if (!agent || typeof deskIdx !== 'number' || !DESK_POSITIONS[deskIdx]) return null;
173
+
174
+ const desk = DESK_POSITIONS[deskIdx];
175
+ const from = { x: agent.baseX, z: agent.baseZ };
176
+ this._setTarget(agent, desk.x, desk.z + 1.2, 'desk', desk);
177
+ this._syncDeskOccupancy();
178
+ this._flashSummon(name);
179
+
180
+ return { from, to: { x: desk.x, z: desk.z + 1.2 }, kind: 'assigned' };
181
+ }
182
+
183
+ applyReleaseEvent(payload) {
184
+ const name = payload?.name;
185
+ if (!name || !agents.has(name)) return null;
186
+
187
+ this._eventAssignments.set(name, false);
188
+ this._deskAssignments.delete(name);
189
+
190
+ const agent = agents.get(name);
191
+ if (!agent) return null;
192
+
193
+ const pond = POND_POSITIONS[agent.pondSlot % Math.max(1, POND_POSITIONS.length)] || { x: -9, z: 6.2 };
194
+ const from = { x: agent.baseX, z: agent.baseZ };
195
+ this._setTarget(agent, pond.x, pond.z, 'pond');
196
+ this._syncDeskOccupancy();
197
+
198
+ return { from, to: { x: pond.x, z: pond.z }, kind: 'released' };
199
+ }
200
+
201
+ _flashSummon(name) {
202
+ const agent = agents.get(name);
203
+ if (!agent) return;
204
+
205
+ // Brief glow spike
206
+ if (agent.parts.glow) {
207
+ const prev = agent.parts.glow.intensity;
208
+ agent.parts.glow.intensity = 1.9;
209
+ setTimeout(() => {
210
+ const a = agents.get(name);
211
+ if (a && a.parts.glow) a.parts.glow.intensity = prev;
212
+ }, 700);
213
+ }
214
+
215
+ // Expanding ring at robot feet (blue for assignment)
216
+ const ringGeo = new THREE.TorusGeometry(0.3, 0.04, 8, 20);
217
+ const ringMat = new THREE.MeshBasicMaterial({
218
+ color: 0x60a5fa, transparent: true, opacity: 0.88, depthWrite: false,
219
+ });
220
+ const ring = new THREE.Mesh(ringGeo, ringMat);
221
+ ring.rotation.x = -Math.PI / 2;
222
+ ring.position.set(agent.baseX, 0.24, agent.baseZ);
223
+ scene.add(ring);
224
+
225
+ let life = 0;
226
+ const ttl = 0.78;
227
+ const tick = () => {
228
+ life += 0.016;
229
+ ring.position.set(agent.baseX, 0.24, agent.baseZ);
230
+ ring.scale.setScalar(1 + (life / ttl) * 4.5);
231
+ ring.material.opacity = Math.max(0, 0.88 * (1 - life / ttl));
232
+ if (life < ttl) {
233
+ requestAnimationFrame(tick);
234
+ } else {
235
+ scene.remove(ring);
236
+ ringGeo.dispose();
237
+ ringMat.dispose();
238
+ }
239
+ };
240
+ requestAnimationFrame(tick);
241
+ }
242
+
90
243
  /** Update all agents each frame */
91
244
  update(dt) {
92
245
  this._time += dt;
@@ -94,6 +247,31 @@ export class AgentManager {
94
247
  for (const [name, agent] of agents) {
95
248
  const anim = STATUS_ANIM[agent.status] || STATUS_ANIM.idle;
96
249
 
250
+ let moveTargetX = agent.targetX;
251
+ let moveTargetZ = agent.targetZ;
252
+ if (agent.path.length) {
253
+ moveTargetX = agent.path[0].x;
254
+ moveTargetZ = agent.path[0].z;
255
+ }
256
+
257
+ // Move toward target zone.
258
+ const dx = moveTargetX - agent.baseX;
259
+ const dz = moveTargetZ - agent.baseZ;
260
+ const dist = Math.hypot(dx, dz);
261
+ if (dist > 0.01) {
262
+ const speed = agent.zone === 'desk' ? 4.2 : 2.6;
263
+ const step = Math.min(1, (dt * speed) / dist);
264
+ agent.baseX += dx * step;
265
+ agent.baseZ += dz * step;
266
+ agent.group.lookAt(new THREE.Vector3(moveTargetX, 0.2, moveTargetZ));
267
+ } else if (agent.path.length) {
268
+ agent.path.shift();
269
+ } else if (agent.zone === 'desk' && agent.deskPos) {
270
+ agent.group.lookAt(new THREE.Vector3(agent.deskPos.x, 0.2, agent.deskPos.z));
271
+ } else if (agent.zone === 'pond') {
272
+ agent.group.lookAt(new THREE.Vector3(-9, 0.2, 6.2));
273
+ }
274
+
97
275
  // Bobbing
98
276
  const bobY = Math.sin(this._time * anim.speed * 2) * anim.bobAmp;
99
277
  agent.group.position.y = agent.baseY + bobY;
@@ -147,7 +325,15 @@ export class AgentManager {
147
325
  ═════════════════════════════════════════════════════════════════════════ */
148
326
  _createRobot(agentData, index, total) {
149
327
  const role = agentData.role || 'parser';
150
- const colors = ROLE_COLORS[role] || DEFAULT_COLORS;
328
+ // Support dynamic color from server (hex string like '#60a5fa')
329
+ let colors = ROLE_COLORS[role] || DEFAULT_COLORS;
330
+ if (agentData.color && !ROLE_COLORS[role]) {
331
+ const hex = parseInt(agentData.color.replace('#', ''), 16);
332
+ if (!isNaN(hex)) {
333
+ const lighter = new THREE.Color(hex).lerp(new THREE.Color(0xffffff), 0.35).getHex();
334
+ colors = { body: hex, accent: hex, eye: lighter, glow: hex };
335
+ }
336
+ }
151
337
  const group = new THREE.Group();
152
338
  group.name = `agent-${agentData.name}`;
153
339
 
@@ -311,14 +497,13 @@ export class AgentManager {
311
497
  group.add(shadow);
312
498
 
313
499
  /* ── Position ────────────────────────────────────────────────────────── */
314
- const desk = DESK_POSITIONS[index] || { x: index * 3 - 6, z: 0 };
315
- const x = desk.x;
316
- const z = desk.z + 1.2; // Position in front of desk (in chair)
500
+ const pondSlot = index % Math.max(1, POND_POSITIONS.length);
501
+ const pond = POND_POSITIONS[pondSlot] || { x: -9, z: 6.2 };
502
+ const x = pond.x;
503
+ const z = pond.z;
317
504
 
318
505
  group.position.set(x, 0.2, z);
319
-
320
- // Point toward desk
321
- group.lookAt(new THREE.Vector3(desk.x, 0.2, desk.z));
506
+ group.lookAt(new THREE.Vector3(-9, 0.2, 6.2));
322
507
 
323
508
  scene.add(group);
324
509
 
@@ -346,7 +531,12 @@ export class AgentManager {
346
531
  baseX: x,
347
532
  baseY: 0.2,
348
533
  baseZ: z,
349
- deskPos: desk,
534
+ deskPos: null,
535
+ pondSlot,
536
+ zone: 'pond',
537
+ targetX: x,
538
+ targetZ: z,
539
+ path: [],
350
540
  });
351
541
  }
352
542
 
@@ -369,14 +559,72 @@ export class AgentManager {
369
559
  }
370
560
  }
371
561
 
562
+ _isActiveStatus(status) {
563
+ return ACTIVE_STATUSES.has(status || 'idle');
564
+ }
565
+
566
+ _nextFreeDesk() {
567
+ const used = new Set(this._deskAssignments.values());
568
+ for (let i = 0; i < DESK_POSITIONS.length; i++) {
569
+ if (!used.has(i)) return i;
570
+ }
571
+ return -1;
572
+ }
573
+
574
+ _setTarget(agent, x, z, zone, deskPos = null) {
575
+ const changed = zone !== agent.zone || Math.hypot(agent.targetX - x, agent.targetZ - z) > 0.06;
576
+ if (!changed) {
577
+ agent.deskPos = deskPos;
578
+ return;
579
+ }
580
+
581
+ agent.targetX = x;
582
+ agent.targetZ = z;
583
+ agent.zone = zone;
584
+ agent.deskPos = deskPos;
585
+ agent.path = this._buildPath(agent, x, z, zone);
586
+ }
587
+
588
+ _buildPath(agent, targetX, targetZ, zone) {
589
+ const path = [];
590
+ const corridorZ = 2.6;
591
+ const pondGateX = -6.4;
592
+
593
+ const nearCorridor = Math.abs(agent.baseZ - corridorZ) < 0.6;
594
+ if (!nearCorridor) {
595
+ path.push({ x: agent.baseX, z: corridorZ });
596
+ }
597
+
598
+ if (zone === 'desk') {
599
+ path.push({ x: targetX, z: corridorZ });
600
+ path.push({ x: targetX, z: targetZ });
601
+ return path;
602
+ }
603
+
604
+ path.push({ x: pondGateX, z: corridorZ + 1.1 });
605
+ path.push({ x: targetX, z: targetZ });
606
+ return path;
607
+ }
608
+
609
+ _syncDeskOccupancy() {
610
+ for (let i = 0; i < DESK_POSITIONS.length; i++) {
611
+ setDeskOccupied(i, false);
612
+ }
613
+ for (const deskIdx of this._deskAssignments.values()) {
614
+ setDeskOccupied(deskIdx, true);
615
+ }
616
+ }
617
+
372
618
  /* ═════════════════════════════════════════════════════════════════════════
373
619
  Remove Robot
374
620
  ═════════════════════════════════════════════════════════════════════════ */
375
- _removeRobot(name) {
376
- const agent = agents.get(name);
377
- if (!agent) return;
378
- scene.remove(agent.group);
379
- agents.delete(name);
621
+ _removeRobot(name) {
622
+ const agent = agents.get(name);
623
+ if (!agent) return;
624
+ scene.remove(agent.group);
625
+ disposeObject3D(agent.group);
626
+ agents.delete(name);
627
+ this._syncDeskOccupancy();
380
628
 
381
629
  // Clean bubble timer
382
630
  const bt = this._bubbleTimers.get(name);
@@ -427,7 +675,7 @@ export class AgentManager {
427
675
  }
428
676
  }
429
677
 
430
- _showBubble(name, text) {
678
+ _showBubble(name, text) {
431
679
  const agent = agents.get(name);
432
680
  if (!agent) return;
433
681
 
@@ -446,14 +694,39 @@ export class AgentManager {
446
694
  agent.bubbleObj = bubble;
447
695
 
448
696
  // Remove after 3 seconds
449
- setTimeout(() => {
450
- if (agent.bubbleObj === bubble) {
451
- agent.group.remove(bubble);
452
- agent.bubbleObj = null;
453
- }
454
- }, 3000);
455
- }
456
- }
697
+ setTimeout(() => {
698
+ if (agent.bubbleObj === bubble) {
699
+ agent.group.remove(bubble);
700
+ agent.bubbleObj = null;
701
+ }
702
+ }, 3000);
703
+ }
704
+
705
+ dispose() {
706
+ for (const timer of this._bubbleTimers.values()) {
707
+ clearTimeout(timer);
708
+ }
709
+ this._bubbleTimers.clear();
710
+ this._deskAssignments.clear();
711
+ this._eventAssignments.clear();
712
+
713
+ for (const name of [...agents.keys()]) {
714
+ this._removeRobot(name);
715
+ }
716
+
717
+ if (css2dResizeHandler) {
718
+ window.removeEventListener('resize', css2dResizeHandler);
719
+ css2dResizeHandler = null;
720
+ }
721
+
722
+ if (css2dRenderer?.domElement?.parentNode) {
723
+ css2dRenderer.domElement.parentNode.removeChild(css2dRenderer.domElement);
724
+ }
725
+
726
+ css2dRenderer = null;
727
+ scene = null;
728
+ }
729
+ }
457
730
 
458
731
  /* ═══════════════════════════════════════════════════════════════════════════════
459
732
  Helper exports
@@ -118,8 +118,15 @@ export class CameraController {
118
118
  this.controls.autoRotate = false;
119
119
  }
120
120
 
121
- /** Get the raw controls for external use */
122
- getControls() {
123
- return this.controls;
124
- }
125
- }
121
+ /** Get the raw controls for external use */
122
+ getControls() {
123
+ return this.controls;
124
+ }
125
+
126
+ dispose() {
127
+ this.controls?.dispose?.();
128
+ if (this.scene?.userData?.camera === this.camera) {
129
+ delete this.scene.userData.camera;
130
+ }
131
+ }
132
+ }
@@ -4,7 +4,18 @@
4
4
  ~2000 lines
5
5
  ═══════════════════════════════════════════════════════════════════════════════ */
6
6
 
7
- import * as THREE from 'three';
7
+ import * as THREE from 'three';
8
+
9
+ function disposeObject3D(object) {
10
+ object?.traverse?.((child) => {
11
+ child.geometry?.dispose?.();
12
+ if (Array.isArray(child.material)) {
13
+ child.material.forEach((material) => material?.dispose?.());
14
+ } else {
15
+ child.material?.dispose?.();
16
+ }
17
+ });
18
+ }
8
19
 
9
20
  /* ─── Color Map per module ─────────────────────────────────────────────────── */
10
21
  const MOD_COLORS = [
@@ -125,10 +136,18 @@ export class GraphViz {
125
136
  }
126
137
  }
127
138
 
128
- /** Show/hide the graph */
129
- show() { this.group.visible = true; }
130
- hide() { this.group.visible = false; }
131
- }
139
+ /** Show/hide the graph */
140
+ show() { this.group.visible = true; }
141
+ hide() { this.group.visible = false; }
142
+
143
+ dispose() {
144
+ this.scene.remove(this.group);
145
+ disposeObject3D(this.group);
146
+ this._nodes.clear();
147
+ this._edges = [];
148
+ this._modules.clear();
149
+ }
150
+ }
132
151
 
133
152
  /* ═══════════════════════════════════════════════════════════════════════════════
134
153
  HologramDisplay — Central floating data display
@@ -250,7 +269,7 @@ export class HologramDisplay {
250
269
  }
251
270
 
252
271
  /** Update each frame */
253
- update(dt, graphData) {
272
+ update(dt, graphData) {
254
273
  this._time += dt;
255
274
 
256
275
  // Rotate elements
@@ -278,11 +297,16 @@ export class HologramDisplay {
278
297
  this.holoLight.intensity = 0.6 + 0.4 * Math.sin(this._time * 2);
279
298
 
280
299
  // Update sphere opacity based on data amount
281
- if (graphData && graphData.nodes) {
282
- const nodeCount = graphData.nodes.length;
283
- const t = Math.min(nodeCount / 100, 1);
284
- this.sphere.material.opacity = 0.1 + t * 0.2;
285
- this.innerSphere.material.opacity = 0.08 + t * 0.15;
286
- }
287
- }
288
- }
300
+ if (graphData && graphData.nodes) {
301
+ const nodeCount = graphData.nodes.length;
302
+ const t = Math.min(nodeCount / 100, 1);
303
+ this.sphere.material.opacity = 0.1 + t * 0.2;
304
+ this.innerSphere.material.opacity = 0.08 + t * 0.15;
305
+ }
306
+ }
307
+
308
+ dispose() {
309
+ this.scene.remove(this.group);
310
+ disposeObject3D(this.group);
311
+ }
312
+ }
@@ -3,7 +3,18 @@
3
3
  ~2000 lines
4
4
  ═══════════════════════════════════════════════════════════════════════════════ */
5
5
 
6
- import * as THREE from 'three';
6
+ import * as THREE from 'three';
7
+
8
+ function disposeObject3D(object) {
9
+ object?.traverse?.((child) => {
10
+ child.geometry?.dispose?.();
11
+ if (Array.isArray(child.material)) {
12
+ child.material.forEach((material) => material?.dispose?.());
13
+ } else {
14
+ child.material?.dispose?.();
15
+ }
16
+ });
17
+ }
7
18
 
8
19
  /* ═══════════════════════════════════════════════════════════════════════════════
9
20
  ParticleManager
@@ -342,4 +353,130 @@ export class ParticleManager {
342
353
 
343
354
  this.systems.push(sys);
344
355
  }
345
- }
356
+
357
+ triggerAgentTransfer(from, to, kind = 'assigned') {
358
+ if (!from || !to) return;
359
+
360
+ const startColor = kind === 'released' ? 0xfbbf24 : 0x60a5fa;
361
+ const endColor = kind === 'released' ? 0x34d399 : 0x22d3ee;
362
+
363
+ this._triggerPulse(from.x, 0.34, from.z, startColor, kind === 'released' ? 0.8 : 1.1);
364
+ this._triggerPulse(to.x, 0.34, to.z, endColor, kind === 'released' ? 1.2 : 0.9, 180);
365
+
366
+ // Expanding ripple at the pond side (departure or arrival).
367
+ const pondSide = kind === 'assigned' ? from : to;
368
+ if (pondSide.x != null) this.triggerPondRipple(pondSide.x, pondSide.z ?? 6.2);
369
+ }
370
+
371
+ triggerPondRipple(x, z) {
372
+ const geo = new THREE.RingGeometry(0.18, 0.32, 24);
373
+ const mat = new THREE.MeshBasicMaterial({
374
+ color: 0x0ea5e9,
375
+ transparent: true,
376
+ opacity: 0.65,
377
+ side: THREE.DoubleSide,
378
+ depthWrite: false,
379
+ });
380
+ const ring = new THREE.Mesh(geo, mat);
381
+ ring.rotation.x = -Math.PI / 2;
382
+ ring.position.set(x, 0.31, z);
383
+ this.scene.add(ring);
384
+
385
+ let life = 0;
386
+ const sys = {
387
+ mesh: ring,
388
+ update: (dt) => {
389
+ life += dt;
390
+ if (life >= 1.1) {
391
+ this.scene.remove(ring);
392
+ geo.dispose();
393
+ mat.dispose();
394
+ const idx = this.systems.indexOf(sys);
395
+ if (idx >= 0) this.systems.splice(idx, 1);
396
+ return;
397
+ }
398
+ ring.scale.setScalar(1 + life * 3.2);
399
+ ring.material.opacity = Math.max(0, 0.65 * (1 - life / 1.1));
400
+ },
401
+ };
402
+ this.systems.push(sys);
403
+ }
404
+
405
+ _triggerPulse(x, y, z, colorHex, lift = 1, delayMs = 0) {
406
+ const spawn = () => {
407
+ const count = 38;
408
+ const positions = new Float32Array(count * 3);
409
+ const velocities = new Float32Array(count * 3);
410
+
411
+ for (let i = 0; i < count; i++) {
412
+ positions[i * 3] = x;
413
+ positions[i * 3 + 1] = y;
414
+ positions[i * 3 + 2] = z;
415
+
416
+ const a = (i / count) * Math.PI * 2;
417
+ const speed = 0.8 + Math.random() * 1.4;
418
+ velocities[i * 3] = Math.cos(a) * speed;
419
+ velocities[i * 3 + 1] = 0.8 + Math.random() * lift;
420
+ velocities[i * 3 + 2] = Math.sin(a) * speed;
421
+ }
422
+
423
+ const geo = new THREE.BufferGeometry();
424
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
425
+ const mat = new THREE.PointsMaterial({
426
+ size: 0.09,
427
+ color: colorHex,
428
+ transparent: true,
429
+ opacity: 0.92,
430
+ blending: THREE.AdditiveBlending,
431
+ depthWrite: false,
432
+ });
433
+ const points = new THREE.Points(geo, mat);
434
+ this.scene.add(points);
435
+
436
+ let life = 0;
437
+ const ttl = 0.68;
438
+ const sys = {
439
+ mesh: points,
440
+ update: (dt) => {
441
+ life += dt;
442
+ if (life >= ttl) {
443
+ this.scene.remove(points);
444
+ geo.dispose();
445
+ mat.dispose();
446
+ const idx = this.systems.indexOf(sys);
447
+ if (idx >= 0) this.systems.splice(idx, 1);
448
+ return;
449
+ }
450
+
451
+ const arr = geo.attributes.position.array;
452
+ for (let i = 0; i < count; i++) {
453
+ arr[i * 3] += velocities[i * 3] * dt;
454
+ arr[i * 3 + 1] += velocities[i * 3 + 1] * dt;
455
+ arr[i * 3 + 2] += velocities[i * 3 + 2] * dt;
456
+ velocities[i * 3 + 1] -= 2.8 * dt;
457
+ }
458
+ geo.attributes.position.needsUpdate = true;
459
+ mat.opacity = Math.max(0, 0.92 - (life / ttl) * 0.92);
460
+ },
461
+ };
462
+
463
+ this.systems.push(sys);
464
+ };
465
+
466
+ if (delayMs > 0) {
467
+ setTimeout(spawn, delayMs);
468
+ } else {
469
+ spawn();
470
+ }
471
+ }
472
+
473
+ dispose() {
474
+ for (const system of this.systems) {
475
+ if (system.mesh) {
476
+ this.scene.remove(system.mesh);
477
+ disposeObject3D(system.mesh);
478
+ }
479
+ }
480
+ this.systems = [];
481
+ }
482
+ }