@cluesmith/codev 1.4.12 → 1.5.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 (35) hide show
  1. package/dist/agent-farm/cli.d.ts.map +1 -1
  2. package/dist/agent-farm/cli.js +19 -0
  3. package/dist/agent-farm/cli.js.map +1 -1
  4. package/dist/agent-farm/commands/architect.d.ts +15 -0
  5. package/dist/agent-farm/commands/architect.d.ts.map +1 -0
  6. package/dist/agent-farm/commands/architect.js +172 -0
  7. package/dist/agent-farm/commands/architect.js.map +1 -0
  8. package/dist/agent-farm/commands/consult.d.ts.map +1 -1
  9. package/dist/agent-farm/commands/consult.js +2 -4
  10. package/dist/agent-farm/commands/consult.js.map +1 -1
  11. package/dist/agent-farm/commands/start.d.ts +13 -0
  12. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/start.js +140 -3
  14. package/dist/agent-farm/commands/start.js.map +1 -1
  15. package/dist/agent-farm/commands/stop.d.ts.map +1 -1
  16. package/dist/agent-farm/commands/stop.js +69 -1
  17. package/dist/agent-farm/commands/stop.js.map +1 -1
  18. package/dist/agent-farm/servers/dashboard-server.js +77 -2
  19. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  20. package/dist/agent-farm/servers/open-server.js +64 -10
  21. package/dist/agent-farm/servers/open-server.js.map +1 -1
  22. package/dist/agent-farm/types.d.ts +2 -0
  23. package/dist/agent-farm/types.d.ts.map +1 -1
  24. package/dist/agent-farm/utils/terminal-ports.d.ts +18 -0
  25. package/dist/agent-farm/utils/terminal-ports.d.ts.map +1 -0
  26. package/dist/agent-farm/utils/terminal-ports.js +35 -0
  27. package/dist/agent-farm/utils/terminal-ports.js.map +1 -0
  28. package/dist/commands/update.d.ts.map +1 -1
  29. package/dist/commands/update.js +8 -0
  30. package/dist/commands/update.js.map +1 -1
  31. package/package.json +3 -1
  32. package/templates/3d-viewer.html +799 -0
  33. package/templates/dashboard/js/tabs.js +49 -4
  34. package/templates/open.html +67 -0
  35. package/templates/stl-viewer.html +0 -473
@@ -1,10 +1,47 @@
1
1
  // Tab Management - Rendering, Selection, Overflow
2
2
 
3
3
  // Get the base URL for ttyd/server connections (uses current hostname for remote access)
4
+ // DEPRECATED: Use getTerminalUrl() for terminal tabs (Spec 0062)
4
5
  function getBaseUrl(port) {
5
6
  return `http://${window.location.hostname}:${port}`;
6
7
  }
7
8
 
9
+ /**
10
+ * Get the terminal URL for a tab (Spec 0062 - Secure Remote Access)
11
+ * Uses the reverse proxy instead of direct port access, enabling SSH tunnel support.
12
+ *
13
+ * @param {Object} tab - The tab object
14
+ * @returns {string} The URL to load in the iframe
15
+ */
16
+ function getTerminalUrl(tab) {
17
+ // Architect terminal
18
+ if (tab.type === 'architect') {
19
+ return '/terminal/architect';
20
+ }
21
+
22
+ // Builder terminal - use the builder's ID (e.g., builder-0055)
23
+ if (tab.type === 'builder') {
24
+ return `/terminal/builder-${tab.projectId}`;
25
+ }
26
+
27
+ // Shell/utility terminal - use the utility's ID (e.g., util-U12345)
28
+ if (tab.type === 'shell') {
29
+ return `/terminal/util-${tab.utilId}`;
30
+ }
31
+
32
+ // File tabs still use direct port access (open-server, not ttyd)
33
+ if (tab.type === 'file' && tab.port) {
34
+ return getBaseUrl(tab.port);
35
+ }
36
+
37
+ // Fallback for backward compatibility
38
+ if (tab.port) {
39
+ return getBaseUrl(tab.port);
40
+ }
41
+
42
+ return null;
43
+ }
44
+
8
45
  // Build tabs from initial state
