@cluesmith/codev 1.5.13 → 1.5.14
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/package.json +1 -1
- package/templates/3d-viewer.html +123 -58
- package/templates/open.html +51 -9
package/package.json
CHANGED
package/templates/3d-viewer.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
body {
|
|
15
|
-
background: #
|
|
15
|
+
background: #000000;
|
|
16
16
|
color: #e0e0e0;
|
|
17
17
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
18
18
|
overflow: hidden;
|
|
@@ -91,12 +91,17 @@
|
|
|
91
91
|
font-weight: bold;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/* Color view buttons to match axes: X=red, Y=green, Z=blue */
|
|
95
|
+
button.view-btn.axis-x {
|
|
96
|
+
color: #ef4444;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
button.view-btn.axis-y {
|
|
100
|
+
color: #22c55e;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
|
-
button.view-btn.
|
|
99
|
-
color: #
|
|
103
|
+
button.view-btn.axis-z {
|
|
104
|
+
color: #3b82f6;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
#canvas-container {
|
|
@@ -177,15 +182,15 @@
|
|
|
177
182
|
<div id="controls">
|
|
178
183
|
<div class="control-group">
|
|
179
184
|
<span class="control-label">View:</span>
|
|
180
|
-
<button id="
|
|
181
|
-
<button id="
|
|
182
|
-
<button id="viewFront" class="view-btn
|
|
183
|
-
<button id="viewBack" class="view-btn
|
|
184
|
-
<button id="
|
|
185
|
-
<button id="
|
|
185
|
+
<button id="viewRight" class="view-btn axis-x" title="Right (from +X)">Right</button>
|
|
186
|
+
<button id="viewLeft" class="view-btn axis-x" title="Left (from -X)">Left</button>
|
|
187
|
+
<button id="viewFront" class="view-btn axis-y" title="Front (from -Y)">Front</button>
|
|
188
|
+
<button id="viewBack" class="view-btn axis-y" title="Back (from +Y)">Back</button>
|
|
189
|
+
<button id="viewTop" class="view-btn axis-z" title="Top (looking down Z)">Top</button>
|
|
190
|
+
<button id="viewBottom" class="view-btn axis-z" title="Bottom (looking up)">Bot</button>
|
|
186
191
|
</div>
|
|
187
192
|
<div class="control-group">
|
|
188
|
-
<button id="viewIso" title="
|
|
193
|
+
<button id="viewIso" title="Toggle perspective/orthographic">Persp</button>
|
|
189
194
|
<button id="resetBtn" title="Fit model to view">Fit</button>
|
|
190
195
|
</div>
|
|
191
196
|
<div class="control-group">
|
|
@@ -207,9 +212,9 @@
|
|
|
207
212
|
<div id="error-message">Failed to load 3D model</div>
|
|
208
213
|
</div>
|
|
209
214
|
<div id="axes-legend">
|
|
210
|
-
<div class="axis-x">X
|
|
211
|
-
<div class="axis-y">Y
|
|
212
|
-
<div class="axis-z">Z
|
|
215
|
+
<div class="axis-x">X → Right</div>
|
|
216
|
+
<div class="axis-y">Y → Front</div>
|
|
217
|
+
<div class="axis-z">Z → Up</div>
|
|
213
218
|
</div>
|
|
214
219
|
</div>
|
|
215
220
|
|
|
@@ -235,13 +240,14 @@
|
|
|
235
240
|
const FORMAT = '{{FORMAT}}'; // 'stl' or '3mf'
|
|
236
241
|
|
|
237
242
|
// Three.js setup
|
|
238
|
-
let scene, camera, renderer, controls;
|
|
243
|
+
let scene, camera, orthoCamera, perspCamera, renderer, controls;
|
|
239
244
|
let model = null; // Can be mesh (STL) or group (3MF)
|
|
240
245
|
let wireframeMesh = null; // Wireframe overlay mesh
|
|
241
246
|
let axesHelper, gridHelper;
|
|
242
247
|
let viewMode = 'solid'; // 'solid', 'wireframe', or 'both'
|
|
243
248
|
let showAxes = true;
|
|
244
249
|
let showGrid = true;
|
|
250
|
+
let isOrthographic = false;
|
|
245
251
|
let modelCenter = new THREE.Vector3();
|
|
246
252
|
let cameraDistance = 100;
|
|
247
253
|
|
|
@@ -270,17 +276,25 @@
|
|
|
270
276
|
function init() {
|
|
271
277
|
// Scene
|
|
272
278
|
scene = new THREE.Scene();
|
|
273
|
-
scene.background = new THREE.Color(
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
scene.background = new THREE.Color(0x000000);
|
|
280
|
+
|
|
281
|
+
// Cameras (Z-up coordinate system)
|
|
282
|
+
const aspect = container.clientWidth / container.clientHeight;
|
|
283
|
+
perspCamera = new THREE.PerspectiveCamera(45, aspect, 0.1, 10000);
|
|
284
|
+
perspCamera.up.set(0, 0, 1); // Z is up
|
|
285
|
+
perspCamera.position.set(100, 100, 100);
|
|
286
|
+
|
|
287
|
+
// Orthographic camera (size will be adjusted when model loads)
|
|
288
|
+
const frustumSize = 100;
|
|
289
|
+
orthoCamera = new THREE.OrthographicCamera(
|
|
290
|
+
-frustumSize * aspect / 2, frustumSize * aspect / 2,
|
|
291
|
+
frustumSize / 2, -frustumSize / 2,
|
|
292
|
+
0.1, 10000
|
|
281
293
|
);
|
|
282
|
-
|
|
283
|
-
|
|
294
|
+
orthoCamera.up.set(0, 0, 1);
|
|
295
|
+
orthoCamera.position.set(100, 100, 100);
|
|
296
|
+
|
|
297
|
+
camera = perspCamera; // Start with perspective
|
|
284
298
|
|
|
285
299
|
// Renderer
|
|
286
300
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
@@ -307,12 +321,15 @@
|
|
|
307
321
|
directionalLight2.position.set(-1, -1, -1);
|
|
308
322
|
scene.add(directionalLight2);
|
|
309
323
|
|
|
310
|
-
// Grid
|
|
311
|
-
|
|
312
|
-
gridHelper
|
|
324
|
+
// Grid on XY plane (Z=0) for Z-up coordinate system
|
|
325
|
+
// GridHelper(size, divisions) - 10mm squares, 200mm total = 20 divisions
|
|
326
|
+
gridHelper = new THREE.GridHelper(200, 20, 0x808080, 0x808080);
|
|
327
|
+
gridHelper.rotation.x = Math.PI / 2; // Rotate from XZ to XY plane
|
|
328
|
+
gridHelper.material.transparent = true;
|
|
329
|
+
gridHelper.material.opacity = 0.5;
|
|
313
330
|
scene.add(gridHelper);
|
|
314
331
|
|
|
315
|
-
// Axes helper
|
|
332
|
+
// Axes helper at origin
|
|
316
333
|
axesHelper = new THREE.AxesHelper(50);
|
|
317
334
|
scene.add(axesHelper);
|
|
318
335
|
|
|
@@ -328,14 +345,14 @@
|
|
|
328
345
|
axesBtn.addEventListener('click', toggleAxes);
|
|
329
346
|
gridBtn.addEventListener('click', toggleGrid);
|
|
330
347
|
|
|
331
|
-
// View buttons (Z-up coordinate system: X=
|
|
348
|
+
// View buttons (Z-up coordinate system: X=right, Y=front, Z=up)
|
|
332
349
|
viewTop.addEventListener('click', () => setView(0, 0, 1)); // Looking down from +Z
|
|
333
350
|
viewBottom.addEventListener('click', () => setView(0, 0, -1)); // Looking up from -Z
|
|
334
|
-
viewFront.addEventListener('click', () => setView(0, -1, 0)); // Looking from -Y
|
|
335
|
-
viewBack.addEventListener('click', () => setView(0, 1, 0)); // Looking from +Y
|
|
336
|
-
viewRight.addEventListener('click', () => setView(1, 0, 0)); // Looking from +X
|
|
337
|
-
viewLeft.addEventListener('click', () => setView(-1, 0, 0)); // Looking from -X
|
|
338
|
-
viewIso.addEventListener('click',
|
|
351
|
+
viewFront.addEventListener('click', () => setView(0, -1, 0)); // Looking from -Y
|
|
352
|
+
viewBack.addEventListener('click', () => setView(0, 1, 0)); // Looking from +Y
|
|
353
|
+
viewRight.addEventListener('click', () => setView(1, 0, 0)); // Looking from +X (right side)
|
|
354
|
+
viewLeft.addEventListener('click', () => setView(-1, 0, 0)); // Looking from -X (left side)
|
|
355
|
+
viewIso.addEventListener('click', toggleOrthographic); // Toggle perspective/orthographic
|
|
339
356
|
|
|
340
357
|
// Animation loop
|
|
341
358
|
animate();
|
|
@@ -402,15 +419,7 @@
|
|
|
402
419
|
(group) => {
|
|
403
420
|
// 3MFLoader returns a Group with meshes
|
|
404
421
|
// 3MF uses Z-up natively, which matches our coordinate system
|
|
405
|
-
|
|
406
|
-
// Center the group in XY
|
|
407
|
-
const box = new THREE.Box3().setFromObject(group);
|
|
408
|
-
const center = box.getCenter(new THREE.Vector3());
|
|
409
|
-
group.position.sub(center);
|
|
410
|
-
|
|
411
|
-
// Move to sit on grid (Z=0 plane)
|
|
412
|
-
const newBox = new THREE.Box3().setFromObject(group);
|
|
413
|
-
group.position.z -= newBox.min.z;
|
|
422
|
+
// Keep model at original position (don't center) to preserve coordinate alignment
|
|
414
423
|
|
|
415
424
|
model = group;
|
|
416
425
|
scene.add(model);
|
|
@@ -446,14 +455,26 @@
|
|
|
446
455
|
const box = new THREE.Box3().setFromObject(object);
|
|
447
456
|
box.getCenter(modelCenter);
|
|
448
457
|
const size = box.getSize(new THREE.Vector3());
|
|
449
|
-
cameraDistance = Math.max(size.x, size.y, size.z) * 2;
|
|
450
|
-
|
|
451
|
-
// Scale grid and axes to model size
|
|
452
458
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
453
|
-
|
|
459
|
+
cameraDistance = maxDim * 2;
|
|
460
|
+
|
|
461
|
+
// Configure orthographic camera frustum based on model size
|
|
462
|
+
const aspect = container.clientWidth / container.clientHeight;
|
|
463
|
+
const frustumSize = maxDim * 1.5;
|
|
464
|
+
orthoCamera.left = -frustumSize * aspect / 2;
|
|
465
|
+
orthoCamera.right = frustumSize * aspect / 2;
|
|
466
|
+
orthoCamera.top = frustumSize / 2;
|
|
467
|
+
orthoCamera.bottom = -frustumSize / 2;
|
|
468
|
+
orthoCamera.updateProjectionMatrix();
|
|
469
|
+
|
|
470
|
+
// Scale grid and axes to model size - always 10mm squares
|
|
471
|
+
const gridSize = Math.ceil(maxDim * 2 / 10) * 10; // Round up to nearest 10mm
|
|
472
|
+
const divisions = gridSize / 10; // 10mm per square
|
|
454
473
|
scene.remove(gridHelper);
|
|
455
|
-
gridHelper = new THREE.GridHelper(gridSize,
|
|
456
|
-
gridHelper.rotation.x = Math.PI / 2; // Rotate to XY plane
|
|
474
|
+
gridHelper = new THREE.GridHelper(gridSize, divisions, 0x808080, 0x808080);
|
|
475
|
+
gridHelper.rotation.x = Math.PI / 2; // Rotate from XZ to XY plane
|
|
476
|
+
gridHelper.material.transparent = true;
|
|
477
|
+
gridHelper.material.opacity = 0.5;
|
|
457
478
|
gridHelper.visible = showGrid;
|
|
458
479
|
scene.add(gridHelper);
|
|
459
480
|
|
|
@@ -462,8 +483,9 @@
|
|
|
462
483
|
axesHelper.visible = showAxes;
|
|
463
484
|
scene.add(axesHelper);
|
|
464
485
|
|
|
465
|
-
//
|
|
466
|
-
|
|
486
|
+
// Set camera to look at model center, default front view
|
|
487
|
+
controls.target.copy(modelCenter);
|
|
488
|
+
setView(0, -1, 0); // Front view
|
|
467
489
|
|
|
468
490
|
// Hide loading
|
|
469
491
|
loading.classList.add('hidden');
|
|
@@ -481,13 +503,46 @@
|
|
|
481
503
|
|
|
482
504
|
const box = new THREE.Box3().setFromObject(model);
|
|
483
505
|
const size = box.getSize(new THREE.Vector3());
|
|
506
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
484
507
|
|
|
485
508
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
486
|
-
const fov =
|
|
509
|
+
const fov = perspCamera.fov * (Math.PI / 180);
|
|
487
510
|
cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
|
|
488
511
|
|
|
489
|
-
//
|
|
490
|
-
|
|
512
|
+
// Update orthographic frustum
|
|
513
|
+
const aspect = container.clientWidth / container.clientHeight;
|
|
514
|
+
const frustumSize = maxDim * 1.5;
|
|
515
|
+
orthoCamera.left = -frustumSize * aspect / 2;
|
|
516
|
+
orthoCamera.right = frustumSize * aspect / 2;
|
|
517
|
+
orthoCamera.top = frustumSize / 2;
|
|
518
|
+
orthoCamera.bottom = -frustumSize / 2;
|
|
519
|
+
orthoCamera.updateProjectionMatrix();
|
|
520
|
+
|
|
521
|
+
// Keep current view direction, just adjust distance
|
|
522
|
+
const currentDir = new THREE.Vector3().subVectors(camera.position, controls.target).normalize();
|
|
523
|
+
camera.position.copy(center).add(currentDir.multiplyScalar(cameraDistance));
|
|
524
|
+
controls.target.copy(center);
|
|
525
|
+
controls.update();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function toggleOrthographic() {
|
|
529
|
+
isOrthographic = !isOrthographic;
|
|
530
|
+
|
|
531
|
+
// Copy position and orientation from current camera to the other
|
|
532
|
+
const newCamera = isOrthographic ? orthoCamera : perspCamera;
|
|
533
|
+
newCamera.position.copy(camera.position);
|
|
534
|
+
newCamera.up.copy(camera.up);
|
|
535
|
+
newCamera.lookAt(controls.target);
|
|
536
|
+
|
|
537
|
+
camera = newCamera;
|
|
538
|
+
|
|
539
|
+
// Update controls to use new camera
|
|
540
|
+
controls.object = camera;
|
|
541
|
+
controls.update();
|
|
542
|
+
|
|
543
|
+
// Update button state
|
|
544
|
+
viewIso.classList.toggle('active', isOrthographic);
|
|
545
|
+
viewIso.textContent = isOrthographic ? 'Ortho' : 'Persp';
|
|
491
546
|
}
|
|
492
547
|
|
|
493
548
|
function setView(x, y, z) {
|
|
@@ -633,8 +688,18 @@
|
|
|
633
688
|
}
|
|
634
689
|
|
|
635
690
|
function onResize() {
|
|
636
|
-
|
|
637
|
-
|
|
691
|
+
const aspect = container.clientWidth / container.clientHeight;
|
|
692
|
+
|
|
693
|
+
// Update perspective camera
|
|
694
|
+
perspCamera.aspect = aspect;
|
|
695
|
+
perspCamera.updateProjectionMatrix();
|
|
696
|
+
|
|
697
|
+
// Update orthographic camera (maintain frustum height, adjust width)
|
|
698
|
+
const frustumHeight = orthoCamera.top - orthoCamera.bottom;
|
|
699
|
+
orthoCamera.left = -frustumHeight * aspect / 2;
|
|
700
|
+
orthoCamera.right = frustumHeight * aspect / 2;
|
|
701
|
+
orthoCamera.updateProjectionMatrix();
|
|
702
|
+
|
|
638
703
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
639
704
|
controls.handleResize(); // TrackballControls needs this to update screen dimensions
|
|
640
705
|
}
|
package/templates/open.html
CHANGED
|
@@ -182,11 +182,34 @@
|
|
|
182
182
|
color: #888;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/* Editor
|
|
186
|
-
#editor {
|
|
185
|
+
/* Editor container with line numbers */
|
|
186
|
+
#editor-container {
|
|
187
187
|
display: none;
|
|
188
|
-
width: 100%;
|
|
189
188
|
height: calc(100vh - 80px);
|
|
189
|
+
position: relative;
|
|
190
|
+
}
|
|
191
|
+
#editor-line-numbers {
|
|
192
|
+
position: absolute;
|
|
193
|
+
left: 0;
|
|
194
|
+
top: 0;
|
|
195
|
+
bottom: 0;
|
|
196
|
+
width: 50px;
|
|
197
|
+
background: #252525;
|
|
198
|
+
color: #666;
|
|
199
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
200
|
+
font-size: 13px;
|
|
201
|
+
line-height: 1.5;
|
|
202
|
+
padding: 15px 8px 15px 0;
|
|
203
|
+
text-align: right;
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
border-right: 1px solid #333;
|
|
206
|
+
user-select: none;
|
|
207
|
+
box-sizing: border-box;
|
|
208
|
+
}
|
|
209
|
+
#editor {
|
|
210
|
+
width: calc(100% - 50px);
|
|
211
|
+
height: 100%;
|
|
212
|
+
margin-left: 50px;
|
|
190
213
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
191
214
|
font-size: 13px;
|
|
192
215
|
line-height: 1.5;
|
|
@@ -473,8 +496,11 @@
|
|
|
473
496
|
</div>
|
|
474
497
|
</div>
|
|
475
498
|
|
|
476
|
-
<!-- Editor mode -->
|
|
477
|
-
<
|
|
499
|
+
<!-- Editor mode with line numbers -->
|
|
500
|
+
<div id="editor-container">
|
|
501
|
+
<div id="editor-line-numbers"></div>
|
|
502
|
+
<textarea id="editor" spellcheck="false"></textarea>
|
|
503
|
+
</div>
|
|
478
504
|
|
|
479
505
|
<!-- Search Bar -->
|
|
480
506
|
<div class="search-bar" id="searchBar">
|
|
@@ -1279,6 +1305,7 @@
|
|
|
1279
1305
|
|
|
1280
1306
|
async function toggleEditMode() {
|
|
1281
1307
|
const viewMode = document.getElementById('viewMode');
|
|
1308
|
+
const editorContainer = document.getElementById('editor-container');
|
|
1282
1309
|
const editor = document.getElementById('editor');
|
|
1283
1310
|
const editBtn = document.getElementById('editBtn');
|
|
1284
1311
|
const saveBtn = document.getElementById('saveBtn');
|
|
@@ -1302,14 +1329,15 @@
|
|
|
1302
1329
|
// to ensure Cancel always reverts to the true disk state
|
|
1303
1330
|
editor.value = currentContent;
|
|
1304
1331
|
viewMode.style.display = 'none';
|
|
1305
|
-
|
|
1332
|
+
editorContainer.style.display = 'block';
|
|
1306
1333
|
editBtn.textContent = 'Switch to Annotate';
|
|
1307
1334
|
subtitle.textContent = 'Edit the file directly by clicking and typing.';
|
|
1308
1335
|
saveBtn.style.display = 'inline-block';
|
|
1309
1336
|
cancelBtn.style.display = 'inline-block';
|
|
1310
1337
|
editMode = true;
|
|
1311
1338
|
|
|
1312
|
-
//
|
|
1339
|
+
// Update line numbers and apply scroll position
|
|
1340
|
+
updateEditorLineNumbers();
|
|
1313
1341
|
editor.scrollTop = scrollTop;
|
|
1314
1342
|
editor.focus();
|
|
1315
1343
|
} else {
|
|
@@ -1324,7 +1352,7 @@
|
|
|
1324
1352
|
|
|
1325
1353
|
currentContent = editor.value;
|
|
1326
1354
|
fileLines = currentContent.split('\n');
|
|
1327
|
-
|
|
1355
|
+
editorContainer.style.display = 'none';
|
|
1328
1356
|
viewMode.style.display = 'grid';
|
|
1329
1357
|
editBtn.textContent = 'Switch to Editing';
|
|
1330
1358
|
subtitle.textContent = 'Click on a line number to leave an annotation.';
|
|
@@ -1343,6 +1371,19 @@
|
|
|
1343
1371
|
}
|
|
1344
1372
|
}
|
|
1345
1373
|
|
|
1374
|
+
// Update line numbers in editor
|
|
1375
|
+
function updateEditorLineNumbers() {
|
|
1376
|
+
const editor = document.getElementById('editor');
|
|
1377
|
+
const lineNumbers = document.getElementById('editor-line-numbers');
|
|
1378
|
+
const lines = editor.value.split('\n');
|
|
1379
|
+
lineNumbers.innerHTML = lines.map((_, i) => `<div>${i + 1}</div>`).join('');
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Sync line numbers scroll with editor scroll
|
|
1383
|
+
document.getElementById('editor').addEventListener('scroll', function() {
|
|
1384
|
+
document.getElementById('editor-line-numbers').scrollTop = this.scrollTop;
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1346
1387
|
async function saveEdit() {
|
|
1347
1388
|
const editor = document.getElementById('editor');
|
|
1348
1389
|
const content = editor.value;
|
|
@@ -1398,10 +1439,11 @@
|
|
|
1398
1439
|
setTimeout(() => notification.remove(), 3000);
|
|
1399
1440
|
}
|
|
1400
1441
|
|
|
1401
|
-
// Track unsaved changes in editor
|
|
1442
|
+
// Track unsaved changes in editor and update line numbers
|
|
1402
1443
|
document.getElementById('editor').addEventListener('input', () => {
|
|
1403
1444
|
hasUnsavedChanges = document.getElementById('editor').value !== originalContent;
|
|
1404
1445
|
updateUnsavedIndicator();
|
|
1446
|
+
updateEditorLineNumbers();
|
|
1405
1447
|
});
|
|
1406
1448
|
|
|
1407
1449
|
// Tab key handling for indentation
|