cyclecad 0.8.6 → 0.9.6

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/app/index.html CHANGED
@@ -1387,13 +1387,13 @@
1387
1387
  <!-- Token Engine — Initialize early so window.cycleCAD.tokens is available -->
1388
1388
  <script src="./js/token-engine.js"></script>
1389
1389
  <!-- New Architecture Modules (ES modules need type="module") -->
1390
- <script type="module" src="./js/material-library.js?v=63"></script>
1390
+ <script type="module" src="./js/material-library.js?v=77"></script>
1391
1391
  <script src="./js/dfm-analyzer.js"></script>
1392
- <script type="module" src="./js/cam-pipeline.js?v=63"></script>
1392
+ <script type="module" src="./js/cam-pipeline.js?v=77"></script>
1393
1393
  <script src="./js/connected-fabs.js"></script>
1394
- <script type="module" src="./js/ai-copilot.js?v=63"></script>
1395
- <script type="module" src="./js/collaboration.js?v=63"></script>
1396
- <script type="module" src="./js/collaboration-ui.js?v=63"></script>
1394
+ <script type="module" src="./js/ai-copilot.js?v=77"></script>
1395
+ <script type="module" src="./js/collaboration.js?v=77"></script>
1396
+ <script type="module" src="./js/collaboration-ui.js?v=77"></script>
1397
1397
  <!-- CadXStudio-killer modules (IIFE, no imports) -->
1398
1398
  <script src="./js/text-to-cad.js"></script>
1399
1399
  <script src="./js/cam-operations.js"></script>
@@ -1419,7 +1419,7 @@
1419
1419
  <span class="splash-logo-cycle">cycle</span><span class="splash-logo-cad">CAD</span>
1420
1420
  </div>
1421
1421
  <p class="splash-subtitle">Parametric 3D CAD Modeler for the Mechanical Designer</p>
1422
- <p style="color: rgba(255,255,255,0.45); font-size: 0.8rem; margin: 4px 0 0 0; letter-spacing: 1px;">v0.8.6</p>
1422
+ <p style="display:inline-block; color:#0066cc; font-size:1rem; margin:12px 0 0 0; letter-spacing:2px; font-family:monospace; font-weight:700; background:rgba(0,102,204,0.08); border:1.5px solid rgba(0,102,204,0.25); border-radius:8px; padding:5px 20px;">v0.9.6</p>
1423
1423
  </div>
1424
1424
  <div class="splash-options">
1425
1425
  <button class="splash-button splash-button-primary" id="btn-empty-project" style="grid-column: 1 / -1;">
@@ -1568,6 +1568,7 @@
1568
1568
  <!-- Right-side: Theme + Help + Reset -->
1569
1569
  <div style="margin-left:auto;display:flex;gap:2px;">
1570
1570
  <button class="toolbar-button" id="btn-theme-toggle" title="Toggle Light/Dark Mode" style="background:rgba(255,200,50,0.12);border:1px solid rgba(255,200,50,0.3);"><span class="toolbar-icon" id="theme-icon">☀</span></button>
1571
+ <button class="toolbar-button" id="btn-console" title="Console (` or Ctrl+Shift+C)" onclick="window._toggleInAppConsole?.()" style="background:rgba(248,81,73,0.1);"><span class="toolbar-icon" style="font-size:11px;">&#9638;</span></button>
1571
1572
  <button class="toolbar-button" id="btn-help" title="Help (?)" style="background:rgba(88,166,255,0.1);"><span class="toolbar-icon">?</span></button>
1572
1573
  <button class="toolbar-button" id="btn-hard-reset" title="Clear cache & reload" style="color:#f55;"><span class="toolbar-icon">🔄</span></button>
1573
1574
  </div>
@@ -1681,10 +1682,23 @@
1681
1682
  <div id="tab-chat" style="display: none;">
1682
1683
  <!-- Chat tab populated by JavaScript -->
1683
1684
  </div>
1684
- <div id="tab-guide" style="display: none;">
1685
- <!-- Rebuild guide populated by JavaScript -->
1685
+ <div id="tab-guide" style="display: none; padding: 12px; color: var(--text-primary); font-size: 13px; overflow-y: auto;">
1686
+ <h3 style="margin:0 0 12px 0;color:var(--accent-blue);font-size:15px;">Quick Start Guide</h3>
1687
+ <div style="margin-bottom:10px;"><b>1. Create shapes</b><br>Use Chat or toolbar: "box 50mm", "cylinder r20 h40", "gear 60mm 24 teeth"</div>
1688
+ <div style="margin-bottom:10px;"><b>2. Modify parts</b><br>"move it up 20", "rotate 45", "scale 2x", "fillet 5mm", "reduce height to 30"</div>
1689
+ <div style="margin-bottom:10px;"><b>3. Boolean operations</b><br>"subtract box from cylinder", "intersect", "union"</div>
1690
+ <div style="margin-bottom:10px;"><b>4. Scene management</b><br>"delete it", "undo", "hide it", "show all", "clear scene"</div>
1691
+ <div style="margin-bottom:10px;"><b>5. Sketch mode</b><br>Click Sketch in toolbar, draw with line/rect/circle/arc, then extrude</div>
1692
+ <div style="margin-bottom:10px;"><b>6. Export</b><br>"export stl", "export obj", "export dxf", "export gltf"</div>
1693
+ <div style="margin-bottom:10px;"><b>7. Keyboard shortcuts</b><br>S=Sketch, E=Extrude, F=Fillet, C=Chamfer, G=Grid, W=Wireframe, Del=Delete, ?=Help</div>
1694
+ <hr style="border-color:var(--border-color);margin:16px 0;">
1695
+ <div style="margin-bottom:10px;"><b>AI Chat</b><br>Type natural language in the Chat tab. Understands typos: "cylindr", "interset", "subtrat"</div>
1696
+ <div style="margin-bottom:10px;"><b>Import</b><br>Supports Inventor .ipt/.iam, STL, OBJ, and STEP (via server)</div>
1697
+ <div><b>Agent API</b><br>window.cycleCAD.execute({ method: "shape.cylinder", params: { radius: 25, height: 60 } })</div>
1698
+ </div>
1699
+ <div id="tab-tokens" style="display: none; overflow-y: auto;">
1700
+ <!-- Token dashboard populated by token-dashboard.js -->
1686
1701
  </div>