9
46
  function buildTabsFromState() {
10
47
  const previousTabIds = new Set(tabs.map(t => t.id));
@@ -88,7 +125,8 @@ function renderArchitect() {
88
125
  // Only update iframe if port changed (avoid flashing on poll)
89
126
  if (currentArchitectPort !== state.architect.port) {
90
127
  currentArchitectPort = state.architect.port;
91
- content.innerHTML = `<iframe src="${getBaseUrl(state.architect.port)}" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
128
+ // Use proxied URL for remote access support (Spec 0062)
129
+ content.innerHTML = `<iframe src="/terminal/architect" title="Architect Terminal" allow="clipboard-read; clipboard-write"></iframe>`;
92
130
  }
93
131
  } else {
94
132
  if (currentArchitectPort !== null) {
@@ -249,7 +287,13 @@ function renderTabContent() {
249
287
  if (currentTabPort !== tab.port || currentTabType !== tab.type) {
250
288
  currentTabPort = tab.port;
251
289
  currentTabType = tab.type;
252
- content.innerHTML = `<iframe src="${getBaseUrl(tab.port)}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
290
+ // Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
291
+ const url = getTerminalUrl(tab);
292
+ if (url) {
293
+ content.innerHTML = `<iframe src="${url}" title="${tab.name}" allow="clipboard-read; clipboard-write"></iframe>`;
294
+ } else {
295
+ content.innerHTML = `<div class="empty-state"><p>Terminal unavailable</p></div>`;
296
+ }
253
297
  }
254
298
  }
255
299
 
@@ -486,11 +530,12 @@ function openInNewTab(tabId) {
486
530
  const tab = tabs.find(t => t.id === tabId);
487
531
  if (!tab) return;
488
532
 
489
- if (!tab.port) {
533
+ // Use proxied URL for terminal tabs (Spec 0062 - Secure Remote Access)
534
+ const url = getTerminalUrl(tab);
535
+ if (!url) {
490
536
  showToast('Tab not ready', 'error');
491
537
  return;
492
538
  }
493
539
 
494
- const url = getBaseUrl(tab.port);
495
540
  window.open(url, '_blank', 'noopener,noreferrer');
496
541
  }
@@ -559,6 +559,7 @@
559
559
  yaml: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
560
560
  yml: { prefix: '# REVIEW', regex: /^(\s*)#\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
561
561
  md: { prefix: '<!-- REVIEW', suffix: ' -->', regex: /^(\s*)<!--\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*-->$/ },
562
+ qmd: { prefix: '<!-- REVIEW', suffix: ' -->', regex: /^(\s*)<!--\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*-->$/ },
562
563
  html: { prefix: '<!-- REVIEW', suffix: ' -->', regex: /^(\s*)<!--\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*-->$/ },
563
564
  css: { prefix: '/* REVIEW', suffix: ' */', regex: /^(\s*)\/\*\s*REVIEW(\(@\w+\))?:\s*(.*?)\s*\*\/$/ },
564
565
  };
@@ -1660,6 +1661,72 @@
1660
1661
  }
1661
1662
  });
1662
1663
 
