@cluesmith/codev 1.5.12 → 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 +143 -76
- 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,16 +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
|
-
|
|
294
|
+
orthoCamera.up.set(0, 0, 1);
|
|
295
|
+
orthoCamera.position.set(100, 100, 100);
|
|
296
|
+
|
|
297
|
+
camera = perspCamera; // Start with perspective
|
|
283
298
|
|
|
284
299
|
// Renderer
|
|
285
300
|
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
@@ -306,11 +321,15 @@
|
|
|
306
321
|
directionalLight2.position.set(-1, -1, -1);
|
|
307
322
|
scene.add(directionalLight2);
|
|
308
323
|
|
|
309
|
-
// Grid
|
|
310
|
-
|
|
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;
|
|
311
330
|
scene.add(gridHelper);
|
|
312
331
|
|
|
313
|
-
// Axes helper
|
|
332
|
+
// Axes helper at origin
|
|
314
333
|
axesHelper = new THREE.AxesHelper(50);
|
|
315
334
|
scene.add(axesHelper);
|
|
316
335
|
|
|
@@ -326,14 +345,14 @@
|
|
|
326
345
|
axesBtn.addEventListener('click', toggleAxes);
|
|
327
346
|
gridBtn.addEventListener('click', toggleGrid);
|
|
328
347
|
|
|
329
|
-
// View buttons
|
|
330
|
-
viewTop.addEventListener('click', () => setView(0,
|
|
331
|
-
viewBottom.addEventListener('click', () => setView(0, -1
|
|
332
|
-
viewFront.addEventListener('click', () => setView(0,
|
|
333
|
-
viewBack.addEventListener('click', () => setView(0,
|
|
334
|
-
viewRight.addEventListener('click', () => setView(1, 0, 0));
|
|
335
|
-
viewLeft.addEventListener('click', () => setView(-1, 0, 0));
|
|
336
|
-
viewIso.addEventListener('click',
|
|
348
|
+
// View buttons (Z-up coordinate system: X=right, Y=front, Z=up)
|
|
349
|
+
viewTop.addEventListener('click', () => setView(0, 0, 1)); // Looking down from +Z
|
|
350
|
+
viewBottom.addEventListener('click', () => setView(0, 0, -1)); // Looking up from -Z
|
|
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
|
|
337
356
|
|
|
338
357
|
// Animation loop
|
|
339
358
|
animate();
|
|
@@ -355,15 +374,15 @@
|
|
|
355
374
|
loader.load(
|
|
356
375
|
'api/model',
|
|
357
376
|
(geometry) => {
|
|
358
|
-
// Center geometry
|
|
377
|
+
// Center geometry in XY, keep Z base at 0
|
|
359
378
|
geometry.computeBoundingBox();
|
|
360
379
|
const center = new THREE.Vector3();
|
|
361
380
|
geometry.boundingBox.getCenter(center);
|
|
362
381
|
geometry.translate(-center.x, -center.y, -center.z);
|
|
363
382
|
|
|
364
|
-
// Move to sit on grid
|
|
365
|
-
const
|
|
366
|
-
geometry.translate(0,
|
|
383
|
+
// Move to sit on grid (Z=0 plane)
|
|
384
|
+
const minZ = geometry.boundingBox.min.z - center.z;
|
|
385
|
+
geometry.translate(0, 0, -minZ);
|
|
367
386
|
|
|
368
387
|
// Recalculate bounding box after translation
|
|
369
388
|
geometry.computeBoundingBox();
|
|
@@ -399,17 +418,8 @@
|
|
|
399
418
|
'api/model',
|
|
400
419
|
(group) => {
|
|
401
420
|
// 3MFLoader returns a Group with meshes
|
|
402
|
-
// 3MF uses Z-up,
|
|
403
|
-
|
|
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;
|
|
421
|
+
// 3MF uses Z-up natively, which matches our coordinate system
|
|
422
|
+
// Keep model at original position (don't center) to preserve coordinate alignment
|
|
413
423
|
|
|
414
424
|
model = group;
|
|
415
425
|
scene.add(model);
|
|
@@ -445,13 +455,26 @@
|
|
|
445
455
|
const box = new THREE.Box3().setFromObject(object);
|
|
446
456
|
box.getCenter(modelCenter);
|
|
447
457
|
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
458
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
452
|
-
|
|
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
|
|
453
473
|
scene.remove(gridHelper);
|
|
454
|
-
gridHelper = new THREE.GridHelper(gridSize,
|
|
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;
|
|
455
478
|
gridHelper.visible = showGrid;
|
|
456
479
|
scene.add(gridHelper);
|
|
457
480
|
|
|
@@ -460,8 +483,9 @@
|
|
|
460
483
|
axesHelper.visible = showAxes;
|
|
461
484
|
scene.add(axesHelper);
|
|
462
485
|
|
|
463
|
-
//
|
|
464
|
-
|
|
486
|
+
// Set camera to look at model center, default front view
|
|
487
|
+
controls.target.copy(modelCenter);
|
|
488
|
+
setView(0, -1, 0); // Front view
|
|
465
489
|
|
|
466
490
|
// Hide loading
|
|
467
491
|
loading.classList.add('hidden');
|
|
@@ -479,13 +503,46 @@
|
|
|
479
503
|
|
|
480
504
|
const box = new THREE.Box3().setFromObject(model);
|
|
481
505
|
const size = box.getSize(new THREE.Vector3());
|
|
506
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
482
507
|
|
|
483
508
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
484
|
-
const fov =
|
|
509
|
+
const fov = perspCamera.fov * (Math.PI / 180);
|
|
485
510
|
cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
|
|
486
511
|
|
|
487
|
-
//
|
|
488
|
-
|
|
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';
|
|
489
546
|
}
|
|
490
547
|
|
|
491
548
|
function setView(x, y, z) {
|
|
@@ -500,11 +557,12 @@
|
|
|
500
557
|
// Set camera position
|
|
501
558
|
camera.position.copy(center).add(dir.multiplyScalar(cameraDistance));
|
|
502
559
|
|
|
503
|
-
// Set up vector (
|
|
504
|
-
|
|
505
|
-
|
|
560
|
+
// Set up vector (Z-up coordinate system)
|
|
561
|
+
// When looking straight up/down along Z, use Y as up reference
|
|
562
|
+
if (Math.abs(z) > 0.9) {
|
|
563
|
+
camera.up.set(0, z > 0 ? 1 : -1, 0);
|
|
506
564
|
} else {
|
|
507
|
-
camera.up.set(0,
|
|
565
|
+
camera.up.set(0, 0, 1);
|
|
508
566
|
}
|
|
509
567
|
|
|
510
568
|
// Look at center
|
|
@@ -630,8 +688,18 @@
|
|
|
630
688
|
}
|
|
631
689
|
|
|
632
690
|
function onResize() {
|
|
633
|
-
|
|
634
|
-
|
|
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
|
+
|
|
635
703
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
636
704
|
controls.handleResize(); // TrackballControls needs this to update screen dimensions
|
|
637
705
|
}
|
|
@@ -703,15 +771,15 @@
|
|
|
703
771
|
function reloadSTL() {
|
|
704
772
|
const loader = new STLLoader();
|
|
705
773
|
loader.load('api/model?t=' + Date.now(), (geometry) => {
|
|
706
|
-
// Center geometry
|
|
774
|
+
// Center geometry in XY
|
|
707
775
|
geometry.computeBoundingBox();
|
|
708
776
|
const center = new THREE.Vector3();
|
|
709
777
|
geometry.boundingBox.getCenter(center);
|
|
710
778
|
geometry.translate(-center.x, -center.y, -center.z);
|
|
711
779
|
|
|
712
|
-
// Move to sit on grid
|
|
713
|
-
const
|
|
714
|
-
geometry.translate(0,
|
|
780
|
+
// Move to sit on grid (Z=0 plane)
|
|
781
|
+
const minZ = geometry.boundingBox.min.z - center.z;
|
|
782
|
+
geometry.translate(0, 0, -minZ);
|
|
715
783
|
|
|
716
784
|
// Recalculate bounding box after translation
|
|
717
785
|
geometry.computeBoundingBox();
|
|
@@ -746,17 +814,16 @@
|
|
|
746
814
|
function reload3MF() {
|
|
747
815
|
const loader = new ThreeMFLoader();
|
|
748
816
|
loader.load('api/model?t=' + Date.now(), (group) => {
|
|
749
|
-
// Z-up
|
|
750
|
-
group.rotation.set(-Math.PI / 2, 0, 0);
|
|
817
|
+
// 3MF uses Z-up natively, which matches our coordinate system
|
|
751
818
|
|
|
752
|
-
// Center the group
|
|
819
|
+
// Center the group in XY
|
|
753
820
|
const box = new THREE.Box3().setFromObject(group);
|
|
754
821
|
const center = box.getCenter(new THREE.Vector3());
|
|
755
822
|
group.position.sub(center);
|
|
756
823
|
|
|
757
|
-
// Move to sit on grid
|
|
824
|
+
// Move to sit on grid (Z=0 plane)
|
|
758
825
|
const newBox = new THREE.Box3().setFromObject(group);
|
|
759
|
-
group.position.
|
|
826
|
+
group.position.z -= newBox.min.z;
|
|
760
827
|
|
|
761
828
|
model = group;
|
|
762
829
|
scene.add(model);
|
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
|