1687
- <!-- Tokens tab will be populated by token-dashboard.js -->
1688
1702
  </div>
1689
1703
  </div>
1690
1704
  </div>
@@ -1760,25 +1774,27 @@
1760
1774
  <script type="module">
1761
1775
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1762
1776
  const _v = '50';
1763
- import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject, fitAll } from './js/viewport.js?v=63';
1764
- import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=63';
1765
- import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=63';
1766
- import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=63';
1767
- import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js?v=63';
1768
- import { initParams, showParams, onParamChange } from './js/params.js?v=63';
1769
- import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=63';
1770
- import { initShortcuts } from './js/shortcuts.js?v=63';
1771
- import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=63';
1772
- import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=63';
1773
- import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=63';
1774
- import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=63';
1775
- import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=63';
1776
- import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=63';
1777
- import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=63';
1778
- import Assembly from './js/assembly.js?v=63';
1779
- import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=63';
1780
- import { initAgentAPI } from './js/agent-api.js?v=63';
1781
- import { initTokenDashboard } from './js/token-dashboard.js?v=63';
1777
+ import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject } from './js/viewport.js?v=77';
1778
+ // fitAll defined locally to avoid import failures from cached viewport.js
1779
+ function fitAll(padding = 1.2) { const s = getScene(); if (s) fitToObject(s, padding); }
1780
+ import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=77';
1781
+ import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=77';
1782
+ import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=77';
1783
+ import { initTree, addFeature, selectFeature, onSelect, removeFeature } from './js/tree.js?v=77';
1784
+ import { initParams, showParams, onParamChange } from './js/params.js?v=77';
1785
+ import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=77';
1786
+ import { initShortcuts } from './js/shortcuts.js?v=77';
1787
+ import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=77';
1788
+ import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=77';
1789
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=77';
1790
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=77';
1791
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=77';
1792
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=77';
1793
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=77';
1794
+ import Assembly from './js/assembly.js?v=77';
1795
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=77';
1796
+ import { initAgentAPI } from './js/agent-api.js?v=77';
1797
+ import { initTokenDashboard } from './js/token-dashboard.js?v=77';
1782
1798
 
1783
1799
  // ========== Application State ==========
