cyclecad 0.8.7 → 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=64"></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=64"></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=64"></script>
1395
- <script type="module" src="./js/collaboration.js?v=64"></script>
1396
- <script type="module" src="./js/collaboration-ui.js?v=64"></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.7</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,8 +1682,19 @@
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>
1686
1698
  </div>
1687
1699
  <div id="tab-tokens" style="display: none; overflow-y: auto;">
1688
1700
  <!-- Token dashboard populated by token-dashboard.js -->
@@ -1762,25 +1774,27 @@
1762
1774
  <script type="module">
1763
1775
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1764
1776
  const _v = '50';
1765
- import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject, fitAll } from './js/viewport.js?v=64';
1766
- import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=64';
1767
- import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=64';
1768
- import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=64';
1769
- import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js?v=64';
1770
- import { initParams, showParams, onParamChange } from './js/params.js?v=64';
1771
- import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=64';
1772
- import { initShortcuts } from './js/shortcuts.js?v=64';
1773
- import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=64';
1774
- import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=64';
1775
- import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=64';
1776
- import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=64';
1777
- import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=64';
1778
- import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=64';
1779
- import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=64';
1780
- import Assembly from './js/assembly.js?v=64';
1781
- import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=64';
1782
- import { initAgentAPI } from './js/agent-api.js?v=64';
1783
- import { initTokenDashboard } from './js/token-dashboard.js?v=64';
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';
1784
1798
 
1785
1799
  // ========== Application State ==========
1786
1800
  const APP = {
@@ -1994,7 +2008,7 @@
1994
2008
 
1995
2009
  // Initialize Agent API — the primary interface
1996
2010
  await tryStepAsync('agentAPI', async () => {
1997
- const agentImports = await import('./js/agent-api.js?v=64');
2011
+ const agentImports = await import('./js/agent-api.js?v=77');
1998
2012
  const agentSession = initAgentAPI({
1999
2013
  viewport: {
2000
2014
  getCamera,
@@ -2869,21 +2883,40 @@
2869
2883
  const splash = document.getElementById('welcome-splash');
2870
2884
  if (splash) splash.classList.add('hidden');
2871
2885
 
2872
- 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);
2873
2896
  const mesh = result.mesh || result;
2874
2897
  addToScene(mesh);
2875
2898
  if (result.wireframe) addToScene(result.wireframe);
2876
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
+
2877
2907
  const feature = {
2878
2908
  id: 'feature_' + Date.now(),
2879
- name: prompt.name || prompt.type || prompt.action || 'Part',
2880
- type: prompt.type || prompt.action,
2909
+ name: prompt.name || desc || 'Part',
2910
+ type: pType,
2881
2911
  mesh: mesh,
2882
- 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',
2883
2915
  };
2884
2916
  APP.features.push(feature);
2885
2917
  addFeature(feature);
2886
2918
  pushHistory();
2919
+ fitToObject(mesh);
2887
2920
  updateStatus(`Created: ${feature.name}`);
2888
2921
  } catch (err) {
2889
2922
  console.error('AI create failed:', err);
@@ -2891,6 +2924,391 @@
2891
2924
  }
2892
2925
  }
2893
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
+
2894
3312
  function cancelOperation() {
2895
3313
  if (APP.mode === 'sketch') {
2896
3314
  endSketch();
@@ -2911,9 +3329,14 @@
2911
3329
 
2912
3330
  function deleteSelected() {
2913
3331
  if (!APP.selectedFeature) return;
2914
- removeFromScene(APP.selectedFeature.mesh);
2915
- 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
+ }
2916
3338
  APP.selectedFeature = null;
3339
+ APP.selectedFeatureIndex = -1;
2917
3340
  pushHistory();
2918
3341
  updateStatus('Feature deleted');
2919
3342
  }
@@ -3613,7 +4036,7 @@
3613
4036
  'vr-panel': '🥽 CAD2VR',
3614
4037
  'generative-panel': '🧬 Generative Design'
3615
4038
  };
3616
- 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>`;
3617
4040
  panel.appendChild(header);
3618
4041
 
3619
4042
  const body = document.createElement('div');
@@ -3966,13 +4389,16 @@
3966
4389
  if (panel) { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; return; }
3967
4390
  panel = document.createElement('div');
3968
4391
  panel.id = 'help-tutorials-panel';
3969
- 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);';
3970
4393
  panel.innerHTML = `
3971
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;">
3972
4395
  <span style="font-weight:700;font-size:15px;">? Help & Tutorials</span>
3973
- <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;">
3974
4400
  </div>
3975
- <div style="padding:18px;">
4401
+ <div style="padding:18px;overflow-y:auto;flex:1;min-height:0;">
3976
4402
  <!-- Tutorials Section -->
3977
4403
  <div style="margin-bottom:20px;">
3978
4404
  <h3 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--accent-blue);">Tutorials</h3>
@@ -4056,6 +4482,35 @@
4056
4482
  </div>
4057
4483
  `).join('');
4058
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
+ }
4059
4514
  });
4060
4515
 
4061
4516
  // ? key opens help
@@ -4065,6 +4520,86 @@
4065
4520
  }
4066
4521
  });
4067
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
+
4068
4603
  </script>
4069
4604
 
4070
4605
  <!-- Operation Dialogs -->
@@ -4414,6 +4949,6 @@
4414
4949
  </div>
4415
4950
  </div>
4416
4951
 
4417
- <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.7</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>
4418
4953
  </body>
4419
4954
  </html>