@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
@@ -0,0 +1,799 @@
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}} - 3D 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 id="loading-text">Loading 3D model...</div>
204
+ </div>
205
+ <div id="error">
206
+ <div style="font-size: 48px; margin-bottom: 12px;">&#9888;</div>
207
+ <div id="error-message">Failed to load 3D model</div>
208
+ </div>
209
+ <div id="axes-legend">
210
+ <div class="axis-x">X &#8594; Right</div>
211
+ <div class="axis-y">Y &#8594; Up</div>
212
+ <div class="axis-z">Z &#8594; Front</div>
213
+ </div>
214
+ </div>
215
+
216
+ <!-- Three.js ES Modules via import map -->
217
+ <script type="importmap">
218
+ {
219
+ "imports": {
220
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
221
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
222
+ }
223
+ }
224
+ </script>
225
+
226
+ <script type="module">
227
+ import * as THREE from 'three';
228
+ import { STLLoader } from 'three/addons/loaders/STLLoader.js';
229
+ import { ThreeMFLoader } from 'three/addons/loaders/3MFLoader.js';
230
+ import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
231
+
232
+ // Configuration (injected by server with proper escaping)
233
+ const FILE_PATH = {{FILE_PATH_JSON}}; // JSON-encoded by server
234
+ const FILE_NAME = '{{FILE}}';
235
+ const FORMAT = '{{FORMAT}}'; // 'stl' or '3mf'
236
+
237
+ // Three.js setup
238
+ let scene, camera, renderer, controls;
239
+ let model = null; // Can be mesh (STL) or group (3MF)
240
+ let wireframeMesh = null; // Wireframe overlay mesh
241
+ let axesHelper, gridHelper;
242
+ let viewMode = 'solid'; // 'solid', 'wireframe', or 'both'
243
+ let showAxes = true;
244
+ let showGrid = true;
245
+ let modelCenter = new THREE.Vector3();
246
+ let cameraDistance = 100;
247
+
248
+ // DOM elements
249
+ const canvas = document.getElementById('canvas');
250
+ const container = document.getElementById('canvas-container');
251
+ const loading = document.getElementById('loading');
252
+ const loadingText = document.getElementById('loading-text');
253
+ const error = document.getElementById('error');
254
+ const errorMessage = document.getElementById('error-message');
255
+ const info = document.getElementById('info');
256
+
257
+ // Buttons
258
+ const resetBtn = document.getElementById('resetBtn');
259
+ const wireframeBtn = document.getElementById('wireframeBtn');
260
+ const axesBtn = document.getElementById('axesBtn');
261
+ const gridBtn = document.getElementById('gridBtn');
262
+ const viewTop = document.getElementById('viewTop');
263
+ const viewBottom = document.getElementById('viewBottom');
264
+ const viewFront = document.getElementById('viewFront');
265
+ const viewBack = document.getElementById('viewBack');
266
+ const viewRight = document.getElementById('viewRight');
267
+ const viewLeft = document.getElementById('viewLeft');
268
+ const viewIso = document.getElementById('viewIso');
269
+
270
+ function init() {
271
+ // Scene
272
+ scene = new THREE.Scene();
273
+ scene.background = new THREE.Color(0x1a1a2e);
274
+
275
+ // Camera
276
+ camera = new THREE.PerspectiveCamera(
277
+ 45,
278
+ container.clientWidth / container.clientHeight,
279
+ 0.1,
280
+ 10000
281
+ );
282
+ camera.position.set(100, 100, 100);
283
+
284
+ // Renderer
285
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
286
+ renderer.setSize(container.clientWidth, container.clientHeight);
287
+ renderer.setPixelRatio(window.devicePixelRatio);
288
+
289
+ // Controls - TrackballControls uses quaternions, eliminating gimbal lock
290
+ controls = new TrackballControls(camera, renderer.domElement);
291
+ controls.rotateSpeed = 2.0;
292
+ controls.zoomSpeed = 1.2;
293
+ controls.panSpeed = 0.8;
294
+ controls.staticMoving = true; // No inertia for precise control
295
+ controls.dynamicDampingFactor = 0.3;
296
+
297
+ // Lighting
298
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
299
+ scene.add(ambientLight);
300
+
301
+ const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
302
+ directionalLight1.position.set(1, 1, 1);
303
+ scene.add(directionalLight1);
304
+
305
+ const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
306
+ directionalLight2.position.set(-1, -1, -1);
307
+ scene.add(directionalLight2);
308
+
309
+ // Grid
310
+ gridHelper = new THREE.GridHelper(200, 20, 0x0f3460, 0x0f3460);
311
+ scene.add(gridHelper);
312
+
313
+ // Axes helper (X=red, Y=green, Z=blue)
314
+ axesHelper = new THREE.AxesHelper(50);
315
+ scene.add(axesHelper);
316
+
317
+ // Load model based on format
318
+ loadModel();
319
+
320
+ // Event listeners
321
+ window.addEventListener('resize', onResize);
322
+
323
+ // Control buttons
324
+ resetBtn.addEventListener('click', fitToView);
325
+ wireframeBtn.addEventListener('click', toggleWireframe);
326
+ axesBtn.addEventListener('click', toggleAxes);
327
+ gridBtn.addEventListener('click', toggleGrid);
328
+
329
+ // View buttons
330
+ viewTop.addEventListener('click', () => setView(0, 1, 0));
331
+ viewBottom.addEventListener('click', () => setView(0, -1, 0));
332
+ viewFront.addEventListener('click', () => setView(0, 0, 1));
333
+ viewBack.addEventListener('click', () => setView(0, 0, -1));
334
+ viewRight.addEventListener('click', () => setView(1, 0, 0));
335
+ viewLeft.addEventListener('click', () => setView(-1, 0, 0));
336
+ viewIso.addEventListener('click', () => setView(1, 1, 1));
337
+
338
+ // Animation loop
339
+ animate();
340
+ }
341
+
342
+ function loadModel() {
343
+ loadingText.textContent = `Loading ${FORMAT.toUpperCase()} model...`;
344
+
345
+ if (FORMAT === '3mf') {
346
+ load3MF();
347
+ } else {
348
+ loadSTL();
349
+ }
350
+ }
351
+
352
+ function loadSTL() {
353
+ const loader = new STLLoader();
354
+
355
+ loader.load(
356
+ '/api/model',
357
+ (geometry) => {
358
+ // Center geometry
359
+ geometry.computeBoundingBox();
360
+ const center = new THREE.Vector3();
361
+ geometry.boundingBox.getCenter(center);
362
+ geometry.translate(-center.x, -center.y, -center.z);
363
+
364
+ // Move to sit on grid
365
+ const minY = geometry.boundingBox.min.y - center.y;
366
+ geometry.translate(0, -minY, 0);
367
+
368
+ // Recalculate bounding box after translation
369
+ geometry.computeBoundingBox();
370
+
371
+ // Material
372
+ const material = new THREE.MeshPhongMaterial({
373
+ color: 0x3b82f6,
374
+ specular: 0x111111,
375
+ shininess: 30,
376
+ flatShading: false
377
+ });
378
+
379
+ model = new THREE.Mesh(geometry, material);
380
+ scene.add(model);
381
+
382
+ setupAfterLoad(model);
383
+
384
+ // Update info
385
+ const triangles = geometry.attributes.position.count / 3;
386
+ info.textContent = ` - ${triangles.toLocaleString()} triangles`;
387
+ },
388
+ undefined,
389
+ (err) => {
390
+ showError('Failed to load STL file: ' + (err.message || 'Unknown error'));
391
+ }
392
+ );
393
+ }
394
+
395
+ function load3MF() {
396
+ const loader = new ThreeMFLoader();
397
+
398
+ loader.load(
399
+ '/api/model',
400
+ (group) => {
401
+ // 3MFLoader returns a Group with meshes
402
+ // 3MF uses Z-up, Three.js uses Y-up - rotate to match
403
+ group.rotation.set(-Math.PI / 2, 0, 0);
404
+
405
+ // Center the group
406
+ const box = new THREE.Box3().setFromObject(group);
407
+ const center = box.getCenter(new THREE.Vector3());
408
+ group.position.sub(center);
409
+
410
+ // Move to sit on grid (after centering)
411
+ const newBox = new THREE.Box3().setFromObject(group);
412
+ group.position.y -= newBox.min.y;
413
+
414
+ model = group;
415
+ scene.add(model);
416
+
417
+ setupAfterLoad(model);
418
+
419
+ // Count triangles and objects
420
+ let triangleCount = 0;
421
+ let objectCount = 0;
422
+ group.traverse((child) => {
423
+ if (child.isMesh) {
424
+ objectCount++;
425
+ const geom = child.geometry;
426
+ if (geom.index) {
427
+ triangleCount += geom.index.count / 3;
428
+ } else if (geom.attributes.position) {
429
+ triangleCount += geom.attributes.position.count / 3;
430
+ }
431
+ }
432
+ });
433
+
434
+ info.textContent = ` - ${triangleCount.toLocaleString()} triangles, ${objectCount} object${objectCount !== 1 ? 's' : ''}`;
435
+ },
436
+ undefined,
437
+ (err) => {
438
+ showError('Failed to load 3MF file: ' + (err.message || 'Unknown error'));
439
+ }
440
+ );
441
+ }
442
+
443
+ function setupAfterLoad(object) {
444
+ // Calculate model center and size
445
+ const box = new THREE.Box3().setFromObject(object);
446
+ box.getCenter(modelCenter);
447
+ const size = box.getSize(new THREE.Vector3());
448
+ cameraDistance = Math.max(size.x, size.y, size.z) * 2;
449
+
450
+ // Scale grid and axes to model size
451
+ const maxDim = Math.max(size.x, size.y, size.z);
452
+ const gridSize = Math.ceil(maxDim * 2 / 10) * 10; // Round up to nearest 10
453
+ scene.remove(gridHelper);
454
+ gridHelper = new THREE.GridHelper(gridSize, gridSize / 5, 0x0f3460, 0x0f3460);
455
+ gridHelper.visible = showGrid;
456
+ scene.add(gridHelper);
457
+
458
+ scene.remove(axesHelper);
459
+ axesHelper = new THREE.AxesHelper(gridSize / 2);
460
+ axesHelper.visible = showAxes;
461
+ scene.add(axesHelper);
462
+
463
+ // Fit camera to model
464
+ fitToView();
465
+
466
+ // Hide loading
467
+ loading.classList.add('hidden');
468
+ }
469
+
470
+ function showError(message) {
471
+ console.error('Error:', message);
472
+ loading.classList.add('hidden');
473
+ error.style.display = 'block';
474
+ errorMessage.textContent = message;
475
+ }
476
+
477
+ function fitToView() {
478
+ if (!model) return;
479
+
480
+ const box = new THREE.Box3().setFromObject(model);
481
+ const size = box.getSize(new THREE.Vector3());
482
+
483
+ const maxDim = Math.max(size.x, size.y, size.z);
484
+ const fov = camera.fov * (Math.PI / 180);
485
+ cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
486
+
487
+ // Isometric view
488
+ setView(1, 1, 1);
489
+ }
490
+
491
+ function setView(x, y, z) {
492
+ if (!model) return;
493
+
494
+ const box = new THREE.Box3().setFromObject(model);
495
+ const center = box.getCenter(new THREE.Vector3());
496
+
497
+ // Normalize direction
498
+ const dir = new THREE.Vector3(x, y, z).normalize();
499
+
500
+ // Set camera position
501
+ camera.position.copy(center).add(dir.multiplyScalar(cameraDistance));
502
+
503
+ // Set up vector (handle top/bottom views)
504
+ if (Math.abs(y) > 0.9) {
505
+ camera.up.set(0, 0, y > 0 ? -1 : 1);
506
+ } else {
507
+ camera.up.set(0, 1, 0);
508
+ }
509
+
510
+ // Look at center
511
+ controls.target.copy(center);
512
+ camera.lookAt(center);
513
+ controls.update();
514
+ }
515
+
516
+ function toggleWireframe() {
517
+ // Cycle through modes: solid -> wireframe -> both -> solid
518
+ if (viewMode === 'solid') {
519
+ viewMode = 'wireframe';
520
+ } else if (viewMode === 'wireframe') {
521
+ viewMode = 'both';
522
+ } else {
523
+ viewMode = 'solid';
524
+ }
525
+ applyViewMode();
526
+ }
527
+
528
+ function applyViewMode() {
529
+ if (!model) return;
530
+
531
+ // Remove existing wireframe overlay if any
532
+ if (wireframeMesh) {
533
+ scene.remove(wireframeMesh);
534
+ wireframeMesh.traverse((child) => {
535
+ if (child.isMesh) {
536
+ child.geometry.dispose();
537
+ child.material.dispose();
538
+ }
539
+ });
540
+ wireframeMesh = null;
541
+ }
542
+
543
+ if (viewMode === 'solid') {
544
+ // Solid mode: no wireframe
545
+ model.traverse((child) => {
546
+ if (child.isMesh && child.material) {
547
+ if (Array.isArray(child.material)) {
548
+ child.material.forEach(m => m.wireframe = false);
549
+ } else {
550
+ child.material.wireframe = false;
551
+ }
552
+ }
553
+ });
554
+ wireframeBtn.textContent = 'Wire';
555
+ wireframeBtn.classList.remove('active');
556
+ } else if (viewMode === 'wireframe') {
557
+ // Wireframe only mode
558
+ model.traverse((child) => {
559
+ if (child.isMesh && child.material) {
560
+ if (Array.isArray(child.material)) {
561
+ child.material.forEach(m => m.wireframe = true);
562
+ } else {
563
+ child.material.wireframe = true;
564
+ }
565
+ }
566
+ });
567
+ wireframeBtn.textContent = 'Wire';
568
+ wireframeBtn.classList.add('active');
569
+ } else {
570
+ // Both mode: solid + black wireframe overlay
571
+ model.traverse((child) => {
572
+ if (child.isMesh && child.material) {
573
+ if (Array.isArray(child.material)) {
574
+ child.material.forEach(m => m.wireframe = false);
575
+ } else {
576
+ child.material.wireframe = false;
577
+ }
578
+ }
579
+ });
580
+ // Create wireframe overlay
581
+ wireframeMesh = createWireframeOverlay(model);
582
+ if (wireframeMesh) {
583
+ scene.add(wireframeMesh);
584
+ }
585
+ wireframeBtn.textContent = 'Both';
586
+ wireframeBtn.classList.add('active');
587
+ }
588
+ }
589
+
590
+ function createWireframeOverlay(sourceModel) {
591
+ // Create a group to hold wireframe meshes
592
+ const wireframeGroup = new THREE.Group();
593
+ wireframeGroup.position.copy(sourceModel.position);
594
+ wireframeGroup.rotation.copy(sourceModel.rotation);
595
+ wireframeGroup.scale.copy(sourceModel.scale);
596
+
597
+ sourceModel.traverse((child) => {
598
+ if (child.isMesh) {
599
+ const wireGeom = child.geometry.clone();
600
+ const wireMat = new THREE.MeshBasicMaterial({
601
+ color: 0x000000,
602
+ wireframe: true,
603
+ transparent: true,
604
+ opacity: 0.3
605
+ });
606
+ const wireMesh = new THREE.Mesh(wireGeom, wireMat);
607
+ wireMesh.position.copy(child.position);
608
+ wireMesh.rotation.copy(child.rotation);
609
+ wireMesh.scale.copy(child.scale);
610
+ // Slight offset to prevent z-fighting
611
+ wireMesh.renderOrder = 1;
612
+ wireframeGroup.add(wireMesh);
613
+ }
614
+ });
615
+
616
+ return wireframeGroup;
617
+ }
618
+
619
+ function toggleAxes() {
620
+ showAxes = !showAxes;
621
+ axesHelper.visible = showAxes;
622
+ axesBtn.classList.toggle('active', showAxes);
623
+ document.getElementById('axes-legend').style.display = showAxes ? 'block' : 'none';
624
+ }
625
+
626
+ function toggleGrid() {
627
+ showGrid = !showGrid;
628
+ gridHelper.visible = showGrid;
629
+ gridBtn.classList.toggle('active', showGrid);
630
+ }
631
+
632
+ function onResize() {
633
+ camera.aspect = container.clientWidth / container.clientHeight;
634
+ camera.updateProjectionMatrix();
635
+ renderer.setSize(container.clientWidth, container.clientHeight);
636
+ controls.handleResize(); // TrackballControls needs this to update screen dimensions
637
+ }
638
+
639
+ function animate() {
640
+ requestAnimationFrame(animate);
641
+ controls.update();
642
+ renderer.render(scene, camera);
643
+ }
644
+
645
+ // Auto-reload: poll file mtime and reload model when changed
646
+ let lastMtime = null;
647
+ const POLL_INTERVAL = 1000; // 1 second
648
+
649
+ async function checkForChanges() {
650
+ try {
651
+ const res = await fetch('/api/mtime');
652
+ if (res.ok) {
653
+ const data = await res.json();
654
+ if (lastMtime === null) {
655
+ lastMtime = data.mtime;
656
+ } else if (data.mtime !== lastMtime) {
657
+ lastMtime = data.mtime;
658
+ reloadModel();
659
+ }
660
+ }
661
+ } catch (err) {
662
+ // Ignore fetch errors (server may be restarting)
663
+ }
664
+ }
665
+
666
+ function reloadModel() {
667
+ // Remove old wireframe overlay
668
+ if (wireframeMesh) {
669
+ scene.remove(wireframeMesh);
670
+ wireframeMesh.traverse((child) => {
671
+ if (child.isMesh) {
672
+ child.geometry.dispose();
673
+ child.material.dispose();
674
+ }
675
+ });
676
+ wireframeMesh = null;
677
+ }
678
+
679
+ // Remove old model
680
+ if (model) {
681
+ scene.remove(model);
682
+ model.traverse((child) => {
683
+ if (child.isMesh) {
684
+ child.geometry.dispose();
685
+ if (Array.isArray(child.material)) {
686
+ child.material.forEach(m => m.dispose());
687
+ } else if (child.material) {
688
+ child.material.dispose();
689
+ }
690
+ }
691
+ });
692
+ model = null;
693
+ }
694
+
695
+ // Reload based on format
696
+ if (FORMAT === '3mf') {
697
+ reload3MF();
698
+ } else {
699
+ reloadSTL();
700
+ }
701
+ }
702
+
703
+ function reloadSTL() {
704
+ const loader = new STLLoader();
705
+ loader.load('/api/model?t=' + Date.now(), (geometry) => {
706
+ // Center geometry
707
+ geometry.computeBoundingBox();
708
+ const center = new THREE.Vector3();
709
+ geometry.boundingBox.getCenter(center);
710
+ geometry.translate(-center.x, -center.y, -center.z);
711
+
712
+ // Move to sit on grid
713
+ const minY = geometry.boundingBox.min.y - center.y;
714
+ geometry.translate(0, -minY, 0);
715
+
716
+ // Recalculate bounding box after translation
717
+ geometry.computeBoundingBox();
718
+
719
+ // Create new material
720
+ const material = new THREE.MeshPhongMaterial({
721
+ color: 0x3b82f6,
722
+ specular: 0x111111,
723
+ shininess: 30,
724
+ flatShading: false
725
+ });
726
+
727
+ model = new THREE.Mesh(geometry, material);
728
+ scene.add(model);
729
+
730
+ // Re-apply current view mode
731
+ applyViewMode();
732
+
733
+ // Update info
734
+ const triangles = geometry.attributes.position.count / 3;
735
+ info.textContent = ` - ${triangles.toLocaleString()} triangles (reloaded)`;
736
+
737
+ // Brief flash to indicate reload
738
+ setTimeout(() => {
739
+ info.textContent = ` - ${triangles.toLocaleString()} triangles`;
740
+ }, 1000);
741
+ }, undefined, (err) => {
742
+ console.error('Error reloading STL:', err);
743
+ });
744
+ }
745
+
746
+ function reload3MF() {
747
+ const loader = new ThreeMFLoader();
748
+ loader.load('/api/model?t=' + Date.now(), (group) => {
749
+ // Z-up to Y-up rotation
750
+ group.rotation.set(-Math.PI / 2, 0, 0);
751
+
752
+ // Center the group
753
+ const box = new THREE.Box3().setFromObject(group);
754
+ const center = box.getCenter(new THREE.Vector3());
755
+ group.position.sub(center);
756
+
757
+ // Move to sit on grid
758
+ const newBox = new THREE.Box3().setFromObject(group);
759
+ group.position.y -= newBox.min.y;
760
+
761
+ model = group;
762
+ scene.add(model);
763
+
764
+ // Re-apply current view mode
765
+ applyViewMode();
766
+
767
+ // Count triangles and objects
768
+ let triangleCount = 0;
769
+ let objectCount = 0;
770
+ group.traverse((child) => {
771
+ if (child.isMesh) {
772
+ objectCount++;
773
+ const geom = child.geometry;
774
+ if (geom.index) {
775
+ triangleCount += geom.index.count / 3;
776
+ } else if (geom.attributes.position) {
777
+ triangleCount += geom.attributes.position.count / 3;
778
+ }
779
+ }
780
+ });
781
+
782
+ info.textContent = ` - ${triangleCount.toLocaleString()} triangles, ${objectCount} object${objectCount !== 1 ? 's' : ''} (reloaded)`;
783
+
784
+ setTimeout(() => {
785
+ info.textContent = ` - ${triangleCount.toLocaleString()} triangles, ${objectCount} object${objectCount !== 1 ? 's' : ''}`;
786
+ }, 1000);
787
+ }, undefined, (err) => {
788
+ console.error('Error reloading 3MF:', err);
789
+ });
790
+ }
791
+
792
+ // Start polling for changes
793
+ setInterval(checkForChanges, POLL_INTERVAL);
794
+
795
+ // Start
796
+ init();
797
+ </script>
798
+ </body>
799
+ </html>