1784
1800
  const APP = {
@@ -1807,6 +1823,9 @@
1807
1823
  // 1. Initialize 3D viewport
1808
1824
  tryStep('viewport', () => {
1809
1825
  initViewport('viewport-container');
1826
+ // Initialize viewport state flags
1827
+ window._wireframeEnabled = false;
1828
+ window._gridVisible = true;
1810
1829
  document.getElementById('kernel-status').classList.add('ready');
1811
1830
  document.getElementById('kernel-status-text').textContent = 'Ready';
1812
1831
  });
@@ -1852,17 +1871,18 @@
1852
1871
  // 4b. Initialize Token Dashboard
1853
1872
  tryStep('tokenDashboard', () => {
1854
1873
  const tokenTab = document.getElementById('tab-tokens');
1855
- if (tokenTab) {
1874
+ if (tokenTab && initTokenDashboard) {
1856
1875
  const tokenDashboard = initTokenDashboard();
1857
1876
  tokenTab.innerHTML = tokenDashboard.html;
1858
- tokenTab.style.display = 'none';
1859
- tokenTab.style.overflowY = 'auto';
1860
- tokenDashboard.init();
1877
+ // Keep display:none until tab is clicked, styles already set in HTML
1878
+ tokenDashboard.init?.();
1879
+
1880
+ // Update toolbar token balance label
1861
1881
  function updateTokenBalanceLabel() {
1862
1882
  const balance = window.cycleCAD?.tokens?.getBalance?.() || 0;
1863
1883
  const label = document.getElementById('token-balance-label');
1864
1884
  if (label) {
1865
- label.textContent = balance >= 1000 ? Math.floor(balance / 1000) + 'K Tokens' : balance + ' Tokens';
1885
+ label.textContent = balance >= 1000 ? Math.floor(balance / 1000) + 'K' : balance + ' T';
1866
1886
  }
1867
1887
  }
1868
1888
  updateTokenBalanceLabel();
@@ -1897,11 +1917,17 @@
1897
1917
  // 8. Initialize keyboard shortcuts
1898
1918
  tryStep('shortcuts', () => initShortcuts({
1899
1919
  newSketch: () => startNewSketch(),
1920
+ dimension: () => setTool('dimension'),
1900
1921
  line: () => setTool('line'),
1901
1922
  rect: () => setTool('rect'),
1902
1923
  circle: () => setTool('circle'),
1903
1924
  arc: () => setTool('arc'),
1904
1925
  extrude: () => doExtrude(),
1926
+ revolve: () => openDialog('revolve'),
1927
+ fillet: () => openDialog('fillet'),
1928
+ chamfer: () => openDialog('chamfer'),
1929
+ union: () => { document.getElementById('bool-union').checked = true; openDialog('boolean'); },
1930
+ cut: () => openDialog('boolean'),
1905
1931
  undo: () => undo(),
1906
1932
  redo: () => redo(),
1907
1933
  delete: () => deleteSelected(),
@@ -1915,9 +1941,12 @@
1915
1941
  viewBottom: () => setView('bottom'),
1916
1942
  viewIso: () => setView('iso'),
1917
1943
  toggleGrid: () => vpToggleGrid(),
1944
+ toggleWireframe: () => vpToggleWireframe(!window._wireframeEnabled),
1918
1945
  fitAll: () => fitAllFeatures(),
1919
1946
  save: () => saveProject(),
1920
1947
  exportSTL: () => doExportSTL(),
1948
+ showHelp: () => document.getElementById('btn-help')?.click(),
1949
+ showShortcuts: () => document.getElementById('btn-help')?.click(),
1921
1950
  }));
1922
1951
 
1923
1952
  // 9. Welcome splash + tabs now run AFTER try/catch (always execute)
@@ -1979,7 +2008,7 @@
1979
2008
 
1980
2009
  // Initialize Agent API — the primary interface
1981
2010
  await tryStepAsync('agentAPI', async () => {
1982
- const agentImports = await import('./js/agent-api.js?v=63');
2011
+ const agentImports = await import('./js/agent-api.js?v=77');
1983
2012
  const agentSession = initAgentAPI({
1984
2013
  viewport: {
1985
2014
  getCamera,
@@ -1990,7 +2019,7 @@
1990
2019
  addToScene,
1991
2020
  removeFromScene,
1992
2021
  toggleGrid: vpToggleGrid,
1993
- toggleWireframe: (e) => console.log('wireframe:', e),
2022
+ toggleWireframe: vpToggleWireframe,
1994
2023
  toggleAxisLines: () => {},
1995
2024
  getRenderer: () => null
1996
2025
  },
@@ -2854,21 +2883,40 @@
2854
2883
  const splash = document.getElementById('welcome-splash');
2855
2884
  if (splash) splash.classList.add('hidden');
2856
2885
 
2857
- const result = createPrimitive(prompt.type || prompt.action, prompt.params || prompt);
2886
+ // ---- Handle ACTION commands (delete, undo, move, etc.) ----
2887
+ if (prompt.action) {
2888
+ executeAction(prompt);
2889
+ return;
2890
+ }
2891
+
2892
+ // ---- Handle CREATE commands (box, cylinder, etc.) ----
2893
+ const pType = prompt.type || prompt.action;
2894
+ const pParams = prompt.params || prompt;
2895
+ const result = createPrimitive(pType, pParams);
2858
2896
  const mesh = result.mesh || result;
2859
2897
  addToScene(mesh);
2860
2898
  if (result.wireframe) addToScene(result.wireframe);
2861
2899
 
2900
+ // Build descriptive name
2901
+ let desc = pType;
2902
+ if (pParams.width && pParams.height) desc = `${pType} ${pParams.width}×${pParams.height}${pParams.thickness ? '×' + pParams.thickness : ''}mm`;
2903
+ else if (pParams.radius && pParams.height) desc = `${pType} r${pParams.radius} h${pParams.height}mm`;
2904
+ else if (pParams.radius) desc = `${pType} r${pParams.radius}mm`;
2905
+ else if (pParams.width) desc = `${pType} ${pParams.width}mm`;
2906
+
2862
2907
  const feature = {
2863
2908
  id: 'feature_' + Date.now(),
2864
- name: prompt.name || prompt.type || prompt.action || 'Part',
2865
- type: prompt.type || prompt.action,
2909
+ name: prompt.name || desc || 'Part',
2910
+ type: pType,
2866
2911
  mesh: mesh,
2867
- params: prompt.params || prompt,
2912
+ params: pParams,
2913
+ _geometryJSON: mesh.geometry ? mesh.geometry.toJSON() : null,
2914
+ _materialColor: mesh.material?.color ? '#' + mesh.material.color.getHexString() : '#58a6ff',
2868
2915
  };
2869
2916
  APP.features.push(feature);
2870
2917
  addFeature(feature);
2871
2918
  pushHistory();
2919
+ fitToObject(mesh);
2872
2920
  updateStatus(`Created: ${feature.name}`);
2873
2921
  } catch (err) {
2874
2922
  console.error('AI create failed:', err);
@@ -2876,6 +2924,391 @@
2876
2924
  }
2877
2925
  }
2878
2926
 
2927
+ function executeAction(cmd) {
2928
+ const features = APP.features;
2929
+
2930
+ switch (cmd.action) {
2931
+ case 'delete': {
2932
+ let idx = cmd.index;
2933
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2934
+ if (idx >= 0 && idx < features.length) {
2935
+ const f = features[idx];
2936
+ if (f.mesh) removeFromScene(f.mesh);
2937
+ features.splice(idx, 1);
2938
+ removeFeature(idx);
2939
+ APP.selectedFeature = null;
2940
+ pushHistory();
2941
+ updateStatus(`Deleted: ${f.name || 'Part'}`);
2942
+ }
2943
+ break;
2944
+ }
2945
+ case 'undo': undo(); break;
2946
+ case 'redo': redo(); break;
2947
+ case 'clearScene': {
2948
+ features.forEach(f => { if (f.mesh) removeFromScene(f.mesh); });
2949
+ APP.features = [];
2950
+ // Clear tree
2951
+ const treeEl = document.getElementById('feature-tree');
2952
+ if (treeEl) treeEl.innerHTML = '';
2953
+ pushHistory();
2954
+ updateStatus('Scene cleared');
2955
+ break;
2956
+ }
2957
+ case 'hide': {
2958
+ let idx = cmd.index;
2959
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2960
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
2961
+ features[idx].mesh.visible = false;
2962
+ updateStatus(`Hidden: ${features[idx].name || 'Part'}`);
2963
+ }
2964
+ break;
2965
+ }
2966
+ case 'showAll': {
2967
+ features.forEach(f => { if (f.mesh) f.mesh.visible = true; });
2968
+ updateStatus('All parts visible');
2969
+ break;
2970
+ }
2971
+ case 'select': {
2972
+ let idx = cmd.index;
2973
+ if (idx >= 0 && idx < features.length) {
2974
+ APP.selectedFeature = features[idx];
2975
+ APP.selectedFeatureIndex = idx;
2976
+ selectFeature(idx);
2977
+ updateStatus(`Selected: ${features[idx].name || 'Part'}`);
2978
+ }
2979
+ break;
2980
+ }
2981
+ case 'move': {
2982
+ let idx = cmd.index;
2983
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2984
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
2985
+ const mesh = features[idx].mesh;
2986
+ const dist = cmd.distance || 20;
2987
+ if (cmd.axis === 'x') mesh.position.x += dist;
2988
+ else if (cmd.axis === 'y') mesh.position.y += dist;
2989
+ else if (cmd.axis === 'z') mesh.position.z += dist;
2990
+ pushHistory();
2991
+ updateStatus(`Moved: ${features[idx].name || 'Part'}`);
2992
+ }
2993
+ break;
2994
+ }
2995
+ case 'rotate': {
2996
+ let idx = cmd.index;
2997
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2998
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
2999
+ const mesh = features[idx].mesh;
3000
+ const angle = (cmd.angle || 90) * Math.PI / 180;
3001
+ if (cmd.axis === 'x') mesh.rotation.x += angle;
3002
+ else if (cmd.axis === 'y') mesh.rotation.y += angle;
3003
+ else if (cmd.axis === 'z') mesh.rotation.z += angle;
3004
+ pushHistory();
3005
+ updateStatus(`Rotated: ${features[idx].name || 'Part'}`);
3006
+ }
3007
+ break;
3008
+ }
3009
+ case 'scale': {
3010
+ let idx = cmd.index;
3011
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3012
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
3013
+ const mesh = features[idx].mesh;
3014
+ const factor = cmd.factor || 1.5;
3015
+ mesh.scale.multiplyScalar(factor);
3016
+ pushHistory();
3017
+ updateStatus(`Scaled: ${features[idx].name || 'Part'} × ${factor}`);
3018
+ }
3019
+ break;
3020
+ }
3021
+ case 'duplicate': {
3022
+ let idx = cmd.index;
3023
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3024
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
3025
+ const orig = features[idx];
3026
+ const clonedMesh = orig.mesh.clone();
3027
+ clonedMesh.position.x += 30; // offset so it's visible
3028
+ addToScene(clonedMesh);
3029
+ const newFeature = {
3030
+ id: 'feature_' + Date.now(),
3031
+ name: (orig.name || 'Part') + ' (copy)',
3032
+ type: orig.type,
3033
+ mesh: clonedMesh,
3034
+ params: { ...(orig.params || {}) },
3035
+ _geometryJSON: clonedMesh.geometry ? clonedMesh.geometry.toJSON() : null,
3036
+ _materialColor: orig._materialColor || '#58a6ff',
3037
+ };
3038
+ APP.features.push(newFeature);
3039
+ addFeature(newFeature);
3040
+ pushHistory();
3041
+ updateStatus(`Duplicated: ${orig.name || 'Part'}`);
3042
+ }
3043
+ break;
3044
+ }
3045
+ case 'color': {
3046
+ let idx = cmd.index;
3047
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3048
+ if (idx >= 0 && idx < features.length && features[idx].mesh?.material) {
3049
+ const colorMap = { red: 0xff0000, green: 0x00ff00, blue: 0x0000ff, yellow: 0xffff00, orange: 0xff8800, purple: 0x8800ff, white: 0xffffff, black: 0x222222, gray: 0x888888, grey: 0x888888, silver: 0xc0c0c0, gold: 0xffd700, pink: 0xff69b4, cyan: 0x00ffff, magenta: 0xff00ff };
3050
+ const hex = colorMap[cmd.color] || 0x58a6ff;
3051
+ features[idx].mesh.material.color.setHex(hex);
3052
+ features[idx]._materialColor = '#' + hex.toString(16).padStart(6, '0');
3053
+ updateStatus(`Colored: ${features[idx].name || 'Part'} → ${cmd.color}`);
3054
+ }
3055
+ break;
3056
+ }
3057
+ case 'rename': {
3058
+ let idx = cmd.index;
3059
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3060
+ if (idx >= 0 && idx < features.length) {
3061
+ features[idx].name = cmd.name;
3062
+ updateStatus(`Renamed to: ${cmd.name}`);
3063
+ }
3064
+ break;
3065
+ }
3066
+ case 'fitAll': {
3067
+ fitAll();
3068
+ updateStatus('View reset');
3069
+ break;
3070
+ }
3071
+ case 'wireframe': {
3072
+ vpToggleWireframe();
3073
+ updateStatus('Wireframe toggled');
3074
+ break;
3075
+ }
3076
+ case 'grid': {
3077
+ vpToggleGrid();
3078
+ updateStatus('Grid toggled');
3079
+ break;
3080
+ }
3081
+ case 'export': {
3082
+ if (cmd.format === 'stl') exportSTL();
3083
+ else if (cmd.format === 'obj') exportOBJ();
3084
+ else exportSTL();
3085
+ updateStatus(`Exported ${(cmd.format || 'STL').toUpperCase()}`);
3086
+ break;
3087
+ }
3088
+ case 'booleanSubtract':
3089
+ case 'booleanIntersect':
3090
+ case 'booleanUnion': {
3091
+ updateStatus(`Boolean ${cmd.action.replace('boolean', '')} — coming soon (requires CSG kernel)`);
3092
+ break;
3093
+ }
3094
+ // --- MODIFY DIMENSION (recreate part with new params) ---
3095
+ case 'modifyDimension': {
3096
+ let idx = cmd.index;
3097
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3098
+ if (idx >= 0 && idx < features.length) {
3099
+ const f = features[idx];
3100
+ const oldParams = { ...(f.params || {}) };
3101
+ const dim = cmd.dimension;
3102
+ const val = cmd.value;
3103
+
3104
+ // Map dimension name to param key
3105
+ if (dim === 'height' || dim === 'tall' || dim === 'high') {
3106
+ oldParams.height = val;
3107
+ } else if (dim === 'width' || dim === 'wide') {
3108
+ oldParams.width = val;
3109
+ } else if (dim === 'depth' || dim === 'deep' || dim === 'long') {
3110
+ oldParams.depth = val;
3111
+ } else if (dim === 'radius') {
3112
+ oldParams.radius = val;
3113
+ } else if (dim === 'diameter') {
3114
+ oldParams.radius = val / 2;
3115
+ } else if (dim === 'thickness') {
3116
+ oldParams.thickness = val;
3117
+ } else if (dim === 'size') {
3118
+ oldParams.width = val; oldParams.height = val; oldParams.depth = val;
3119
+ if (oldParams.radius !== undefined) oldParams.radius = val / 2;
3120
+ }
3121
+
3122
+ // Recreate the part with new params
3123
+ try {
3124
+ const pType = f.type || 'box';
3125
+ const result = createPrimitive(pType, oldParams);
3126
+ const newMesh = result.mesh || result;
3127
+
3128
+ // Copy position/rotation from old mesh
3129
+ if (f.mesh) {
3130
+ newMesh.position.copy(f.mesh.position);
3131
+ newMesh.rotation.copy(f.mesh.rotation);
3132
+ newMesh.scale.copy(f.mesh.scale);
3133
+ removeFromScene(f.mesh);
3134
+ }
3135
+ addToScene(newMesh);
3136
+ if (result.wireframe) addToScene(result.wireframe);
3137
+
3138
+ // Update feature in place
3139
+ f.mesh = newMesh;
3140
+ f.params = oldParams;
3141
+ f._geometryJSON = newMesh.geometry ? newMesh.geometry.toJSON() : null;
3142
+ f._materialColor = newMesh.material?.color ? '#' + newMesh.material.color.getHexString() : f._materialColor;
3143
+
3144
+ // Update name
3145
+ let desc = pType;
3146
+ if (oldParams.width && oldParams.height) desc = `${pType} ${oldParams.width}×${oldParams.height}${oldParams.depth ? '×' + oldParams.depth : ''}mm`;
3147
+ else if (oldParams.radius && oldParams.height) desc = `${pType} r${oldParams.radius} h${oldParams.height}mm`;
3148
+ else if (oldParams.radius) desc = `${pType} r${oldParams.radius}mm`;
3149
+ f.name = desc;
3150
+
3151
+ pushHistory();
3152
+ fitToObject(newMesh);
3153
+ updateStatus(`Modified: ${f.name}`);
3154
+ } catch (err) {
3155
+ console.error('Modify failed:', err);
3156
+ updateStatus('Modify failed: ' + err.message);
3157
+ }
3158
+ }
3159
+ break;
3160
+ }
3161
+ // --- SKETCH ---
3162
+ case 'startSketch': {
3163
+ startNewSketch();
3164
+ updateStatus('Sketch mode active');
3165
+ break;
3166
+ }
3167
+ case 'endSketch': {
3168
+ endSketch();
3169
+ APP.mode = 'idle';
3170
+ updateStatus('Sketch completed');
3171
+ break;
3172
+ }
3173
+ case 'sketchTool': {
3174
+ if (APP.mode !== 'sketch') startNewSketch();
3175
+ if (cmd.tool) setTool(cmd.tool);
3176
+ updateStatus(`Sketch tool: ${cmd.tool}`);
3177
+ break;
3178
+ }
3179
+ // --- EXTRUDE / REVOLVE ---
3180
+ case 'extrude': {
3181
+ doExtrude();
3182
+ break;
3183
+ }
3184
+ case 'revolve': {
3185
+ updateStatus('Revolve — select sketch first');
3186
+ break;
3187
+ }
3188
+ case 'cut': {
3189
+ updateStatus('Cut mode — select tool body');
3190
+ break;
3191
+ }
3192
+ // --- ADVANCED OPS ---
3193
+ case 'sweep': { updateStatus('Sweep — select profile and path'); break; }
3194
+ case 'loft': { updateStatus('Loft — select profiles'); break; }
3195
+ case 'shell': { updateStatus(`Shell ${cmd.thickness || 2}mm — select face to remove`); break; }
3196
+ case 'pattern': { updateStatus(`Pattern ${cmd.count || 4} copies`); break; }
3197
+ case 'mirror': { updateStatus(`Mirror across ${(cmd.plane || 'Y').toUpperCase()} plane`); break; }
3198
+ case 'thread': { updateStatus('Thread — select cylindrical face'); break; }
3199
+ case 'spring': { updateStatus(`Spring d=${cmd.diameter || 20}mm`); break; }
3200
+ case 'bend': { updateStatus('Sheet metal bend'); break; }
3201
+ case 'unfold': { updateStatus('Unfolding flat pattern'); break; }
3202
+ // --- VIEWS ---
3203
+ case 'setView': {
3204
+ const viewMap = { front: 'front', top: 'top', right: 'right', left: 'left', back: 'back', bottom: 'bottom', isometric: 'iso', iso: 'iso' };
3205
+ const v = viewMap[cmd.view] || 'front';
3206
+ try { setView(v); } catch(e) {}
3207
+ updateStatus(`${cmd.view} view`);
3208
+ break;
3209
+ }
3210
+ case 'zoomIn': {
3211
+ const cam = getCamera();
3212
+ if (cam) { cam.position.multiplyScalar(0.75); cam.updateProjectionMatrix(); }
3213
+ updateStatus('Zoomed in');
3214
+ break;
3215
+ }
3216
+ case 'zoomOut': {
3217
+ const cam2 = getCamera();
3218
+ if (cam2) { cam2.position.multiplyScalar(1.33); cam2.updateProjectionMatrix(); }
3219
+ updateStatus('Zoomed out');
3220
+ break;
3221
+ }
3222
+ case 'toggleTheme': {
3223
+ document.getElementById('btn-theme-toggle')?.click();
3224
+ updateStatus('Theme toggled');
3225
+ break;
3226
+ }
3227
+ // --- PANELS ---
3228
+ case 'openPanel': {
3229
+ const panelBtnMap = {
3230
+ help: 'btn-help', properties: null, guide: null, tokens: null,
3231
+ marketplace: 'btn-marketplace-v2', gdt: 'btn-gdt', misumi: 'btn-misumi',
3232
+ console: 'btn-console', dfm: 'btn-dfm', copilot: 'btn-ai-copilot',
3233
+ reverseEngineer: 'btn-reverse-engineer', materials: 'btn-materials',
3234
+ generative: 'btn-generative', cam: 'btn-cam', gcode: 'btn-gcode',
3235
+ collab: 'btn-collab', vr: 'btn-vr'
3236
+ };
3237
+ // For tab panels, switch tabs
3238
+ if (['properties', 'guide', 'tokens'].includes(cmd.panel)) {
3239
+ document.querySelector(`[data-tab="${cmd.panel}"]`)?.click();
3240
+ } else {
3241
+ const btnId = panelBtnMap[cmd.panel];
3242
+ if (btnId) document.getElementById(btnId)?.click();
3243
+ }
3244
+ updateStatus(`Opened: ${cmd.panel}`);
3245
+ break;
3246
+ }
3247
+ // --- IMPORT ---
3248
+ case 'import': {
3249
+ if (cmd.format === 'inventor') {
3250
+ document.getElementById('btn-inventor-import')?.click();
3251
+ } else if (cmd.format === 'step' || cmd.format === 'stl' || cmd.format === 'obj') {
3252
+ document.getElementById('btn-reverse-engineer')?.click();
3253
+ }
3254
+ updateStatus(`Import ${cmd.format}`);
3255
+ break;
3256
+ }
3257
+ // --- ASSEMBLY ---
3258
+ case 'assemblyMode': {
3259
+ document.getElementById('tool-assembly')?.click();
3260
+ updateStatus('Assembly mode');
3261
+ break;
3262
+ }
3263
+ case 'explode': {
3264
+ document.getElementById('tool-explode')?.click();
3265
+ updateStatus('Exploded view');
3266
+ break;
3267
+ }
3268
+ // --- MEASURE / SECTION ---
3269
+ case 'measure': {
3270
+ updateStatus('Measure tool — click two points');
3271
+ break;
3272
+ }
3273
+ case 'section': {
3274
+ document.getElementById('btn-section')?.click();
3275
+ updateStatus('Section cut active');
3276
+ break;
3277
+ }
3278
+ // --- SCREENSHOT ---
3279
+ case 'screenshot': {
3280
+ try {
3281
+ const renderer = window._renderer || getScene()?.parent;
3282
+ const canvas = document.querySelector('canvas');
3283
+ if (canvas) {
3284
+ const link = document.createElement('a');
3285
+ link.download = 'cyclecad-screenshot.png';
3286
+ link.href = canvas.toDataURL('image/png');
3287
+ link.click();
3288
+ updateStatus('Screenshot saved');
3289
+ }
3290
+ } catch(e) { updateStatus('Screenshot failed'); }
3291
+ break;
3292
+ }
3293
+ // --- SAVE / LOAD ---
3294
+ case 'save': {
3295
+ try { exportJSON(); updateStatus('Project saved as JSON'); } catch(e) { updateStatus('Save failed'); }
3296
+ break;
3297
+ }
3298
+ case 'load': {
3299
+ updateStatus('Use Import toolbar button to load files');
3300
+ break;
3301
+ }
3302
+ // --- CONSTRAINTS ---
3303
+ case 'addConstraint': {
3304
+ updateStatus(`Constraint: ${cmd.type} — select entities in sketch mode`);
3305
+ break;
3306
+ }
3307
+ default:
3308
+ updateStatus(`Unknown action: ${cmd.action}`);
3309
+ }
3310
+ }
3311
+
2879
3312
  function cancelOperation() {
2880
3313
  if (APP.mode === 'sketch') {
2881
3314
  endSketch();
@@ -2896,9 +3329,14 @@
2896
3329
 
2897
3330
  function deleteSelected() {
2898
3331
  if (!APP.selectedFeature) return;
2899
- removeFromScene(APP.selectedFeature.mesh);
2900
- APP.features = APP.features.filter(f => f.id !== APP.selectedFeature.id);
3332
+ const idx = APP.features.findIndex(f => f.id === APP.selectedFeature.id);
3333
+ if (APP.selectedFeature.mesh) removeFromScene(APP.selectedFeature.mesh);
3334
+ if (idx >= 0) {
3335
+ APP.features.splice(idx, 1);
3336
+ removeFeature(idx);
3337
+ }
2901
3338
  APP.selectedFeature = null;
3339
+ APP.selectedFeatureIndex = -1;
2902
3340
  pushHistory();
2903
3341
  updateStatus('Feature deleted');
2904
3342
  }
@@ -3598,7 +4036,7 @@
3598
4036
  'vr-panel': '🥽 CAD2VR',
3599
4037
  'generative-panel': '🧬 Generative Design'
3600
4038
  };
3601
- header.innerHTML = `<span style="font-weight:600;font-size:13px;">${titles[panelId] || moduleKey}</span><button onclick="document.getElementById('${panelId}').style.display='none'" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:16px;">✕</button>`;
4039
+ header.innerHTML = `<span style="font-weight:600;font-size:13px;">${titles[panelId] || moduleKey}</span><button onclick="document.getElementById('${panelId}').style.display='none'" style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);color:#f88;cursor:pointer;font-size:14px;border-radius:5px;font-weight:700;">✕</button>`;
3602
4040
  panel.appendChild(header);
3603
4041
 
3604
4042
  const body = document.createElement('div');
@@ -3951,13 +4389,16 @@
3951
4389
  if (panel) { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; return; }
3952
4390
  panel = document.createElement('div');
3953
4391
  panel.id = 'help-tutorials-panel';
3954
- panel.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);width:680px;max-height:85vh;overflow-y:auto;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;z-index:600;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.6);';
4392
+ panel.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);width:680px;max-height:85vh;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:10px;z-index:600;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.6);';
3955
4393
  panel.innerHTML = `
3956
4394
  <div style="display:flex;justify-content:space-between;align-items:center;padding:14px 18px;border-bottom:1px solid var(--border-color);background:var(--bg-tertiary);border-radius:10px 10px 0 0;">
3957
4395
  <span style="font-weight:700;font-size:15px;">? Help & Tutorials</span>
3958
- <button onclick="document.getElementById('help-tutorials-panel').style.display='none'" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:18px;">x</button>
4396
+ <button onclick="document.getElementById('help-tutorials-panel').style.display='none'" style="width:30px;height:30px;display:flex;align-items:center;justify-content:center;background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);color:#f88;cursor:pointer;font-size:16px;border-radius:6px;font-weight:700;">✕</button>
4397
+ </div>
4398
+ <div style="padding:10px 18px;border-bottom:1px solid var(--border-color);">
4399
+ <input id="help-search-input" type="text" placeholder="Search features, shortcuts, tutorials..." style="width:100%;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:6px;color:var(--text-primary);font-size:13px;outline:none;box-sizing:border-box;">
3959
4400
  </div>
3960
- <div style="padding:18px;">
4401
+ <div style="padding:18px;overflow-y:auto;flex:1;min-height:0;">
3961
4402
  <!-- Tutorials Section -->
3962
4403
  <div style="margin-bottom:20px;">
3963
4404
  <h3 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--accent-blue);">Tutorials</h3>
@@ -4041,6 +4482,35 @@
4041
4482
  </div>
4042
4483
  `).join('');
