opencroc 1.8.1 → 1.8.3

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 (37) 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/package.json +11 -2
  17. package/dist/web/index-studio.html +0 -1644
  18. package/dist/web/index-v2-pixel.html +0 -1571
  19. package/dist/web/index.html +0 -573
  20. package/dist/web/js/agents.js +0 -465
  21. package/dist/web/js/camera.js +0 -125
  22. package/dist/web/js/dataviz.js +0 -288
  23. package/dist/web/js/effects.js +0 -345
  24. package/dist/web/js/engine.js +0 -489
  25. package/dist/web/js/office.js +0 -816
  26. package/dist/web/js/state.js +0 -37
  27. package/dist/web/js/ui.js +0 -384
  28. /package/dist/web/{assets → dist}/botreview/char_0.png +0 -0
  29. /package/dist/web/{assets → dist}/botreview/char_1.png +0 -0
  30. /package/dist/web/{assets → dist}/botreview/char_2.png +0 -0
  31. /package/dist/web/{assets → dist}/botreview/coffee-machine.gif +0 -0
  32. /package/dist/web/{assets → dist}/botreview/server.gif +0 -0
  33. /package/dist/web/{assets → dist}/botreview/walls.png +0 -0
  34. /package/dist/web/{assets → dist}/star/desk-v3.webp +0 -0
  35. /package/dist/web/{assets → dist}/star/office_bg_small.webp +0 -0
  36. /package/dist/web/{assets → dist}/star/star-idle-v5.png +0 -0
  37. /package/dist/web/{assets → dist}/star/star-working-spritesheet-grid.webp +0 -0
