@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cluesmith/codev",
3
- "version": "1.5.12",
3
+ "version": "1.5.14",
4
4
  "description": "Codev CLI - AI-assisted software development framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  }
13
13
 
14
14
  body {
15
- background: #1a1a2e;
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
- button.view-btn.positive {
95
- color: #4ade80;
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.negative {
99
- color: #f87171;
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="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>
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="Isometric view">Iso</button>
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 &#8594; Right</div>
211
- <div class="axis-y">Y &#8594; Up</div>
212
- <div class="axis-z">Z &#8594; Front</div>
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(0x1a1a2e);
274
-
275
- // Camera
276
- camera = new THREE.PerspectiveCamera(
277
- 45,
278
- container.clientWidth / container.clientHeight,
279
- 0.1,
280
- 10000
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
- camera.position.set(100, 100, 100);
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
- gridHelper = new THREE.GridHelper(200, 20, 0x0f3460, 0x0f3460);
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 (X=red, Y=green, Z=blue)
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, 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));
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 minY = geometry.boundingBox.min.y - center.y;
366
- geometry.translate(0, -minY, 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, 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;
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
- const gridSize = Math.ceil(maxDim * 2 / 10) * 10; // Round up to nearest 10
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, gridSize / 5, 0x0f3460, 0x0f3460);
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
- // Fit camera to model
464
- fitToView();
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 = camera.fov * (Math.PI / 180);
509
+ const fov = perspCamera.fov * (Math.PI / 180);
485
510
  cameraDistance = maxDim / (2 * Math.tan(fov / 2)) * 1.5;
486
511
 
487
- // Isometric view
488
- setView(1, 1, 1);
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 (handle top/bottom views)
504
- if (Math.abs(y) > 0.9) {
505
- camera.up.set(0, 0, y > 0 ? -1 : 1);
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, 1, 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
- camera.aspect = container.clientWidth / container.clientHeight;
634
- camera.updateProjectionMatrix();
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 minY = geometry.boundingBox.min.y - center.y;
714
- geometry.translate(0, -minY, 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 to Y-up rotation
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.y -= newBox.min.y;
826
+ group.position.z -= newBox.min.z;
760
827
 
761
828
  model = group;
762
829
  scene.add(model);
@@ -182,11 +182,34 @@
182
182
  color: #888;
183
183
  }
184
184
 
185
- /* Editor textarea */
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
- <textarea id="editor" spellcheck="false"></textarea>
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
- editor.style.display = 'block';
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
- // Apply scroll position to editor
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
- editor.style.display = 'none';
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