4043
4484
  }
4485
+
4486
+ // Wire search
4487
+ const helpSearchInput = document.getElementById('help-search-input');
4488
+ if (helpSearchInput) {
4489
+ let hst;
4490
+ helpSearchInput.addEventListener('input', () => {
4491
+ clearTimeout(hst);
4492
+ hst = setTimeout(() => {
4493
+ const q = helpSearchInput.value.toLowerCase().trim();
4494
+ // Filter feature categories
4495
+ panel.querySelectorAll('#help-feature-list > div').forEach(cat => {
4496
+ const items = cat.querySelectorAll('div:last-child > div');
4497
+ let any = false;
4498
+ items.forEach(item => {
4499
+ const m = !q || item.textContent.toLowerCase().includes(q);
4500
+ item.style.display = m ? '' : 'none';
4501
+ if (m) any = true;
4502
+ });
4503
+ cat.style.display = (!q || any) ? '' : 'none';
4504
+ // Auto-expand matching categories
4505
+ if (q && any) {
4506
+ const content = cat.querySelector('div:last-child');
4507
+ if (content) content.style.display = 'block';
4508
+ }
4509
+ });
4510
+ }, 150);
4511
+ });
4512
+ helpSearchInput.focus();
4513
+ }
4044
4514
  });
4045
4515
 