1664
+ // Auto-reload: poll file mtime and reload when changed
1665
+ let lastMtime = null;
1666
+ const POLL_INTERVAL = 1000; // 1 second
1667
+
1668
+ async function checkForChanges() {
1669
+ // Don't auto-reload if user has unsaved changes
1670
+ if (hasUnsavedChanges || editMode) return;
1671
+
1672
+ try {
1673
+ const res = await fetch('/api/mtime');
1674
+ if (res.ok) {
1675
+ const data = await res.json();
1676
+ if (lastMtime === null) {
1677
+ lastMtime = data.mtime;
1678
+ } else if (data.mtime !== lastMtime) {
1679
+ lastMtime = data.mtime;
1680
+ autoReloadFile();
1681
+ }
1682
+ }
1683
+ } catch (err) {
1684
+ // Ignore fetch errors (server may be restarting)
1685
+ }
1686
+ }
1687
+
1688
+ async function autoReloadFile() {
1689
+ // For images and videos, just reload the source
1690
+ if (isImageFile) {
1691
+ const img = document.getElementById('image-display');
1692
+ img.src = '/api/image?t=' + Date.now();
1693
+ showNotification('Image reloaded');
1694
+ return;
1695
+ }
1696
+
1697
+ if (isVideoFile) {
1698
+ const video = document.getElementById('video-display');
1699
+ video.src = '/api/video?t=' + Date.now();
1700
+ showNotification('Video reloaded');
1701
+ return;
1702
+ }
1703
+
1704
+ // For text files, fetch new content
1705
+ try {
1706
+ const res = await fetch('/file');
1707
+ if (res.ok) {
1708
+ const content = await res.text();
1709
+ currentContent = content;
1710
+ originalContent = content;
1711
+ fileLines = content.split('\n');
1712
+
1713
+ // Re-render based on current mode
1714
+ if (isPreviewMode) {
1715
+ renderPreview();
1716
+ } else {
1717
+ renderFile();
1718
+ }
1719
+ updateAnnotationsList();
1720
+ showNotification('File reloaded');
1721
+ }
1722
+ } catch (err) {
1723
+ // Ignore errors
1724
+ }
1725
+ }
1726
+
1727
+ // Start polling for changes
1728
+ setInterval(checkForChanges, POLL_INTERVAL);
1729
+
1663
1730
  // FILE_CONTENT will be injected by the server
1664
1731
  </script>
1665
1732
  </body>