@@ -1,465 +0,0 @@
1
- /* ═══════════════════════════════════════════════════════════════════════════════
2
- OpenCroc Studio 3D — Agent Robot Characters
3
- Low-poly robot agents built from Three.js primitives
4
- ~2500 lines
5
- ═══════════════════════════════════════════════════════════════════════════════ */
6
-
7
- import * as THREE from 'three';
8
- import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
9
- import { DESK_POSITIONS } from './office.js';
10
-
11
- /* ─── Module state ─────────────────────────────────────────────────────────── */
12
- const agents = new Map(); // name → { group, parts, label, bubble, anim }
13
- let scene = null;
14
- let css2dRenderer = null;
15
-
16
- /* ─── Robot Colors per Role ────────────────────────────────────────────────── */
17
- const ROLE_COLORS = {
18
- parser: { body: 0x60a5fa, accent: 0x3b82f6, eye: 0xdbeafe, glow: 0x60a5fa },
19
- analyzer: { body: 0xa78bfa, accent: 0x8b5cf6, eye: 0xede9fe, glow: 0xa78bfa },
20
- tester: { body: 0x34d399, accent: 0x10b981, eye: 0xd1fae5, glow: 0x34d399 },
21
- healer: { body: 0xfbbf24, accent: 0xf59e0b, eye: 0xfef3c7, glow: 0xfbbf24 },
22
- planner: { body: 0xf472b6, accent: 0xec4899, eye: 0xfce7f3, glow: 0xf472b6 },
23
- reporter: { body: 0x22d3ee, accent: 0x06b6d4, eye: 0xcffafe, glow: 0x22d3ee },
24
- };
25
-
26
- const DEFAULT_COLORS = { body: 0x94a3b8, accent: 0x64748b, eye: 0xf1f5f9, glow: 0x94a3b8 };
27
-
28
- /* ─── Animation parameters per status ─────────────────────────────────────── */
29
- const STATUS_ANIM = {
30
- idle: { speed: 0.5, bobAmp: 0.03, rotSpeed: 0 },
31
- working: { speed: 2.0, bobAmp: 0.06, rotSpeed: 0.5 },
32
- testing: { speed: 2.5, bobAmp: 0.08, rotSpeed: 0.8 },
33
- thinking: { speed: 1.0, bobAmp: 0.02, rotSpeed: 0.3 },
34
- error: { speed: 4.0, bobAmp: 0.04, rotSpeed: 0, shake: true },
35
- failed: { speed: 4.0, bobAmp: 0.04, rotSpeed: 0, shake: true },
36
- done: { speed: 1.0, bobAmp: 0.05, rotSpeed: 0.2 },
37
- passed: { speed: 1.0, bobAmp: 0.05, rotSpeed: 0.2 },
38
- };
39
-
40
- /* ═══════════════════════════════════════════════════════════════════════════════
41
- AgentManager — Creates, updates, removes 3D robot agents
42
- ═══════════════════════════════════════════════════════════════════════════════ */
43
- export class AgentManager {
44
- constructor(sceneRef) {
45
- scene = sceneRef;
46
- this._time = 0;
47
- this._bubbleTimers = new Map();
48
- this._initCSS2D();
49
- }
50
-
51
- /** Initialize CSS2D renderer for labels and bubbles */
52
- _initCSS2D() {
53
- css2dRenderer = new CSS2DRenderer();
54
- css2dRenderer.setSize(window.innerWidth, window.innerHeight);
55
- css2dRenderer.domElement.style.position = 'fixed';
56
- css2dRenderer.domElement.style.top = '0';
57
- css2dRenderer.domElement.style.left = '0';
58
- css2dRenderer.domElement.style.pointerEvents = 'none';
59
- css2dRenderer.domElement.style.zIndex = '5';
60
- document.body.appendChild(css2dRenderer.domElement);
61
-
62
- window.addEventListener('resize', () => {
63
- css2dRenderer.setSize(window.innerWidth, window.innerHeight);
64
- });
65
- }
66
-
67
- /** Sync agents from backend data */
68
- sync(agentData) {
69
- const current = new Set();
70
-
71
- agentData.forEach((a, i) => {
72
- current.add(a.name);
73
- if (!agents.has(a.name)) {
74
- this._createRobot(a, i, agentData.length);
75
- }
76
- this._updateStatus(a.name, a.status);
77
- });
78
-
79
- // Remove stale agents
80
- for (const [name] of agents) {
81
- if (!current.has(name)) {
82
- this._removeRobot(name);
83
- }
84
- }
85
-
86
- // Schedule bubbles
87
- this._scheduleBubbles(agentData);
88
- }
89
-
90
- /** Update all agents each frame */
91
- update(dt) {
92
- this._time += dt;
93
-
94
- for (const [name, agent] of agents) {
95
- const anim = STATUS_ANIM[agent.status] || STATUS_ANIM.idle;
96
-
97
- // Bobbing
98
- const bobY = Math.sin(this._time * anim.speed * 2) * anim.bobAmp;
99
- agent.group.position.y = agent.baseY + bobY;
100
-
101
- // Arm rotation (working animation)
102
- if (agent.parts.leftArm) {
103
- agent.parts.leftArm.rotation.x = Math.sin(this._time * anim.speed) * 0.3;
104
- }
105
- if (agent.parts.rightArm) {
106
- agent.parts.rightArm.rotation.x = -Math.sin(this._time * anim.speed) * 0.3;
107
- }
108
-
109
- // Head rotation (thinking)
110
- if (agent.parts.head && anim.rotSpeed > 0) {
111
- agent.parts.head.rotation.y = Math.sin(this._time * anim.rotSpeed) * 0.2;
112
- }
113
-
114
- // Shake effect (error)
115
- if (anim.shake) {
116
- agent.group.position.x = agent.baseX + Math.sin(this._time * 30) * 0.04;
117
- agent.group.position.z = agent.baseZ + Math.cos(this._time * 25) * 0.02;
118
- } else {
119
- agent.group.position.x = agent.baseX;
120
- agent.group.position.z = agent.baseZ;
121
- }
122
-
123
- // Eye glow pulsing
124
- if (agent.parts.leftEye && agent.parts.rightEye) {
125
- const eyePulse = 0.5 + 0.5 * Math.sin(this._time * anim.speed * 1.5);
126
- agent.parts.leftEye.material.opacity = 0.6 + eyePulse * 0.4;
127
- agent.parts.rightEye.material.opacity = 0.6 + eyePulse * 0.4;
128
- }
129
-
130
- // Antenna glow
131
- if (agent.parts.antenna) {
132
- const glow = 0.3 + 0.7 * Math.abs(Math.sin(this._time * 3));
133
- agent.parts.antenna.material.emissiveIntensity = glow;
134
- }
135
- }
136
-
137
- // Update CSS2D renderer
138
- if (css2dRenderer && scene) {
139
- // find camera from scene parent
140
- const camera = scene.userData.camera;
141
- if (camera) css2dRenderer.render(scene, camera);
142
- }
143
- }
144
-
145
- /* ═════════════════════════════════════════════════════════════════════════
146
- Robot Construction — Build a low-poly robot from primitives
147
- ═════════════════════════════════════════════════════════════════════════ */
148
- _createRobot(agentData, index, total) {
149
- const role = agentData.role || 'parser';
150
- const colors = ROLE_COLORS[role] || DEFAULT_COLORS;
151
- const group = new THREE.Group();
152
- group.name = `agent-${agentData.name}`;
153
-
154
- const parts = {};
155
-
156
- // Materials
157
- const bodyMat = new THREE.MeshStandardMaterial({
158
- color: colors.body, roughness: 0.4, metalness: 0.5,
159
- });
160
- const accentMat = new THREE.MeshStandardMaterial({
161
- color: colors.accent, roughness: 0.3, metalness: 0.6,
162
- });
163
- const eyeMat = new THREE.MeshBasicMaterial({
164
- color: colors.eye, transparent: true, opacity: 0.9,
165
- });
166
- const metalMat = new THREE.MeshStandardMaterial({
167
- color: 0x94a3b8, roughness: 0.2, metalness: 0.8,
168
- });
169
- const glowMat = new THREE.MeshStandardMaterial({
170
- color: colors.glow, roughness: 0.3, metalness: 0.4,
171
- emissive: colors.glow, emissiveIntensity: 0.5,
172
- });
173
-
174
- /* ── Body (torso) ────────────────────────────────────────────────────── */
175
- const bodyGeo = new THREE.BoxGeometry(0.5, 0.6, 0.35);
176
- const body = new THREE.Mesh(bodyGeo, bodyMat);
177
- body.position.y = 0.9;
178
- body.castShadow = true;
179
- group.add(body);
180
- parts.body = body;
181
-
182
- // Chest plate
183
- const chestGeo = new THREE.BoxGeometry(0.35, 0.35, 0.02);
184
- const chest = new THREE.Mesh(chestGeo, accentMat);
185
- chest.position.set(0, 0.95, 0.19);
186
- group.add(chest);
187
-
188
- // Chest LED
189
- const ledGeo = new THREE.CircleGeometry(0.04, 8);
190
- const led = new THREE.Mesh(ledGeo, glowMat);
191
- led.position.set(0, 1.0, 0.205);
192
- parts.chestLed = led;
193
- group.add(led);
194
-
195
- /* ── Head ────────────────────────────────────────────────────────────── */
196
- const headGeo = new THREE.BoxGeometry(0.4, 0.35, 0.3);
197
- const head = new THREE.Mesh(headGeo, bodyMat);
198
- head.position.y = 1.45;
199
- head.castShadow = true;
200
- group.add(head);
201
- parts.head = head;
202
-
203
- // Visor / Face plate
204
- const visorGeo = new THREE.BoxGeometry(0.35, 0.15, 0.02);
205
- const visor = new THREE.Mesh(visorGeo, new THREE.MeshStandardMaterial({
206
- color: 0x111827, roughness: 0.1, metalness: 0.9,
207
- }));
208
- visor.position.set(0, 1.48, 0.17);
209
- group.add(visor);
210
-
211
- // Eyes (glowing dots)
212
- const eyeGeo = new THREE.CircleGeometry(0.035, 8);
213
- const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
214
- leftEye.position.set(-0.08, 1.48, 0.185);
215
- group.add(leftEye);
216
- parts.leftEye = leftEye;
217
-
218
- const rightEye = new THREE.Mesh(eyeGeo, eyeMat.clone());
219
- rightEye.position.set(0.08, 1.48, 0.185);
220
- group.add(rightEye);
221
- parts.rightEye = rightEye;
222
-
223
- // Antenna
224
- const antennaGeo = new THREE.CylinderGeometry(0.015, 0.015, 0.2, 6);
225
- const antenna = new THREE.Mesh(antennaGeo, metalMat);
226
- antenna.position.set(0, 1.72, 0);
227
- group.add(antenna);
228
-
229
- // Antenna tip (glowing ball)
230
- const tipGeo = new THREE.SphereGeometry(0.035, 8, 8);
231
- const tip = new THREE.Mesh(tipGeo, glowMat);
232
- tip.position.set(0, 1.84, 0);
233
- group.add(tip);
234
- parts.antenna = tip;
235
-
236
- /* ── Arms ────────────────────────────────────────────────────────────── */
237
- // Left arm
238
- const armGeo = new THREE.BoxGeometry(0.12, 0.45, 0.12);
239
- const leftArm = new THREE.Mesh(armGeo, accentMat);
240
- leftArm.position.set(-0.36, 0.85, 0);
241
- leftArm.castShadow = true;
242
- group.add(leftArm);
243
- parts.leftArm = leftArm;
244
-
245
- // Left hand
246
- const handGeo = new THREE.SphereGeometry(0.06, 8, 8);
247
- const leftHand = new THREE.Mesh(handGeo, metalMat);
248
- leftHand.position.set(-0.36, 0.58, 0);
249
- group.add(leftHand);
250
-
251
- // Right arm
252
- const rightArm = new THREE.Mesh(armGeo, accentMat);
253
- rightArm.position.set(0.36, 0.85, 0);
254
- rightArm.castShadow = true;
255
- group.add(rightArm);
256
- parts.rightArm = rightArm;
257
-
258
- // Right hand
259
- const rightHand = new THREE.Mesh(handGeo, metalMat);
260
- rightHand.position.set(0.36, 0.58, 0);
261
- group.add(rightHand);
262
-
263
- /* ── Legs ────────────────────────────────────────────────────────────── */
264
- const legGeo = new THREE.BoxGeometry(0.14, 0.35, 0.14);
265
- const leftLeg = new THREE.Mesh(legGeo, bodyMat);
266
- leftLeg.position.set(-0.12, 0.38, 0);
267
- leftLeg.castShadow = true;
268
- group.add(leftLeg);
269
- parts.leftLeg = leftLeg;
270
-
271
- const rightLeg = new THREE.Mesh(legGeo, bodyMat);
272
- rightLeg.position.set(0.12, 0.38, 0);
273
- rightLeg.castShadow = true;
274
- group.add(rightLeg);
275
- parts.rightLeg = rightLeg;
276
-
277
- // Feet
278
- const footGeo = new THREE.BoxGeometry(0.16, 0.06, 0.2);
279
- const leftFoot = new THREE.Mesh(footGeo, accentMat);
280
- leftFoot.position.set(-0.12, 0.23, 0.03);
281
- group.add(leftFoot);
282
-
283
- const rightFoot = new THREE.Mesh(footGeo, accentMat);
284
- rightFoot.position.set(0.12, 0.23, 0.03);
285
- group.add(rightFoot);
286
-
287
- /* ── Backpack (jet-pack) ─────────────────────────────────────────────── */
288
- const backpackGeo = new THREE.BoxGeometry(0.25, 0.3, 0.15);
289
- const backpack = new THREE.Mesh(backpackGeo, accentMat);
290
- backpack.position.set(0, 0.95, -0.25);
291
- backpack.castShadow = true;
292
- group.add(backpack);
293
-
294
- // Exhaust ports
295
- const exhaustGeo = new THREE.CylinderGeometry(0.03, 0.04, 0.06, 6);
296
- const exhaust1 = new THREE.Mesh(exhaustGeo, metalMat);
297
- exhaust1.position.set(-0.06, 0.77, -0.3);
298
- group.add(exhaust1);
299
- const exhaust2 = exhaust1.clone();
300
- exhaust2.position.x = 0.06;
301
- group.add(exhaust2);
302
-
303
- /* ── Shadow blob ─────────────────────────────────────────────────────── */
304
- const shadowGeo = new THREE.CircleGeometry(0.3, 16);
305
- const shadowMat = new THREE.MeshBasicMaterial({
306
- color: 0x000000, transparent: true, opacity: 0.15,
307
- });
308
- const shadow = new THREE.Mesh(shadowGeo, shadowMat);
309
- shadow.rotation.x = -Math.PI / 2;
310
- shadow.position.y = 0.21;
311
- group.add(shadow);
312
-
313
- /* ── 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)
317
-
318
- group.position.set(x, 0.2, z);
319
-
320
- // Point toward desk
321
- group.lookAt(new THREE.Vector3(desk.x, 0.2, desk.z));
322
-
323
- scene.add(group);
324
-
325
- /* ── CSS2D Label ─────────────────────────────────────────────────────── */
326
- const labelDiv = document.createElement('div');
327
- labelDiv.className = 'agent-label-3d';
328
- labelDiv.innerHTML = `${agentData.name}<span class="role">${role}</span>`;
329
- const label = new CSS2DObject(labelDiv);
330
- label.position.set(0, 2.1, 0);
331
- group.add(label);
332
-
333
- /* ── Point light (glow effect around robot) ──────────────────────────── */
334
- const glow = new THREE.PointLight(colors.glow, 0.3, 4, 2);
335
- glow.position.set(0, 1.2, 0);
336
- group.add(glow);
337
- parts.glow = glow;
338
-
339
- /* ── Store ───────────────────────────────────────────────────────────── */
340
- agents.set(agentData.name, {
341
- group,
342
- parts,
343
- label,
344
- status: agentData.status || 'idle',
345
- role,
346
- baseX: x,
347
- baseY: 0.2,
348
- baseZ: z,
349
- deskPos: desk,
350
- });
351
- }
352
-
353
- /* ═════════════════════════════════════════════════════════════════════════
354
- Status Update
355
- ═════════════════════════════════════════════════════════════════════════ */
356
- _updateStatus(name, status) {
357
- const agent = agents.get(name);
358
- if (!agent) return;
359
- agent.status = status;
360
-
361
- // Update glow intensity based on status
362
- const intensityMap = {
363
- idle: 0.2, working: 0.6, testing: 0.7,
364
- thinking: 0.4, error: 1.0, failed: 1.0,
365
- done: 0.5, passed: 0.5,
366
- };
367
- if (agent.parts.glow) {
368
- agent.parts.glow.intensity = intensityMap[status] || 0.2;
369
- }
370
- }
371
-
372
- /* ═════════════════════════════════════════════════════════════════════════
373
- Remove Robot
374
- ═════════════════════════════════════════════════════════════════════════ */
375
- _removeRobot(name) {
376
- const agent = agents.get(name);
377
- if (!agent) return;
378
- scene.remove(agent.group);
379
- agents.delete(name);
380
-
381
- // Clean bubble timer
382
- const bt = this._bubbleTimers.get(name);
383
- if (bt) { clearTimeout(bt); this._bubbleTimers.delete(name); }
384
- }
385
-
386
- /* ═════════════════════════════════════════════════════════════════════════
387
- Bubble System — 3D floating chat bubbles
388
- ═════════════════════════════════════════════════════════════════════════ */
389
- _scheduleBubbles(agentData) {
390
- const BUBBLE_TEXTS = {
391
- working: ['正在执行...', '快了快了', '处理中...', '加油 💪'],
392
- testing: ['跑测试中...', '验证 API...', '等结果...'],
393
- thinking: ['让我想想...', '分析中...', '推理...', '🤔'],
394
- error: ['出错了!', '修复中...', '糟糕...'],
395
- idle: ['摸鱼中~', '等任务...', '☕ 喝咖啡', 'zzZ'],
396
- done: ['搞定!', '完成 ✓', '下一个!'],
397
- passed: ['全绿 ✓', '测试通过!'],
398
- failed: ['有失败...', '需要修复'],
399
- };
400
-
401
- const current = new Set();
402
- agentData.forEach(a => {
403
- current.add(a.name);
404
- if (this._bubbleTimers.has(a.name)) return;
405
-
406
- const schedule = () => {
407
- const agent = agents.get(a.name);
408
- if (!agent) return;
409
- const status = agent.status || 'idle';
410
- const texts = BUBBLE_TEXTS[status] || BUBBLE_TEXTS.idle;
411
- const text = texts[Math.floor(Math.random() * texts.length)];
412
- this._showBubble(a.name, text);
413
- const next = 6000 + Math.random() * 8000;
414
- this._bubbleTimers.set(a.name, setTimeout(schedule, next));
415
- };
416
-
417
- const delay = 1000 + Math.random() * 3000;
418
- this._bubbleTimers.set(a.name, setTimeout(schedule, delay));
419
- });
420
-
421
- // Remove timers for removed agents
422
- for (const [name, timer] of this._bubbleTimers) {
423
- if (!current.has(name)) {
424
- clearTimeout(timer);
425
- this._bubbleTimers.delete(name);
426
- }
427
- }
428
- }
429
-
430
- _showBubble(name, text) {
431
- const agent = agents.get(name);
432
- if (!agent) return;
433
-
434
- // Remove existing bubble
435
- if (agent.bubbleObj) {
436
- agent.group.remove(agent.bubbleObj);
437
- }
438
-
439
- const div = document.createElement('div');
440
- div.className = 'bubble-3d';
441
- div.textContent = text;
442
-
443
- const bubble = new CSS2DObject(div);
444
- bubble.position.set(0.4, 2.3, 0);
445
- agent.group.add(bubble);
446
- agent.bubbleObj = bubble;
447
-
448
- // 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
- }
457
-
458
- /* ═══════════════════════════════════════════════════════════════════════════════
459
- Helper exports
460
- ═══════════════════════════════════════════════════════════════════════════════ */
461
- export function getAgentPosition(name) {
462
- const agent = agents.get(name);
463
- if (!agent) return null;
464
- return agent.group.position.clone();
465
- }
@@ -1,125 +0,0 @@
1
- /* ═══════════════════════════════════════════════════════════════════════════════
2
- OpenCroc Studio 3D — Camera Controller
3
- Orbit controls, fly-to transitions, auto-rotate
4
- ~1000 lines
5
- ═══════════════════════════════════════════════════════════════════════════════ */
6
-
7
- import * as THREE from 'three';
8
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
9
-
10
- /* ═══════════════════════════════════════════════════════════════════════════════
11
- Camera Presets
12
- ═══════════════════════════════════════════════════════════════════════════════ */
13
- const PRESETS = {
14
- office: {
15
- position: new THREE.Vector3(18, 14, 18),
16
- target: new THREE.Vector3(0, 2, 0),
17
- },
18
- graph: {
19
- position: new THREE.Vector3(0, 25, 0.1),
20
- target: new THREE.Vector3(0, 0, 0),
21
- },
22
- closeup: {
23
- position: new THREE.Vector3(5, 4, 8),
24
- target: new THREE.Vector3(0, 2, 0),
25
- },
26
- overview: {
27
- position: new THREE.Vector3(30, 20, 30),
28
- target: new THREE.Vector3(0, 0, 0),
29
- },
30
- };
31
-
32
- /* ═══════════════════════════════════════════════════════════════════════════════
33
- CameraController
34
- ═══════════════════════════════════════════════════════════════════════════════ */
35
- export class CameraController {
36
- constructor(canvas, camera, scene) {
37
- this.camera = camera;
38
- this.scene = scene;
39
- scene.userData.camera = camera; // Store ref for CSS2D renderer
40
-
41
- // Orbit controls
42
- this.controls = new OrbitControls(camera, canvas);
43
- this.controls.enableDamping = true;
44
- this.controls.dampingFactor = 0.08;
45
- this.controls.enablePan = true;
46
- this.controls.panSpeed = 0.8;
47
- this.controls.rotateSpeed = 0.6;
48
- this.controls.zoomSpeed = 1.2;
49
- this.controls.minDistance = 3;
50
- this.controls.maxDistance = 60;
51
- this.controls.maxPolarAngle = Math.PI * 0.48;
52
- this.controls.minPolarAngle = Math.PI * 0.05;
53
- this.controls.target.set(0, 2, 0);
54
- this.controls.autoRotate = true;
55
- this.controls.autoRotateSpeed = 0.3;
56
-
57
- // Fly-to animation state
58
- this._flying = false;
59
- this._flyStart = { pos: new THREE.Vector3(), tgt: new THREE.Vector3() };
60
- this._flyEnd = { pos: new THREE.Vector3(), tgt: new THREE.Vector3() };
61
- this._flyProgress = 0;
62
- this._flyDuration = 1.5;
63
- }
64
-
65
- /** Update each frame */
66
- update(dt) {
67
- if (this._flying) {
68
- this._flyProgress += dt / this._flyDuration;
69
- if (this._flyProgress >= 1) {
70
- this._flyProgress = 1;
71
- this._flying = false;
72
- }
73
-
74
- // Smooth easing (ease-out-expo)
75
- const t = 1 - Math.pow(1 - this._flyProgress, 3);
76
-
77
- this.camera.position.lerpVectors(this._flyStart.pos, this._flyEnd.pos, t);
78
- this.controls.target.lerpVectors(this._flyStart.tgt, this._flyEnd.tgt, t);
79
- }
80
-
81
- this.controls.update();
82
- }
83
-
84
- /** Fly to a named preset or custom position */
85
- flyTo(presetOrTarget, duration = 1.5) {
86
- let target;
87
- if (typeof presetOrTarget === 'string') {
88
- target = PRESETS[presetOrTarget];
89
- if (!target) return;
90
- } else {
91
- target = presetOrTarget;
92
- }
93
-
94
- this._flyStart.pos.copy(this.camera.position);
95
- this._flyStart.tgt.copy(this.controls.target);
96
- this._flyEnd.pos.copy(target.position);
97
- this._flyEnd.tgt.copy(target.target);
98
- this._flyProgress = 0;
99
- this._flyDuration = duration;
100
- this._flying = true;
101
- }
102
-
103
- /** Fly to a specific module in the graph */
104
- flyToModule(x, z) {
105
- this.flyTo({
106
- position: new THREE.Vector3(x + 8, 10, z + 8),
107
- target: new THREE.Vector3(x, 1, z),
108
- });
109
- }
110
-
111
- /** Enable/disable auto rotation */
112
- enableAutoRotate() {
113
- this.controls.autoRotate = true;
114
- this.controls.autoRotateSpeed = 0.3;
115
- }
116
-
117
- disableAutoRotate() {
118
- this.controls.autoRotate = false;
119
- }
120
-
121
- /** Get the raw controls for external use */
122
- getControls() {
123
- return this.controls;
124
- }
125
- }