cyclecad 0.8.7 → 0.9.7

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,26 @@
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; min-height: 0;">
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 style="margin-bottom:10px;"><b>Agent API</b><br>window.cycleCAD.execute({ method: "shape.cylinder", params: { radius: 25, height: 60 } })</div>
1698
+ <hr style="border-color:var(--border-color);margin:16px 0;">
1699
+ <h3 style="margin:0 0 12px 0;color:var(--accent-green);font-size:15px;">ExplodeView Integration</h3>
1700
+ <div style="margin-bottom:10px;"><b>Viewer Mode</b><br>Press <b>V</b> or click Viewer Mode to switch to ExplodeView. All 57 viewer features are available inside cycleCAD — shared 3D scene, no duplication.</div>
1701
+ <div style="margin-bottom:10px;"><b>Available in Viewer Mode</b><br>Assembly Tree, Explode/Collapse, Section Cut, Part Selection, BOM Export, Annotations, Measurement, AI Part ID, Heatmaps, Service Mode, and 45+ more tools.</div>
1702
+ <div style="margin-bottom:10px;"><b>Viewer API</b><br>viewer.toggleViewerMode(), viewer.selectPart(idx), viewer.explodeParts(0.5), viewer.setSectionCut('x', 100), viewer.exportBOM()</div>
1703
+ <div style="margin-bottom:10px;"><b>Docker (all services)</b><br>docker-compose up -d<br>ExplodeView :8080 · cycleCAD :3000 · STEP Converter :8787</div>
1704
+ <div style="padding:8px 10px;background:rgba(88,166,255,0.08);border:1px solid rgba(88,166,255,0.2);border-radius:6px;font-size:12px;color:var(--accent-blue);">Full integration tutorial: <a href="tutorials/explodeview-integration.html" style="color:var(--accent-blue);">tutorials/explodeview-integration.html</a></div>
1686
1705
  </div>
1687
1706
  <div id="tab-tokens" style="display: none; overflow-y: auto;">
1688
1707
  <!-- Token dashboard populated by token-dashboard.js -->
@@ -1762,25 +1781,27 @@
1762
1781
  <script type="module">
1763
1782
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1764
1783
  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';
1784
+ import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject } from './js/viewport.js?v=77';
1785
+ // fitAll defined locally to avoid import failures from cached viewport.js
1786
+ function fitAll(padding = 1.2) { const s = getScene(); if (s) fitToObject(s, padding); }
1787
+ import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=77';
1788
+ import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=77';
1789
+ import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=77';
1790
+ import { initTree, addFeature, selectFeature, onSelect, removeFeature } from './js/tree.js?v=77';
1791
+ import { initParams, showParams, onParamChange } from './js/params.js?v=77';
1792
+ import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=77';
1793
+ import { initShortcuts } from './js/shortcuts.js?v=77';
1794
+ import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=77';
1795
+ import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=77';
1796
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=77';
1797
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=77';
1798
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=77';
1799
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=77';
1800
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=77';
1801
+ import Assembly from './js/assembly.js?v=77';
1802
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=77';
1803
+ import { initAgentAPI } from './js/agent-api.js?v=77';
1804
+ import { initTokenDashboard } from './js/token-dashboard.js?v=77';
1784
1805
 
1785
1806
  // ========== Application State ==========