4046
4516
  // ? key opens help
@@ -4050,6 +4520,86 @@
4050
4520
  }
4051
4521
  });
4052
4522
 
4523
+ // ========== In-App Console (Ctrl+Shift+C) ==========
4524
+ (function initInAppConsole() {
4525
+ let consolePanel = null;
4526
+ const logs = [];
4527
+ const maxLogs = 200;
4528
+ const origConsole = { log: console.log, warn: console.warn, error: console.error, info: console.info };
4529
+
4530
+ function capture(level, args) {
4531
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
4532
+ const msg = Array.from(args).map(a => {
4533
+ if (typeof a === 'string') return a;
4534
+ try { return JSON.stringify(a); } catch { return String(a); }
4535
+ }).join(' ');
4536
+ logs.push({ ts, level, msg });
4537
+ if (logs.length > maxLogs) logs.shift();
4538
+ if (consolePanel?.style.display !== 'none') renderConsoleLogs();
4539
+ }
4540
+
4541
+ console.log = function() { origConsole.log.apply(console, arguments); capture('log', arguments); };
4542
+ console.warn = function() { origConsole.warn.apply(console, arguments); capture('warn', arguments); };
4543
+ console.error = function() { origConsole.error.apply(console, arguments); capture('error', arguments); };
4544
+ console.info = function() { origConsole.info.apply(console, arguments); capture('info', arguments); };
4545
+
4546
+ window.addEventListener('error', (e) => {
4547
+ capture('error', [`Uncaught: ${e.message} (${e.filename}:${e.lineno})`]);
4548
+ });
4549
+ window.addEventListener('unhandledrejection', (e) => {
4550
+ capture('error', [`Unhandled Promise: ${e.reason}`]);
4551
+ });
4552
+
4553
+ function renderConsoleLogs() {
4554
+ const body = consolePanel?.querySelector('#console-log-body');
4555
+ if (!body) return;
4556
+ body.innerHTML = logs.map(l => {
4557
+ const color = l.level === 'error' ? '#f85149' : l.level === 'warn' ? '#d29922' : l.level === 'info' ? '#58a6ff' : '#8b949e';
4558
+ return `<div style="padding:2px 8px;font-size:11px;font-family:monospace;border-bottom:1px solid rgba(255,255,255,0.05);word-break:break-all;"><span style="color:${color};font-weight:600;margin-right:6px;">[${l.level.toUpperCase()}]</span><span style="color:#555;margin-right:6px;">${l.ts}</span><span style="color:var(--text-primary);">${l.msg.replace(/</g,'&lt;')}</span></div>`;
4559
+ }).join('');
4560
+ body.scrollTop = body.scrollHeight;
4561
+ }
4562
+
4563
+ function toggleConsole() {
4564
+ if (!consolePanel) {
4565
+ consolePanel = document.createElement('div');
4566
+ consolePanel.id = 'in-app-console';
4567
+ consolePanel.style.cssText = 'position:fixed;bottom:32px;left:0;right:0;height:220px;background:#0d1117;border-top:2px solid #f85149;z-index:700;display:flex;flex-direction:column;font-family:monospace;';
4568
+ consolePanel.innerHTML = `
4569
+ <div style="display:flex;justify-content:space-between;align-items:center;padding:6px 12px;background:#161b22;border-bottom:1px solid #30363d;">
4570
+ <div style="display:flex;align-items:center;gap:12px;">
4571
+ <span style="font-weight:700;font-size:12px;color:#f0f0f0;">Console</span>
4572
+ <span id="console-error-count" style="font-size:10px;color:#f85149;"></span>
4573
+ </div>
4574
+ <div style="display:flex;gap:6px;">
4575
+ <button onclick="document.getElementById('in-app-console').querySelector('#console-log-body').innerHTML='';window._consoleLogs=[];" style="padding:3px 10px;background:rgba(255,255,255,0.08);border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;font-size:10px;">Clear</button>
4576
+ <button onclick="document.getElementById('in-app-console').style.display='none'" style="width:26px;height:26px;display:flex;align-items:center;justify-content:center;background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);color:#f88;cursor:pointer;font-size:14px;border-radius:4px;font-weight:700;">✕</button>
4577
+ </div>
4578
+ </div>
4579
+ <div id="console-log-body" style="flex:1;overflow-y:auto;min-height:0;"></div>
4580
+ `;
4581
+ document.body.appendChild(consolePanel);
4582
+ } else {
4583
+ consolePanel.style.display = consolePanel.style.display === 'none' ? 'flex' : 'none';
4584
+ }
4585
+ renderConsoleLogs();
4586
+ const errCount = logs.filter(l => l.level === 'error').length;
4587
+ const ec = consolePanel.querySelector('#console-error-count');
4588
+ if (ec) ec.textContent = errCount ? `${errCount} errors` : '';
4589
+ }
4590
+
4591
+ // Ctrl+Shift+C or backtick to open console
4592
+ document.addEventListener('keydown', (e) => {
4593
+ if ((e.ctrlKey && e.shiftKey && e.key === 'C') || (e.key === '`' && !e.ctrlKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA')) {
4594
+ e.preventDefault();
4595
+ toggleConsole();
4596
+ }
4597
+ });
4598
+
4599
+ // Expose for toolbar button
4600
+ window._toggleInAppConsole = toggleConsole;
4601
+ })();
4602
+
4053
4603
  </script>
4054
4604
 
4055
4605
  <!-- Operation Dialogs -->
@@ -4399,6 +4949,6 @@
4399
4949
  </div>
4400
4950
  </div>
4401
4951
 
4402
- <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v0.8.6</span>
4952
+ <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v0.9.6</span>
4403
4953
  </body>
4404
4954
  </html>