@cluesmith/codev 1.5.1 → 1.5.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.
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +4 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/consult.d.ts.map +1 -1
- package/dist/agent-farm/commands/consult.js +2 -4
- package/dist/agent-farm/commands/consult.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts +18 -0
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +152 -3
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/commands/stop.d.ts.map +1 -1
- package/dist/agent-farm/commands/stop.js +69 -1
- package/dist/agent-farm/commands/stop.js.map +1 -1
- package/dist/agent-farm/servers/dashboard-server.js +77 -0
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/open-server.js +51 -10
- package/dist/agent-farm/servers/open-server.js.map +1 -1
- package/dist/agent-farm/types.d.ts +2 -0
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/terminal-ports.d.ts +18 -0
- package/dist/agent-farm/utils/terminal-ports.d.ts.map +1 -0
- package/dist/agent-farm/utils/terminal-ports.js +35 -0
- package/dist/agent-farm/utils/terminal-ports.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +8 -0
- package/dist/commands/update.js.map +1 -1
- package/package.json +3 -1
- package/templates/3d-viewer.html +799 -0
- package/templates/dashboard/js/tabs.js +49 -4
- package/templates/open.html +1 -0
- package/templates/stl-viewer.html +0 -528
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/templates/open.html
CHANGED
|
@@ -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
|
};
|
|
@@ -1,528 +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/TrackballControls.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 - TrackballControls uses quaternions, eliminating gimbal lock
|
|
276
|
-
controls = new THREE.TrackballControls(camera, renderer.domElement);
|
|
277
|
-
controls.rotateSpeed = 2.0;
|
|
278
|
-
controls.zoomSpeed = 1.2;
|
|
279
|
-
controls.panSpeed = 0.8;
|
|
280
|
-
controls.staticMoving = true; // No inertia for precise control
|
|
281
|
-
controls.dynamicDampingFactor = 0.3;
|
|
282
|
-
|
|
283
|
-
// Lighting
|
|
284
|
-
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
|
285
|
-
scene.add(ambientLight);
|
|
286
|
-
|
|
287
|
-
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
288
|
-
directionalLight1.position.set(1, 1, 1);
|
|
289
|
-
scene.add(directionalLight1);
|
|
290
|
-
|
|
291
|
-
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
292
|
-
directionalLight2.position.set(-1, -1, -1);
|
|
293
|
-
scene.add(directionalLight2);
|
|
294
|
-
|
|
295
|
-
// Grid
|
|
296
|
-
gridHelper = new THREE.GridHelper(200, 20, 0x0f3460, 0x0f3460);
|
|
297
|
-
scene.add(gridHelper);
|
|
298
|
-
|
|
299
|
-
// Axes helper (X=red, Y=green, Z=blue)
|
|
300
|
-
axesHelper = new THREE.AxesHelper(50);
|
|
301
|
-
scene.add(axesHelper);
|
|
302
|
-
|
|
303
|
-
// Load STL
|
|
304
|
-
loadSTL();
|
|
305
|
-
|
|
306
|
-
// Event listeners
|
|
307
|
-
window.addEventListener('resize', onResize);
|
|
308
|
-
|
|
309
|
-
// Control buttons
|
|
310
|
-
resetBtn.addEventListener('click', fitToView);
|
|
311
|
-
wireframeBtn.addEventListener('click', toggleWireframe);
|
|
312
|
-
axesBtn.addEventListener('click', toggleAxes);
|
|
313
|
-
gridBtn.addEventListener('click', toggleGrid);
|
|
314
|
-
|
|
315
|
-
// View buttons
|
|
316
|
-
viewTop.addEventListener('click', () => setView(0, 1, 0));
|
|
317
|
-
viewBottom.addEventListener('click', () => setView(0, -1, 0));
|
|
318
|
-
viewFront.addEventListener('click', () => setView(0, 0, 1));
|
|
319
|
-
viewBack.addEventListener('click', () => setView(0, 0, -1));
|
|
320
|
-
viewRight.addEventListener('click', () => setView(1, 0, 0));
|
|
321
|
-
viewLeft.addEventListener('click', () => setView(-1, 0, 0));
|
|
322
|
-
viewIso.addEventListener('click', () => setView(1, 1, 1));
|
|
323
|
-
|
|
324
|
-
// Animation loop
|
|
325
|
-
animate();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function loadSTL() {
|
|
329
|
-
const loader = new THREE.STLLoader();
|
|
330
|
-
|
|
331
|
-
// Load from API endpoint
|
|
332
|
-
loader.load(
|
|
333
|
-
'/api/stl',
|
|
334
|
-
(geometry) => {
|
|
335
|
-
// Center geometry
|
|
336
|
-
geometry.computeBoundingBox();
|
|
337
|
-
const center = new THREE.Vector3();
|
|
338
|
-
geometry.boundingBox.getCenter(center);
|
|
339
|
-
geometry.translate(-center.x, -center.y, -center.z);
|
|
340
|
-
|
|
341
|
-
// Move to sit on grid (optional - comment out to keep centered)
|
|
342
|
-
const minY = geometry.boundingBox.min.y - center.y;
|
|
343
|
-
geometry.translate(0, -minY, 0);
|
|
344
|
-
|
|
345
|
-
// Recalculate bounding box after translation
|
|
346
|
-
geometry.computeBoundingBox();
|
|
347
|
-
|
|
348
|
-
// Material
|
|
349
|
-
const material = new THREE.MeshPhongMaterial({
|
|
350
|
-
color: 0x3b82f6,
|
|
351
|
-
specular: 0x111111,
|
|
352
|
-
shininess: 30,
|
|
353
|
-
flatShading: false
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
mesh = new THREE.Mesh(geometry, material);
|
|
357
|
-
scene.add(mesh);
|
|
358
|
-
|
|
359
|
-
// Calculate model center and size
|
|
360
|
-
const box = new THREE.Box3().setFromObject(mesh);
|
|
361
|
-
box.getCenter(modelCenter);
|
|
362
|
-
const size = box.getSize(new THREE.Vector3());
|
|
363
|
-
cameraDistance = Math.max(size.x, size.y, size.z) * 2;
|
|
364
|
-
|
|
365
|
-
// Scale grid and axes to model size
|
|
366
|
-
const maxDim = Math.max(size.x, size.y, size.z);
|
|
367
|
-
const gridSize = Math.ceil(maxDim * 2 / 10) * 10; // Round up to nearest 10
|
|
368
|
-
scene.remove(gridHelper);
|
|
369
|
-
gridHelper = new THREE.GridHelper(gridSize, gridSize / 5, 0x0f3460, 0x0f3460);
|
|
370
|
-
scene.add(gridHelper);
|
|
371
|
-
|
|
372
|
-
scene.remove(axesHelper);
|
|
373
|
-
axesHelper = new THREE.AxesHelper(gridSize / 2);
|
|
374
|
-
scene.add(axesHelper);
|
|
375
|
-
|
|
376
|
-
// Fit camera to model
|
|
377
|
-
fitToView();
|
|
378
|
-
|
|
379
|
-
// Update info
|
|
380
|
-
const triangles = geometry.attributes.position.count / 3;
|
|
381
|
-
info.textContent = ` — ${triangles.toLocaleString()} triangles`;
|
|
382
|
-
|
|
383
|
-
// Hide loading
|
|
384
|
-
loading.classList.add('hidden');
|
|
385
|
-
},
|
|
386
|
-
(progress) => {
|
|
387
|
-
// Progress callback (optional)
|
|
388
|
-
},
|
|
389
|
-
(err) => {
|
|
390
|
-
console.error('Error loading STL:', err);
|
|
391
|
-
loading.classList.add('hidden');
|
|
392
|
-
error.style.display = 'block';
|
|
393
|
-
errorMessage.textContent = 'Failed to load STL file: ' + (err.message || 'Unknown error');
|
|
394
|
-
}
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function fitToView() {
|
|
399
|
-
if (!mesh) return;
|
|
400
|
-
|
|
401
|
-
const box = new THREE.Box3().setFromObject(mesh);
|
|
402
|
-
const size = box.getSize(new THREE.Vector3());
|
|
403
|
-
const center = box.getCenter(new THREE.Vector3());
|
|
404
|
-
|
|
405
|
-
const maxDim = Math.max(size.x, size.y, size.z);
|
|
406
|
-
const fov = camera.fov * (Math.PI / 180);
|
|
407
|
-
cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
|
|
408
|
-
|
|
409
|
-
// Isometric view
|
|
410
|
-
setView(1, 1, 1);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function setView(x, y, z) {
|
|
414
|
-
if (!mesh) return;
|
|
415
|
-
|
|
416
|
-
const box = new THREE.Box3().setFromObject(mesh);
|
|
417
|
-
const center = box.getCenter(new THREE.Vector3());
|
|
418
|
-
|
|
419
|
-
// Normalize direction
|
|
420
|
-
const dir = new THREE.Vector3(x, y, z).normalize();
|
|
421
|
-
|
|
422
|
-
// Set camera position
|
|
423
|
-
camera.position.copy(center).add(dir.multiplyScalar(cameraDistance));
|
|
424
|
-
|
|
425
|
-
// Set up vector (handle top/bottom views)
|
|
426
|
-
if (Math.abs(y) > 0.9) {
|
|
427
|
-
camera.up.set(0, 0, y > 0 ? -1 : 1);
|
|
428
|
-
} else {
|
|
429
|
-
camera.up.set(0, 1, 0);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Look at center
|
|
433
|
-
controls.target.copy(center);
|
|
434
|
-
camera.lookAt(center);
|
|
435
|
-
controls.update();
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function toggleWireframe() {
|
|
439
|
-
wireframeMode = !wireframeMode;
|
|
440
|
-
if (mesh) {
|
|
441
|
-
mesh.material.wireframe = wireframeMode;
|
|
442
|
-
}
|
|
443
|
-
wireframeBtn.classList.toggle('active', wireframeMode);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function toggleAxes() {
|
|
447
|
-
showAxes = !showAxes;
|
|
448
|
-
axesHelper.visible = showAxes;
|
|
449
|
-
axesBtn.classList.toggle('active', showAxes);
|
|
450
|
-
document.getElementById('axes-legend').style.display = showAxes ? 'block' : 'none';
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function toggleGrid() {
|
|
454
|
-
showGrid = !showGrid;
|
|
455
|
-
gridHelper.visible = showGrid;
|
|
456
|
-
gridBtn.classList.toggle('active', showGrid);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function onResize() {
|
|
460
|
-
camera.aspect = container.clientWidth / container.clientHeight;
|
|
461
|
-
camera.updateProjectionMatrix();
|
|
462
|
-
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
463
|
-
controls.handleResize(); // TrackballControls needs this to update screen dimensions
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function animate() {
|
|
467
|
-
requestAnimationFrame(animate);
|
|
468
|
-
controls.update();
|
|
469
|
-
renderer.render(scene, camera);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Auto-reload: poll file mtime and reload STL when changed
|
|
473
|
-
let lastMtime = null;
|
|
474
|
-
const POLL_INTERVAL = 1000; // 1 second
|
|
475
|
-
|
|
476
|
-
async function checkForChanges() {
|
|
477
|
-
try {
|
|
478
|
-
const res = await fetch('/api/mtime');
|
|
479
|
-
if (res.ok) {
|
|
480
|
-
const data = await res.json();
|
|
481
|
-
if (lastMtime === null) {
|
|
482
|
-
lastMtime = data.mtime;
|
|
483
|
-
} else if (data.mtime !== lastMtime) {
|
|
484
|
-
lastMtime = data.mtime;
|
|
485
|
-
reloadSTL();
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
} catch (err) {
|
|
489
|
-
// Ignore fetch errors (server may be restarting)
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function reloadSTL() {
|
|
494
|
-
// Remove old mesh
|
|
495
|
-
if (mesh) {
|
|
496
|
-
scene.remove(mesh);
|
|
497
|
-
mesh.geometry.dispose();
|
|
498
|
-
mesh.material.dispose();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Reload STL with cache-busting
|
|
502
|
-
const loader = new THREE.STLLoader();
|
|
503
|
-
loader.load('/api/stl?t=' + Date.now(), (geometry) => {
|
|
504
|
-
geometry.computeVertexNormals();
|
|
505
|
-
geometry.center();
|
|
506
|
-
|
|
507
|
-
mesh = new THREE.Mesh(geometry, material);
|
|
508
|
-
scene.add(mesh);
|
|
509
|
-
|
|
510
|
-
// Update info
|
|
511
|
-
const triangles = geometry.attributes.position.count / 3;
|
|
512
|
-
document.getElementById('info').textContent = triangles.toLocaleString() + ' triangles (reloaded)';
|
|
513
|
-
|
|
514
|
-
// Brief flash to indicate reload
|
|
515
|
-
setTimeout(() => {
|
|
516
|
-
document.getElementById('info').textContent = triangles.toLocaleString() + ' triangles';
|
|
517
|
-
}, 1000);
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Start polling for changes
|
|
522
|
-
setInterval(checkForChanges, POLL_INTERVAL);
|
|
523
|
-
|
|
524
|
-
// Start
|
|
525
|
-
init();
|
|
526
|
-
</script>
|
|
527
|
-
</body>
|
|
528
|
-
</html>
|