@@ -1,473 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{{FILE}} - STL Viewer</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
-
14
- body {
15
- background: #1a1a2e;
16
- color: #e0e0e0;
17
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
18
- overflow: hidden;
19
- height: 100vh;
20
- display: flex;
21
- flex-direction: column;
22
- }
23
-
24
- #header {
25
- background: #16213e;
26
- padding: 8px 16px;
27
- display: flex;
28
- align-items: center;
29
- justify-content: space-between;
30
- border-bottom: 1px solid #0f3460;
31
- flex-shrink: 0;
32
- }
33
-
34
- #filename {
35
- font-size: 14px;
36
- color: #94a3b8;
37
- }
38
-
39
- #info {
40
- font-size: 12px;
41
- color: #64748b;
42
- }
43
-
44
- #controls {
45
- display: flex;
46
- gap: 8px;
47
- align-items: center;
48
- }
49
-
50
- .control-group {
51
- display: flex;
52
- gap: 4px;
53
- padding: 0 8px;
54
- border-right: 1px solid #0f3460;
55
- }
56
-
57
- .control-group:last-child {
58
- border-right: none;
59
- }
60
-
61
- .control-label {
62
- font-size: 10px;
63
- color: #64748b;
64
- margin-right: 4px;
65
- align-self: center;
66
- }
67
-
68
- button {
69
- background: #0f3460;
70
- color: #e0e0e0;
71
- border: 1px solid #1e4976;
72
- padding: 4px 8px;
73
- border-radius: 4px;
74
- cursor: pointer;
75
- font-size: 11px;
76
- transition: background 0.2s;
77
- min-width: 28px;
78
- }
79
-
80
- button:hover {
81
- background: #1e4976;
82
- }
83
-
84
- button.active {
85
- background: #2563eb;
86
- border-color: #3b82f6;
87
- }
88
-
89
- button.view-btn {
90
- font-family: monospace;
91
- font-weight: bold;
92
- }
93
-
94
- button.view-btn.positive {
95
- color: #4ade80;
96
- }
97
-
98
- button.view-btn.negative {
99
- color: #f87171;
100
- }
101
-
102
- #canvas-container {
103
- flex: 1;
104
- position: relative;
105
- }
106
-
107
- #canvas {
108
- display: block;
109
- width: 100%;
110
- height: 100%;
111
- }
112
-
113
- #loading {
114
- position: absolute;
115
- top: 50%;
116
- left: 50%;
117
- transform: translate(-50%, -50%);
118
- text-align: center;
119
- color: #94a3b8;
120
- }
121
-
122
- #loading .spinner {
123
- width: 40px;
124
- height: 40px;
125
- border: 3px solid #0f3460;
126
- border-top-color: #3b82f6;
127
- border-radius: 50%;
128
- animation: spin 1s linear infinite;
129
- margin: 0 auto 12px;
130
- }
131
-
132
- @keyframes spin {
133
- to { transform: rotate(360deg); }
134
- }
135
-
136
- #error {
137
- position: absolute;
138
- top: 50%;
139
- left: 50%;
140
- transform: translate(-50%, -50%);
141
- text-align: center;
142
- color: #ef4444;
143
- display: none;
144
- }
145
-
146
- .hidden {
147
- display: none !important;
148
- }
149
-
150
- /* Axes legend */
151
- #axes-legend {
152
- position: absolute;
153
- bottom: 16px;
154
- left: 16px;
155
- background: rgba(22, 33, 62, 0.9);
156
- padding: 8px 12px;
157
- border-radius: 4px;
158
- font-size: 11px;
159
- font-family: monospace;
160
- }
161
-
162
- #axes-legend div {
163
- margin: 2px 0;
164
- }
165
-
166
- .axis-x { color: #ef4444; }
167
- .axis-y { color: #22c55e; }
168
- .axis-z { color: #3b82f6; }
169
- </style>
170
- </head>
171
- <body>
172
- <div id="header">
173
- <div>
174
- <span id="filename">{{FILE}}</span>
175
- <span id="info"></span>
176
- </div>
177
- <div id="controls">
178
- <div class="control-group">
179
- <span class="control-label">View:</span>
180
- <button id="viewTop" class="view-btn positive" title="Top (+Y)">+Y</button>
181
- <button id="viewBottom" class="view-btn negative" title="Bottom (-Y)">-Y</button>
182
- <button id="viewFront" class="view-btn positive" title="Front (+Z)">+Z</button>
183
- <button id="viewBack" class="view-btn negative" title="Back (-Z)">-Z</button>
184
- <button id="viewRight" class="view-btn positive" title="Right (+X)">+X</button>
185
- <button id="viewLeft" class="view-btn negative" title="Left (-X)">-X</button>
186
- </div>
187
- <div class="control-group">
188
- <button id="viewIso" title="Isometric view">Iso</button>
189
- <button id="resetBtn" title="Fit model to view">Fit</button>
190
- </div>
191
- <div class="control-group">
192
- <button id="wireframeBtn" title="Toggle wireframe mode">Wire</button>
193
- <button id="axesBtn" class="active" title="Toggle axes">Axes</button>
194
- <button id="gridBtn" class="active" title="Toggle grid">Grid</button>
195
- </div>
196
- </div>
197
- </div>
198
-
199
- <div id="canvas-container">
200
- <canvas id="canvas"></canvas>
201
- <div id="loading">
202
- <div class="spinner"></div>
203
- <div>Loading STL...</div>
204
- </div>
205
- <div id="error">
206
- <div style="font-size: 48px; margin-bottom: 12px;">⚠️</div>
207
- <div id="error-message">Failed to load STL file</div>
208
- </div>
209
- <div id="axes-legend">
210
- <div class="axis-x">X → Right</div>
211
- <div class="axis-y">Y → Up</div>
212
- <div class="axis-z">Z → Front</div>
213
- </div>
214
- </div>
215
-
216
- <!-- Three.js from CDN (using r128 which has global builds) -->
217
- <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
218
- <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/STLLoader.js"></script>
219
- <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
220
-
221
- <script>
222
- // Configuration
223
- const FILE_PATH = '{{FILE_PATH}}';
224
- const FILE_NAME = '{{FILE}}';
225
-
226
- // Three.js setup
227
- let scene, camera, renderer, controls, mesh;
228
- let axesHelper, gridHelper;
229
- let wireframeMode = false;
230
- let showAxes = true;
231
- let showGrid = true;
232
- let modelCenter = new THREE.Vector3();
233
- let cameraDistance = 100;
234
-
235
- // DOM elements
236
- const canvas = document.getElementById('canvas');
237
- const container = document.getElementById('canvas-container');
238
- const loading = document.getElementById('loading');
239
- const error = document.getElementById('error');
240
- const errorMessage = document.getElementById('error-message');
241
- const info = document.getElementById('info');
242
-
243
- // Buttons
244
- const resetBtn = document.getElementById('resetBtn');
245
- const wireframeBtn = document.getElementById('wireframeBtn');
246
- const axesBtn = document.getElementById('axesBtn');
247
- const gridBtn = document.getElementById('gridBtn');
248
- const viewTop = document.getElementById('viewTop');
249
- const viewBottom = document.getElementById('viewBottom');
250
- const viewFront = document.getElementById('viewFront');
251
- const viewBack = document.getElementById('viewBack');
252
- const viewRight = document.getElementById('viewRight');
253
- const viewLeft = document.getElementById('viewLeft');
254
- const viewIso = document.getElementById('viewIso');
255
-
256
- function init() {
257
- // Scene
258
- scene = new THREE.Scene();
259
- scene.background = new THREE.Color(0x1a1a2e);
260
-
261
- // Camera
262
- camera = new THREE.PerspectiveCamera(
263
- 45,
264
- container.clientWidth / container.clientHeight,
265
- 0.1,
266
- 10000
267
- );
268
- camera.position.set(100, 100, 100);
269
-
270
- // Renderer
271
- renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
272
- renderer.setSize(container.clientWidth, container.clientHeight);
273
- renderer.setPixelRatio(window.devicePixelRatio);
274
-
275
- // Controls
276
- controls = new THREE.OrbitControls(camera, renderer.domElement);
277
- controls.enableDamping = true;
278
- controls.dampingFactor = 0.1;
279
- controls.screenSpacePanning = true;
280
-
281
- // Lighting
282
- const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
283
- scene.add(ambientLight);
284
-
285
- const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
286
- directionalLight1.position.set(1, 1, 1);
287
- scene.add(directionalLight1);
288
-
289
- const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
290
- directionalLight2.position.set(-1, -1, -1);
291
- scene.add(directionalLight2);
292
-
293
- // Grid
294
- gridHelper = new THREE.GridHelper(200, 20, 0x0f3460, 0x0f3460);
295
- scene.add(gridHelper);
296
-
297
- // Axes helper (X=red, Y=green, Z=blue)
298
- axesHelper = new THREE.AxesHelper(50);
299
- scene.add(axesHelper);
300
-
301
- // Load STL
302
- loadSTL();
303
-
304
- // Event listeners
305
- window.addEventListener('resize', onResize);
306
-
307
- // Control buttons
308
- resetBtn.addEventListener('click', fitToView);
309
- wireframeBtn.addEventListener('click', toggleWireframe);
310
- axesBtn.addEventListener('click', toggleAxes);
311
- gridBtn.addEventListener('click', toggleGrid);
312
-
313
- // View buttons
314
- viewTop.addEventListener('click', () => setView(0, 1, 0));
315
- viewBottom.addEventListener('click', () => setView(0, -1, 0));
316
- viewFront.addEventListener('click', () => setView(0, 0, 1));
317
- viewBack.addEventListener('click', () => setView(0, 0, -1));
318
- viewRight.addEventListener('click', () => setView(1, 0, 0));
319
- viewLeft.addEventListener('click', () => setView(-1, 0, 0));
320
- viewIso.addEventListener('click', () => setView(1, 1, 1));
321
-
322
- // Animation loop
323
- animate();
324
- }
325
-
326
- function loadSTL() {
327
- const loader = new THREE.STLLoader();
328
-
329
- // Load from API endpoint
330
- loader.load(
331
- '/api/stl',
332
- (geometry) => {
333
- // Center geometry
334
- geometry.computeBoundingBox();
335
- const center = new THREE.Vector3();
336
- geometry.boundingBox.getCenter(center);
337
- geometry.translate(-center.x, -center.y, -center.z);
338
-
339
- // Move to sit on grid (optional - comment out to keep centered)
340
- const minY = geometry.boundingBox.min.y - center.y;
341
- geometry.translate(0, -minY, 0);
342
-
343
- // Recalculate bounding box after translation
344
- geometry.computeBoundingBox();
345
-
346
- // Material
347
- const material = new THREE.MeshPhongMaterial({
348
- color: 0x3b82f6,
349
- specular: 0x111111,
350
- shininess: 30,
351
- flatShading: false
352
- });
353
-
354
- mesh = new THREE.Mesh(geometry, material);
355
- scene.add(mesh);
356
-
357
- // Calculate model center and size
358
- const box = new THREE.Box3().setFromObject(mesh);
359
- box.getCenter(modelCenter);
360
- const size = box.getSize(new THREE.Vector3());
361
- cameraDistance = Math.max(size.x, size.y, size.z) * 2;
362
-
363
- // Scale grid and axes to model size
364
- const maxDim = Math.max(size.x, size.y, size.z);
365
- const gridSize = Math.ceil(maxDim * 2 / 10) * 10; // Round up to nearest 10
366
- scene.remove(gridHelper);
367
- gridHelper = new THREE.GridHelper(gridSize, gridSize / 5, 0x0f3460, 0x0f3460);
368
- scene.add(gridHelper);
369
-
370
- scene.remove(axesHelper);
371
- axesHelper = new THREE.AxesHelper(gridSize / 2);
372
- scene.add(axesHelper);
373
-
374
- // Fit camera to model
375
- fitToView();
376
-
377
- // Update info
378
- const triangles = geometry.attributes.position.count / 3;
379
- info.textContent = ` — ${triangles.toLocaleString()} triangles`;
380
-
381
- // Hide loading
382
- loading.classList.add('hidden');
383
- },
384
- (progress) => {
385
- // Progress callback (optional)
386
- },
387
- (err) => {
388
- console.error('Error loading STL:', err);
389
- loading.classList.add('hidden');
390
- error.style.display = 'block';
391
- errorMessage.textContent = 'Failed to load STL file: ' + (err.message || 'Unknown error');
392
- }
393
- );
394
- }
395
-
396
- function fitToView() {
397
- if (!mesh) return;
398
-
399
- const box = new THREE.Box3().setFromObject(mesh);
400
- const size = box.getSize(new THREE.Vector3());
401
- const center = box.getCenter(new THREE.Vector3());
402
-
403
- const maxDim = Math.max(size.x, size.y, size.z);
404
- const fov = camera.fov * (Math.PI / 180);
405
- cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
406
-
407
- // Isometric view
408
- setView(1, 1, 1);
409
- }
410
-
411
- function setView(x, y, z) {
412
- if (!mesh) return;
413
-
414
- const box = new THREE.Box3().setFromObject(mesh);
415
- const center = box.getCenter(new THREE.Vector3());
416
-
417
- // Normalize direction
418
- const dir = new THREE.Vector3(x, y, z).normalize();
419
-
420
- // Set camera position
421
- camera.position.copy(center).add(dir.multiplyScalar(cameraDistance));
422
-
423
- // Set up vector (handle top/bottom views)
424
- if (Math.abs(y) > 0.9) {
425
- camera.up.set(0, 0, y > 0 ? -1 : 1);
426
- } else {
427
- camera.up.set(0, 1, 0);
428
- }
429
-
430
- // Look at center
431
- controls.target.copy(center);
432
- camera.lookAt(center);
433
- controls.update();
434
- }
435
-
436
- function toggleWireframe() {
437
- wireframeMode = !wireframeMode;
438
- if (mesh) {
439
- mesh.material.wireframe = wireframeMode;
440
- }
441
- wireframeBtn.classList.toggle('active', wireframeMode);
442
- }
443
-
444
- function toggleAxes() {
445
- showAxes = !showAxes;
446
- axesHelper.visible = showAxes;
447
- axesBtn.classList.toggle('active', showAxes);
448
- document.getElementById('axes-legend').style.display = showAxes ? 'block' : 'none';
449
- }
450
-
451
- function toggleGrid() {
452
- showGrid = !showGrid;
453
- gridHelper.visible = showGrid;
454
- gridBtn.classList.toggle('active', showGrid);
455
- }
456
-
457
- function onResize() {
458
- camera.aspect = container.clientWidth / container.clientHeight;
459
- camera.updateProjectionMatrix();
460
- renderer.setSize(container.clientWidth, container.clientHeight);
461
- }
462
-
463
- function animate() {
464
- requestAnimationFrame(animate);
465
- controls.update();
466
- renderer.render(scene, camera);
467
- }
468
-
469
- // Start
470
- init();
471
- </script>
472
- </body>
473
- </html>