1786
1807
  const APP = {
@@ -1994,7 +2015,7 @@
1994
2015
 
1995
2016
  // Initialize Agent API — the primary interface
1996
2017
  await tryStepAsync('agentAPI', async () => {
1997
- const agentImports = await import('./js/agent-api.js?v=64');
2018
+ const agentImports = await import('./js/agent-api.js?v=77');
1998
2019
  const agentSession = initAgentAPI({
1999
2020
  viewport: {
2000
2021
  getCamera,
@@ -2869,21 +2890,40 @@
2869
2890
  const splash = document.getElementById('welcome-splash');
2870
2891
  if (splash) splash.classList.add('hidden');
2871
2892
 
2872
- const result = createPrimitive(prompt.type || prompt.action, prompt.params || prompt);
2893
+ // ---- Handle ACTION commands (delete, undo, move, etc.) ----
2894
+ if (prompt.action) {
2895
+ executeAction(prompt);
2896
+ return;
2897
+ }
2898
+
2899
+ // ---- Handle CREATE commands (box, cylinder, etc.) ----
2900
+ const pType = prompt.type || prompt.action;
2901
+ const pParams = prompt.params || prompt;
2902
+ const result = createPrimitive(pType, pParams);
2873
2903
  const mesh = result.mesh || result;
2874
2904
  addToScene(mesh);
2875
2905
  if (result.wireframe) addToScene(result.wireframe);
2876
2906
 
2907
+ // Build descriptive name
2908
+ let desc = pType;
2909
+ if (pParams.width && pParams.height) desc = `${pType} ${pParams.width}×${pParams.height}${pParams.thickness ? '×' + pParams.thickness : ''}mm`;
2910
+ else if (pParams.radius && pParams.height) desc = `${pType} r${pParams.radius} h${pParams.height}mm`;
2911
+ else if (pParams.radius) desc = `${pType} r${pParams.radius}mm`;
2912
+ else if (pParams.width) desc = `${pType} ${pParams.width}mm`;
2913
+
2877
2914
  const feature = {
2878
2915
  id: 'feature_' + Date.now(),
2879
- name: prompt.name || prompt.type || prompt.action || 'Part',
2880
- type: prompt.type || prompt.action,
2916
+ name: prompt.name || desc || 'Part',
2917
+ type: pType,
2881
2918
  mesh: mesh,
2882
- params: prompt.params || prompt,
2919
+ params: pParams,
2920
+ _geometryJSON: mesh.geometry ? mesh.geometry.toJSON() : null,
2921
+ _materialColor: mesh.material?.color ? '#' + mesh.material.color.getHexString() : '#58a6ff',
2883
2922
  };
2884
2923
  APP.features.push(feature);
2885
2924
  addFeature(feature);
2886
2925
  pushHistory();
2926
+ fitToObject(mesh);
2887
2927
  updateStatus(`Created: ${feature.name}`);
2888
2928
  } catch (err) {
2889
2929
  console.error('AI create failed:', err);
@@ -2891,6 +2931,391 @@
2891
2931
  }
2892
2932
  }
2893
2933
 
2934
+ function executeAction(cmd) {
2935
+ const features = APP.features;
2936
+
2937
+ switch (cmd.action) {
2938
+ case 'delete': {
2939
+ let idx = cmd.index;
2940
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2941
+ if (idx >= 0 && idx < features.length) {
2942
+ const f = features[idx];
2943
+ if (f.mesh) removeFromScene(f.mesh);
2944
+ features.splice(idx, 1);
2945
+ removeFeature(idx);
2946
+ APP.selectedFeature = null;
2947
+ pushHistory();
2948
+ updateStatus(`Deleted: ${f.name || 'Part'}`);
2949
+ }
2950
+ break;
2951
+ }
2952
+ case 'undo': undo(); break;
2953
+ case 'redo': redo(); break;
2954
+ case 'clearScene': {
2955
+ features.forEach(f => { if (f.mesh) removeFromScene(f.mesh); });
2956
+ APP.features = [];
2957
+ // Clear tree
2958
+ const treeEl = document.getElementById('feature-tree');
2959
+ if (treeEl) treeEl.innerHTML = '';
2960
+ pushHistory();
2961
+ updateStatus('Scene cleared');
2962
+ break;
2963
+ }
2964
+ case 'hide': {
2965
+ let idx = cmd.index;
2966
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2967
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
2968
+ features[idx].mesh.visible = false;
2969
+ updateStatus(`Hidden: ${features[idx].name || 'Part'}`);
2970
+ }
2971
+ break;
2972
+ }
2973
+ case 'showAll': {
2974
+ features.forEach(f => { if (f.mesh) f.mesh.visible = true; });
2975
+ updateStatus('All parts visible');
2976
+ break;
2977
+ }
2978
+ case 'select': {
2979
+ let idx = cmd.index;
2980
+ if (idx >= 0 && idx < features.length) {
2981
+ APP.selectedFeature = features[idx];
2982
+ APP.selectedFeatureIndex = idx;
2983
+ selectFeature(idx);
2984
+ updateStatus(`Selected: ${features[idx].name || 'Part'}`);
2985
+ }
2986
+ break;
2987
+ }
2988
+ case 'move': {
2989
+ let idx = cmd.index;
2990
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
2991
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
2992
+ const mesh = features[idx].mesh;
2993
+ const dist = cmd.distance || 20;
2994
+ if (cmd.axis === 'x') mesh.position.x += dist;
2995
+ else if (cmd.axis === 'y') mesh.position.y += dist;
2996
+ else if (cmd.axis === 'z') mesh.position.z += dist;
2997
+ pushHistory();
2998
+ updateStatus(`Moved: ${features[idx].name || 'Part'}`);
2999
+ }
3000
+ break;
3001
+ }
3002
+ case 'rotate': {
3003
+ let idx = cmd.index;
3004
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3005
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
3006
+ const mesh = features[idx].mesh;
3007
+ const angle = (cmd.angle || 90) * Math.PI / 180;
3008
+ if (cmd.axis === 'x') mesh.rotation.x += angle;
3009
+ else if (cmd.axis === 'y') mesh.rotation.y += angle;
3010
+ else if (cmd.axis === 'z') mesh.rotation.z += angle;
3011
+ pushHistory();
3012
+ updateStatus(`Rotated: ${features[idx].name || 'Part'}`);
3013
+ }
3014
+ break;
3015
+ }
3016
+ case 'scale': {
3017
+ let idx = cmd.index;
3018
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3019
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
3020
+ const mesh = features[idx].mesh;
3021
+ const factor = cmd.factor || 1.5;
3022
+ mesh.scale.multiplyScalar(factor);
3023
+ pushHistory();
3024
+ updateStatus(`Scaled: ${features[idx].name || 'Part'} × ${factor}`);
3025
+ }
3026
+ break;
3027
+ }
3028
+ case 'duplicate': {
3029
+ let idx = cmd.index;
3030
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3031
+ if (idx >= 0 && idx < features.length && features[idx].mesh) {
3032
+ const orig = features[idx];
3033
+ const clonedMesh = orig.mesh.clone();
3034
+ clonedMesh.position.x += 30; // offset so it's visible
3035
+ addToScene(clonedMesh);
3036
+ const newFeature = {
3037
+ id: 'feature_' + Date.now(),
3038
+ name: (orig.name || 'Part') + ' (copy)',
3039
+ type: orig.type,
3040
+ mesh: clonedMesh,
3041
+ params: { ...(orig.params || {}) },
3042
+ _geometryJSON: clonedMesh.geometry ? clonedMesh.geometry.toJSON() : null,
3043
+ _materialColor: orig._materialColor || '#58a6ff',
3044
+ };
3045
+ APP.features.push(newFeature);
3046
+ addFeature(newFeature);
3047
+ pushHistory();
3048
+ updateStatus(`Duplicated: ${orig.name || 'Part'}`);
3049
+ }
3050
+ break;
3051
+ }
3052
+ case 'color': {
3053
+ let idx = cmd.index;
3054
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3055
+ if (idx >= 0 && idx < features.length && features[idx].mesh?.material) {
3056
+ 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 };
3057
+ const hex = colorMap[cmd.color] || 0x58a6ff;
3058
+ features[idx].mesh.material.color.setHex(hex);
3059
+ features[idx]._materialColor = '#' + hex.toString(16).padStart(6, '0');
3060
+ updateStatus(`Colored: ${features[idx].name || 'Part'} → ${cmd.color}`);
3061
+ }
3062
+ break;
3063
+ }
3064
+ case 'rename': {
3065
+ let idx = cmd.index;
3066
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3067
+ if (idx >= 0 && idx < features.length) {
3068
+ features[idx].name = cmd.name;
3069
+ updateStatus(`Renamed to: ${cmd.name}`);
3070
+ }
3071
+ break;
3072
+ }
3073
+ case 'fitAll': {
3074
+ fitAll();
3075
+ updateStatus('View reset');
3076
+ break;
3077
+ }
3078
+ case 'wireframe': {
3079
+ vpToggleWireframe();
3080
+ updateStatus('Wireframe toggled');
3081
+ break;
3082
+ }
3083
+ case 'grid': {
3084
+ vpToggleGrid();
3085
+ updateStatus('Grid toggled');
3086
+ break;
3087
+ }
3088
+ case 'export': {
3089
+ if (cmd.format === 'stl') exportSTL();
3090
+ else if (cmd.format === 'obj') exportOBJ();
3091
+ else exportSTL();
3092
+ updateStatus(`Exported ${(cmd.format || 'STL').toUpperCase()}`);
3093
+ break;
3094
+ }
3095
+ case 'booleanSubtract':
3096
+ case 'booleanIntersect':
3097
+ case 'booleanUnion': {
3098
+ updateStatus(`Boolean ${cmd.action.replace('boolean', '')} — coming soon (requires CSG kernel)`);
3099
+ break;
3100
+ }
3101
+ // --- MODIFY DIMENSION (recreate part with new params) ---
3102
+ case 'modifyDimension': {
3103
+ let idx = cmd.index;
3104
+ if (idx === -1 || idx === undefined) idx = features.length - 1;
3105
+ if (idx >= 0 && idx < features.length) {
3106
+ const f = features[idx];
3107
+ const oldParams = { ...(f.params || {}) };
3108
+ const dim = cmd.dimension;
3109
+ const val = cmd.value;
3110
+
3111
+ // Map dimension name to param key
3112
+ if (dim === 'height' || dim === 'tall' || dim === 'high') {
3113
+ oldParams.height = val;
3114
+ } else if (dim === 'width' || dim === 'wide') {
3115
+ oldParams.width = val;
3116
+ } else if (dim === 'depth' || dim === 'deep' || dim === 'long') {
3117
+ oldParams.depth = val;
3118
+ } else if (dim === 'radius') {
3119
+ oldParams.radius = val;
3120
+ } else if (dim === 'diameter') {
3121
+ oldParams.radius = val / 2;
3122
+ } else if (dim === 'thickness') {
3123
+ oldParams.thickness = val;
3124
+ } else if (dim === 'size') {
3125
+ oldParams.width = val; oldParams.height = val; oldParams.depth = val;
3126
+ if (oldParams.radius !== undefined) oldParams.radius = val / 2;
3127
+ }
3128
+
3129
+ // Recreate the part with new params
3130
+ try {
3131
+ const pType = f.type || 'box';
3132
+ const result = createPrimitive(pType, oldParams);
3133
+ const newMesh = result.mesh || result;
3134
+
3135
+ // Copy position/rotation from old mesh
3136
+ if (f.mesh) {
3137
+ newMesh.position.copy(f.mesh.position);
3138
+ newMesh.rotation.copy(f.mesh.rotation);
3139
+ newMesh.scale.copy(f.mesh.scale);
3140
+ removeFromScene(f.mesh);
3141
+ }
3142
+ addToScene(newMesh);
3143
+ if (result.wireframe) addToScene(result.wireframe);
3144
+
3145
+ // Update feature in place
3146
+ f.mesh = newMesh;
3147
+ f.params = oldParams;
3148
+ f._geometryJSON = newMesh.geometry ? newMesh.geometry.toJSON() : null;
3149
+ f._materialColor = newMesh.material?.color ? '#' + newMesh.material.color.getHexString() : f._materialColor;
3150
+
3151
+ // Update name
3152
+ let desc = pType;
3153
+ if (oldParams.width && oldParams.height) desc = `${pType} ${oldParams.width}×${oldParams.height}${oldParams.depth ? '×' + oldParams.depth : ''}mm`;
3154
+ else if (oldParams.radius && oldParams.height) desc = `${pType} r${oldParams.radius} h${oldParams.height}mm`;
3155
+ else if (oldParams.radius) desc = `${pType} r${oldParams.radius}mm`;
3156
+ f.name = desc;
3157
+
3158
+ pushHistory();
3159
+ fitToObject(newMesh);
3160
+ updateStatus(`Modified: ${f.name}`);
3161
+ } catch (err) {
3162
+ console.error('Modify failed:', err);
3163
+ updateStatus('Modify failed: ' + err.message);
3164
+ }
3165
+ }
3166
+ break;
3167
+ }
3168
+ // --- SKETCH ---
3169
+ case 'startSketch': {
3170
+ startNewSketch();
3171
+ updateStatus('Sketch mode active');
3172
+ break;
3173
+ }
3174
+ case 'endSketch': {
3175
+ endSketch();
3176
+ APP.mode = 'idle';
3177
+ updateStatus('Sketch completed');
3178
+ break;
3179
+ }
3180
+ case 'sketchTool': {
3181
+ if (APP.mode !== 'sketch') startNewSketch();
3182
+ if (cmd.tool) setTool(cmd.tool);
3183
+ updateStatus(`Sketch tool: ${cmd.tool}`);
3184
+ break;
3185
+ }
3186
+ // --- EXTRUDE / REVOLVE ---
3187
+ case 'extrude': {
3188
+ doExtrude();
3189
+ break;
3190
+ }
3191
+ case 'revolve': {
3192
+ updateStatus('Revolve — select sketch first');
3193
+ break;
3194
+ }
3195
+ case 'cut': {
3196
+ updateStatus('Cut mode — select tool body');
3197
+ break;
3198
+ }
3199
+ // --- ADVANCED OPS ---
3200
+ case 'sweep': { updateStatus('Sweep — select profile and path'); break; }
3201
+ case 'loft': { updateStatus('Loft — select profiles'); break; }
3202
+ case 'shell': { updateStatus(`Shell ${cmd.thickness || 2}mm — select face to remove`); break; }
3203
+ case 'pattern': { updateStatus(`Pattern ${cmd.count || 4} copies`); break; }
3204
+ case 'mirror': { updateStatus(`Mirror across ${(cmd.plane || 'Y').toUpperCase()} plane`); break; }
3205
+ case 'thread': { updateStatus('Thread — select cylindrical face'); break; }
3206
+ case 'spring': { updateStatus(`Spring d=${cmd.diameter || 20}mm`); break; }
3207
+ case 'bend': { updateStatus('Sheet metal bend'); break; }
3208
+ case 'unfold': { updateStatus('Unfolding flat pattern'); break; }
3209
+ // --- VIEWS ---
3210
+ case 'setView': {
3211
+ const viewMap = { front: 'front', top: 'top', right: 'right', left: 'left', back: 'back', bottom: 'bottom', isometric: 'iso', iso: 'iso' };
3212
+ const v = viewMap[cmd.view] || 'front';
3213
+ try { setView(v); } catch(e) {}
3214
+ updateStatus(`${cmd.view} view`);
3215
+ break;
3216
+ }
3217
+ case 'zoomIn': {
3218
+ const cam = getCamera();
3219
+ if (cam) { cam.position.multiplyScalar(0.75); cam.updateProjectionMatrix(); }
3220
+ updateStatus('Zoomed in');
3221
+ break;
3222
+ }
3223
+ case 'zoomOut': {
3224
+ const cam2 = getCamera();
3225
+ if (cam2) { cam2.position.multiplyScalar(1.33); cam2.updateProjectionMatrix(); }
3226
+ updateStatus('Zoomed out');
3227
+ break;
3228
+ }
3229
+ case 'toggleTheme': {
3230
+ document.getElementById('btn-theme-toggle')?.click();
3231
+ updateStatus('Theme toggled');
3232
+ break;
3233
+ }
3234
+ // --- PANELS ---
3235
+ case 'openPanel': {
3236
+ const panelBtnMap = {
3237
+ help: 'btn-help', properties: null, guide: null, tokens: null,
3238
+ marketplace: 'btn-marketplace-v2', gdt: 'btn-gdt', misumi: 'btn-misumi',
3239
+ console: 'btn-console', dfm: 'btn-dfm', copilot: 'btn-ai-copilot',
3240
+ reverseEngineer: 'btn-reverse-engineer', materials: 'btn-materials',
3241
+ generative: 'btn-generative', cam: 'btn-cam', gcode: 'btn-gcode',
3242
+ collab: 'btn-collab', vr: 'btn-vr'
3243
+ };
3244
+ // For tab panels, switch tabs
3245
+ if (['properties', 'guide', 'tokens'].includes(cmd.panel)) {
3246
+ document.querySelector(`[data-tab="${cmd.panel}"]`)?.click();
3247
+ } else {
3248
+ const btnId = panelBtnMap[cmd.panel];
3249
+ if (btnId) document.getElementById(btnId)?.click();
3250
+ }
3251
+ updateStatus(`Opened: ${cmd.panel}`);
3252
+ break;
3253
+ }
3254
+ // --- IMPORT ---
3255
+ case 'import': {
3256
+ if (cmd.format === 'inventor') {
3257
+ document.getElementById('btn-inventor-import')?.click();
3258
+ } else if (cmd.format === 'step' || cmd.format === 'stl' || cmd.format === 'obj') {
3259
+ document.getElementById('btn-reverse-engineer')?.click();
3260
+ }
3261
+ updateStatus(`Import ${cmd.format}`);
3262
+ break;
3263
+ }
3264
+ // --- ASSEMBLY ---
3265
+ case 'assemblyMode': {
3266
+ document.getElementById('tool-assembly')?.click();
3267
+ updateStatus('Assembly mode');
3268
+ break;
3269
+ }
3270
+ case 'explode': {
3271
+ document.getElementById('tool-explode')?.click();
3272
+ updateStatus('Exploded view');
3273
+ break;
3274
+ }
3275
+ // --- MEASURE / SECTION ---
3276
+ case 'measure': {
3277
+ updateStatus('Measure tool — click two points');
3278
+ break;
3279
+ }
3280
+ case 'section': {
3281
+ document.getElementById('btn-section')?.click();
3282
+ updateStatus('Section cut active');
3283
+ break;
3284
+ }
3285
+ // --- SCREENSHOT ---
3286
+ case 'screenshot': {
3287
+ try {
3288
+ const renderer = window._renderer || getScene()?.parent;
3289
+ const canvas = document.querySelector('canvas');
3290
+ if (canvas) {
3291
+ const link = document.createElement('a');
3292
+ link.download = 'cyclecad-screenshot.png';
3293
+ link.href = canvas.toDataURL('image/png');
3294
+ link.click();
3295
+ updateStatus('Screenshot saved');
3296
+ }
3297
+ } catch(e) { updateStatus('Screenshot failed'); }
3298
+ break;
3299
+ }
3300
+ // --- SAVE / LOAD ---
3301
+ case 'save': {
3302
+ try { exportJSON(); updateStatus('Project saved as JSON'); } catch(e) { updateStatus('Save failed'); }
3303
+ break;
3304
+ }
3305
+ case 'load': {
3306
+ updateStatus('Use Import toolbar button to load files');
3307
+ break;
3308
+ }
3309
+ // --- CONSTRAINTS ---
3310
+ case 'addConstraint': {
3311
+ updateStatus(`Constraint: ${cmd.type} — select entities in sketch mode`);
3312
+ break;
3313
+ }
3314
+ default:
3315
+ updateStatus(`Unknown action: ${cmd.action}`);
3316
+ }
3317
+ }
3318
+
2894
3319
  function cancelOperation() {
2895
3320
  if (APP.mode === 'sketch') {
2896
3321
  endSketch();
@@ -2911,9 +3336,14 @@
2911
3336
 
2912
3337
  function deleteSelected() {
2913
3338
  if (!APP.selectedFeature) return;
2914
- removeFromScene(APP.selectedFeature.mesh);
2915
- APP.features = APP.features.filter(f => f.id !== APP.selectedFeature.id);
3339
+ const idx = APP.features.findIndex(f => f.id === APP.selectedFeature.id);
3340
+ if (APP.selectedFeature.mesh) removeFromScene(APP.selectedFeature.mesh);
3341
+ if (idx >= 0) {
3342
+ APP.features.splice(idx, 1);
3343
+ removeFeature(idx);
3344
+ }
2916
3345
  APP.selectedFeature = null;
3346
+ APP.selectedFeatureIndex = -1;
2917
3347
  pushHistory();
2918
3348
  updateStatus('Feature deleted');
2919
3349
  }
@@ -3613,7 +4043,7 @@
3613
4043
  'vr-panel': '🥽 CAD2VR',
3614
4044
  'generative-panel': '🧬 Generative Design'
3615
4045
  };
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>`;
4046
+ 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
4047
  panel.appendChild(header);
3618
4048
 
3619
4049
  const body = document.createElement('div');
@@ -3966,13 +4396,16 @@
3966
4396
  if (panel) { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; return; }
3967
4397
  panel = document.createElement('div');
3968
4398
  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);';
4399
+ 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
4400
  panel.innerHTML = `
3971
4401
  <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
4402
  <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>
4403
+ <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>
4404
+ </div>
4405
+ <div style="padding:10px 18px;border-bottom:1px solid var(--border-color);">
4406
+ <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
4407
  </div>
3975
- <div style="padding:18px;">
4408
+ <div style="padding:18px;overflow-y:auto;flex:1;min-height:0;">
3976
4409
  <!-- Tutorials Section -->
3977
4410
  <div style="margin-bottom:20px;">
3978
4411
  <h3 style="font-size:14px;font-weight:600;margin-bottom:12px;color:var(--accent-blue);">Tutorials</h3>
@@ -4056,6 +4489,35 @@
4056
4489
  </div>
4057
4490
  `).join('');
4058
4491
  }
4492
+
4493
+ // Wire search
4494
+ const helpSearchInput = document.getElementById('help-search-input');
4495
+ if (helpSearchInput) {
4496
+ let hst;
4497
+ helpSearchInput.addEventListener('input', () => {
4498
+ clearTimeout(hst);
4499
+ hst = setTimeout(() => {
4500
+ const q = helpSearchInput.value.toLowerCase().trim();
4501
+ // Filter feature categories
4502
+ panel.querySelectorAll('#help-feature-list > div').forEach(cat => {
4503
+ const items = cat.querySelectorAll('div:last-child > div');
4504
+ let any = false;
4505
+ items.forEach(item => {
4506
+ const m = !q || item.textContent.toLowerCase().includes(q);
4507
+ item.style.display = m ? '' : 'none';
4508
+ if (m) any = true;
4509
+ });
4510
+ cat.style.display = (!q || any) ? '' : 'none';
4511
+ // Auto-expand matching categories
4512
+ if (q && any) {
4513
+ const content = cat.querySelector('div:last-child');
4514
+ if (content) content.style.display = 'block';
4515
+ }
4516
+ });
4517
+ }, 150);
4518
+ });
4519
+ helpSearchInput.focus();
4520
+ }
4059
4521
  });
4060
4522
 
4061
4523
  // ? key opens help
@@ -4065,6 +4527,86 @@
4065
4527
  }
4066
4528
  });
4067
4529
 
4530
+ // ========== In-App Console (Ctrl+Shift+C) ==========
4531
+ (function initInAppConsole() {
4532
+ let consolePanel = null;
4533
+ const logs = [];
4534
+ const maxLogs = 200;
4535
+ const origConsole = { log: console.log, warn: console.warn, error: console.error, info: console.info };
4536
+
4537
+ function capture(level, args) {
4538
+ const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
4539
+ const msg = Array.from(args).map(a => {
4540
+ if (typeof a === 'string') return a;
4541
+ try { return JSON.stringify(a); } catch { return String(a); }
4542
+ }).join(' ');
4543
+ logs.push({ ts, level, msg });
4544
+ if (logs.length > maxLogs) logs.shift();
4545
+ if (consolePanel?.style.display !== 'none') renderConsoleLogs();
4546
+ }
4547
+
4548
+ console.log = function() { origConsole.log.apply(console, arguments); capture('log', arguments); };
4549
+ console.warn = function() { origConsole.warn.apply(console, arguments); capture('warn', arguments); };
4550
+ console.error = function() { origConsole.error.apply(console, arguments); capture('error', arguments); };
4551
+ console.info = function() { origConsole.info.apply(console, arguments); capture('info', arguments); };
4552
+
4553
+ window.addEventListener('error', (e) => {
4554
+ capture('error', [`Uncaught: ${e.message} (${e.filename}:${e.lineno})`]);
4555
+ });
4556
+ window.addEventListener('unhandledrejection', (e) => {
4557
+ capture('error', [`Unhandled Promise: ${e.reason}`]);
4558
+ });
4559
+
4560
+ function renderConsoleLogs() {
4561
+ const body = consolePanel?.querySelector('#console-log-body');
4562
+ if (!body) return;
4563
+ body.innerHTML = logs.map(l => {
4564
+ const color = l.level === 'error' ? '#f85149' : l.level === 'warn' ? '#d29922' : l.level === 'info' ? '#58a6ff' : '#8b949e';
4565
+ 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>`;
4566
+ }).join('');
4567
+ body.scrollTop = body.scrollHeight;
4568
+ }
4569
+
4570
+ function toggleConsole() {
4571
+ if (!consolePanel) {
4572
+ consolePanel = document.createElement('div');
4573
+ consolePanel.id = 'in-app-console';
4574
+ 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;';
4575
+ consolePanel.innerHTML = `
4576
+ <div style="display:flex;justify-content:space-between;align-items:center;padding:6px 12px;background:#161b22;border-bottom:1px solid #30363d;">
4577
+ <div style="display:flex;align-items:center;gap:12px;">
4578
+ <span style="font-weight:700;font-size:12px;color:#f0f0f0;">Console</span>
4579
+ <span id="console-error-count" style="font-size:10px;color:#f85149;"></span>
4580
+ </div>
4581
+ <div style="display:flex;gap:6px;">
4582
+ <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>
4583
+ <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>
4584
+ </div>
4585
+ </div>
4586
+ <div id="console-log-body" style="flex:1;overflow-y:auto;min-height:0;"></div>
4587
+ `;
4588
+ document.body.appendChild(consolePanel);
4589
+ } else {
4590
+ consolePanel.style.display = consolePanel.style.display === 'none' ? 'flex' : 'none';
4591
+ }
4592
+ renderConsoleLogs();
4593
+ const errCount = logs.filter(l => l.level === 'error').length;
4594
+ const ec = consolePanel.querySelector('#console-error-count');
4595
+ if (ec) ec.textContent = errCount ? `${errCount} errors` : '';
4596
+ }
4597
+
4598
+ // Ctrl+Shift+C or backtick to open console
4599
+ document.addEventListener('keydown', (e) => {
4600
+ if ((e.ctrlKey && e.shiftKey && e.key === 'C') || (e.key === '`' && !e.ctrlKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA')) {
4601
+ e.preventDefault();
4602
+ toggleConsole();
4603
+ }
4604
+ });
4605
+
4606
+ // Expose for toolbar button
4607
+ window._toggleInAppConsole = toggleConsole;
4608
+ })();
4609
+
4068
4610
  </script>
4069
4611
 
4070
4612
  <!-- Operation Dialogs -->
@@ -4414,6 +4956,6 @@
4414
4956
  </div>
4415
4957
  </div>
4416
4958
 
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>
4959
+ <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
4960
  </body>
4419
4961
  </html>