cyclecad 0.1.3 → 0.1.5

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
@@ -211,14 +211,37 @@
211
211
  }
212
212
 
213
213
  /* ===== Left Panel (Feature Tree) ===== */
214
- #left-panel-header {
215
- padding: 8px 12px;
214
+ #left-panel-tabs {
215
+ display: flex;
216
216
  border-bottom: 1px solid var(--border-color);
217
+ }
218
+
219
+ .left-tab {
220
+ flex: 1;
221
+ padding: 6px 8px;
222
+ font-size: 11px;
217
223
  font-weight: 600;
218
- font-size: 12px;
219
224
  color: var(--text-secondary);
220
225
  text-transform: uppercase;
221
- letter-spacing: 0.5px;
226
+ letter-spacing: 0.3px;
227
+ border-bottom: 2px solid transparent;
228
+ transition: all var(--transition-fast);
229
+ text-align: center;
230
+ }
231
+
232
+ .left-tab:hover {
233
+ color: var(--text-primary);
234
+ background: var(--bg-tertiary);
235
+ }
236
+
237
+ .left-tab.active {
238
+ color: var(--accent-blue);
239
+ border-bottom-color: var(--accent-blue);
240
+ }
241
+
242
+ .left-tab-content {
243
+ flex: 1;
244
+ min-height: 0;
222
245
  }
223
246
 
224
247
  #feature-tree {
@@ -228,6 +251,83 @@
228
251
  padding: 4px 0;
229
252
  }
230
253
 
254
+ /* Inline Project Browser in Left Panel */
255
+ #inline-project-browser {
256
+ display: flex;
257
+ flex-direction: column;
258
+ height: 100%;
259
+ min-height: 0;
260
+ }
261
+
262
+ .ipb-search-box {
263
+ padding: 6px 8px;
264
+ border-bottom: 1px solid var(--border-color);
265
+ }
266
+
267
+ #ipb-tree {
268
+ font-size: 12px;
269
+ }
270
+
271
+ .ipb-folder, .ipb-file {
272
+ padding: 3px 8px;
273
+ cursor: pointer;
274
+ display: flex;
275
+ align-items: center;
276
+ gap: 4px;
277
+ transition: background var(--transition-fast);
278
+ white-space: nowrap;
279
+ overflow: hidden;
280
+ text-overflow: ellipsis;
281
+ }
282
+
283
+ .ipb-folder:hover, .ipb-file:hover {
284
+ background: var(--bg-tertiary);
285
+ }
286
+
287
+ .ipb-file.selected {
288
+ background: rgba(88, 166, 255, 0.15);
289
+ color: var(--accent-blue);
290
+ }
291
+
292
+ .ipb-toggle {
293
+ width: 12px;
294
+ font-size: 8px;
295
+ color: var(--text-muted);
296
+ flex-shrink: 0;
297
+ transition: transform var(--transition-fast);
298
+ text-align: center;
299
+ }
300
+
301
+ .ipb-toggle.expanded {
302
+ transform: rotate(90deg);
303
+ }
304
+
305
+ .ipb-children {
306
+ padding-left: 12px;
307
+ }
308
+
309
+ .ipb-icon {
310
+ flex-shrink: 0;
311
+ font-size: 12px;
312
+ }
313
+
314
+ .ipb-label {
315
+ overflow: hidden;
316
+ text-overflow: ellipsis;
317
+ }
318
+
319
+ .ipb-badge {
320
+ font-size: 9px;
321
+ padding: 0 4px;
322
+ border-radius: 2px;
323
+ margin-left: auto;
324
+ flex-shrink: 0;
325
+ }
326
+
327
+ .ipb-badge-green { background: rgba(63, 185, 80, 0.2); color: var(--accent-green); }
328
+ .ipb-badge-blue { background: rgba(88, 166, 255, 0.2); color: var(--accent-blue); }
329
+ .ipb-badge-yellow { background: rgba(210, 153, 34, 0.2); color: var(--accent-yellow); }
330
+
231
331
  #feature-tree::-webkit-scrollbar {
232
332
  width: 10px;
233
333
  }
@@ -945,6 +1045,236 @@
945
1045
  }
946
1046
  }
947
1047
 
1048
+ /* ===== Project Browser ===== */
1049
+ #project-browser-container {
1050
+ position: fixed;
1051
+ top: 0;
1052
+ left: 0;
1053
+ bottom: 0;
1054
+ z-index: 500;
1055
+ }
1056
+
1057
+ /* ===== Operation Dialogs ===== */
1058
+ .operation-dialog {
1059
+ position: fixed;
1060
+ top: 50%;
1061
+ left: 50%;
1062
+ transform: translate(-50%, -50%);
1063
+ background: var(--bg-secondary);
1064
+ border: 1px solid var(--border-color);
1065
+ border-radius: 8px;
1066
+ box-shadow: var(--shadow-lg);
1067
+ z-index: 999;
1068
+ min-width: 340px;
1069
+ display: none;
1070
+ flex-direction: column;
1071
+ animation: slideIn 150ms cubic-bezier(0.4, 0, 0.2, 1);
1072
+ }
1073
+
1074
+ .operation-dialog.visible {
1075
+ display: flex;
1076
+ }
1077
+
1078
+ @keyframes slideIn {
1079
+ from {
1080
+ opacity: 0;
1081
+ transform: translate(-50%, -48%);
1082
+ }
1083
+ to {
1084
+ opacity: 1;
1085
+ transform: translate(-50%, -50%);
1086
+ }
1087
+ }
1088
+
1089
+ .dialog-header {
1090
+ display: flex;
1091
+ align-items: center;
1092
+ justify-content: space-between;
1093
+ padding: 12px 16px;
1094
+ border-bottom: 1px solid var(--border-color);
1095
+ }
1096
+
1097
+ .dialog-title {
1098
+ font-weight: 600;
1099
+ font-size: 14px;
1100
+ color: var(--text-primary);
1101
+ }
1102
+
1103
+ .dialog-close-btn {
1104
+ width: 20px;
1105
+ height: 20px;
1106
+ display: flex;
1107
+ align-items: center;
1108
+ justify-content: center;
1109
+ border-radius: 3px;
1110
+ color: var(--text-secondary);
1111
+ transition: all var(--transition-fast);
1112
+ cursor: pointer;
1113
+ }
1114
+
1115
+ .dialog-close-btn:hover {
1116
+ background: var(--bg-tertiary);
1117
+ color: var(--text-primary);
1118
+ }
1119
+
1120
+ .dialog-content {
1121
+ padding: 16px;
1122
+ overflow-y: auto;
1123
+ flex: 1;
1124
+ }
1125
+
1126
+ .dialog-footer {
1127
+ display: flex;
1128
+ gap: 8px;
1129
+ padding: 12px 16px;
1130
+ border-top: 1px solid var(--border-color);
1131
+ justify-content: flex-end;
1132
+ }
1133
+
1134
+ .dialog-button {
1135
+ padding: 8px 16px;
1136
+ border-radius: 4px;
1137
+ font-size: 12px;
1138
+ font-weight: 500;
1139
+ transition: all var(--transition-fast);
1140
+ cursor: pointer;
1141
+ border: 1px solid var(--border-color);
1142
+ }
1143
+
1144
+ .dialog-button.primary {
1145
+ background: var(--accent-blue-dark);
1146
+ color: var(--accent-blue);
1147
+ border-color: var(--accent-blue);
1148
+ }
1149
+
1150
+ .dialog-button.primary:hover:not(:disabled) {
1151
+ background: var(--accent-blue);
1152
+ color: #000;
1153
+ }
1154
+
1155
+ .dialog-button.secondary {
1156
+ background: transparent;
1157
+ color: var(--text-primary);
1158
+ }
1159
+
1160
+ .dialog-button.secondary:hover:not(:disabled) {
1161
+ background: var(--bg-tertiary);
1162
+ }
1163
+
1164
+ .dialog-form-group {
1165
+ margin-bottom: 12px;
1166
+ }
1167
+
1168
+ .dialog-form-group:last-child {
1169
+ margin-bottom: 0;
1170
+ }
1171
+
1172
+ .dialog-label {
1173
+ display: block;
1174
+ font-size: 11px;
1175
+ font-weight: 600;
1176
+ color: var(--text-secondary);
1177
+ text-transform: uppercase;
1178
+ letter-spacing: 0.3px;
1179
+ margin-bottom: 4px;
1180
+ }
1181
+
1182
+ .dialog-input {
1183
+ width: 100%;
1184
+ padding: 6px 8px;
1185
+ background: var(--bg-tertiary);
1186
+ color: var(--text-primary);
1187
+ border: 1px solid var(--border-color);
1188
+ border-radius: 3px;
1189
+ font-size: 11px;
1190
+ transition: border-color var(--transition-fast);
1191
+ }
1192
+
1193
+ .dialog-input:focus {
1194
+ outline: none;
1195
+ border-color: var(--accent-blue);
1196
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
1197
+ }
1198
+
1199
+ .dialog-input[type="range"] {
1200
+ width: 100%;
1201
+ height: 6px;
1202
+ padding: 0;
1203
+ cursor: pointer;
1204
+ }
1205
+
1206
+ .dialog-select {
1207
+ width: 100%;
1208
+ padding: 6px 8px;
1209
+ background: var(--bg-tertiary);
1210
+ color: var(--text-primary);
1211
+ border: 1px solid var(--border-color);
1212
+ border-radius: 3px;
1213
+ font-size: 11px;
1214
+ cursor: pointer;
1215
+ transition: border-color var(--transition-fast);
1216
+ }
1217
+
1218
+ .dialog-select:focus {
1219
+ outline: none;
1220
+ border-color: var(--accent-blue);
1221
+ }
1222
+
1223
+ .dialog-radio-group {
1224
+ display: flex;
1225
+ gap: 12px;
1226
+ flex-direction: column;
1227
+ }
1228
+
1229
+ .dialog-radio-item {
1230
+ display: flex;
1231
+ align-items: center;
1232
+ gap: 6px;
1233
+ cursor: pointer;
1234
+ }
1235
+
1236
+ .dialog-radio-item input[type="radio"] {
1237
+ cursor: pointer;
1238
+ accent-color: var(--accent-blue);
1239
+ }
1240
+
1241
+ .dialog-radio-item label {
1242
+ cursor: pointer;
1243
+ font-size: 12px;
1244
+ }
1245
+
1246
+ .dialog-checkbox {
1247
+ display: flex;
1248
+ align-items: center;
1249
+ gap: 6px;
1250
+ cursor: pointer;
1251
+ }
1252
+
1253
+ .dialog-checkbox input[type="checkbox"] {
1254
+ cursor: pointer;
1255
+ accent-color: var(--accent-blue);
1256
+ }
1257
+
1258
+ .dialog-checkbox label {
1259
+ cursor: pointer;
1260
+ font-size: 12px;
1261
+ }
1262
+
1263
+ .dialog-backdrop {
1264
+ position: fixed;
1265
+ top: 0;
1266
+ left: 0;
1267
+ right: 0;
1268
+ bottom: 0;
1269
+ background: rgba(0, 0, 0, 0.4);
1270
+ z-index: 998;
1271
+ display: none;
1272
+ }
1273
+
1274
+ .dialog-backdrop.visible {
1275
+ display: block;
1276
+ }
1277
+
948
1278
  </style>
949
1279
  </head>
950
1280
  <body>
@@ -1034,16 +1364,72 @@
1034
1364
  </button>
1035
1365
  </div>
1036
1366
 
1037
- <!-- Export Tools -->
1367
+ <!-- Advanced Operations -->
1038
1368
  <div class="toolbar-group">
1039
- <button class="toolbar-button" id="export-stl" title="Export STL">
1040
- <span class="toolbar-icon">💾</span>
1041
- <span class="toolbar-label">STL</span>
1369
+ <button class="toolbar-button" id="tool-sweep" title="Sweep (Profile Along Path)">
1370
+ <span class="toolbar-icon">&#8634;</span>
1371
+ <span class="toolbar-label">Sweep</span>
1042
1372
  </button>
1043
- <button class="toolbar-button" id="export-step" title="Export STEP">
1044
- <span class="toolbar-icon">💾</span>
1373
+ <button class="toolbar-button" id="tool-loft" title="Loft (Between Profiles)">
1374
+ <span class="toolbar-icon">&#9651;</span>
1375
+ <span class="toolbar-label">Loft</span>
1376
+ </button>
1377
+ <button class="toolbar-button" id="tool-spring" title="Generate Spring" style="background:rgba(63,185,80,0.08);">
1378
+ <span class="toolbar-icon">&#8635;</span>
1379
+ <span class="toolbar-label">Spring</span>
1380
+ </button>
1381
+ <button class="toolbar-button" id="tool-thread" title="Generate Thread" style="background:rgba(63,185,80,0.08);">
1382
+ <span class="toolbar-icon">&#10038;</span>
1383
+ <span class="toolbar-label">Thread</span>
1384
+ </button>
1385
+ </div>
1386
+
1387
+ <!-- Sheet Metal -->
1388
+ <div class="toolbar-group">
1389
+ <button class="toolbar-button" id="tool-bend" title="Sheet Metal Bend" style="background:rgba(210,153,34,0.08);">
1390
+ <span class="toolbar-icon">&#9484;</span>
1391
+ <span class="toolbar-label">Bend</span>
1392
+ </button>
1393
+ <button class="toolbar-button" id="tool-flange" title="Sheet Metal Flange" style="background:rgba(210,153,34,0.08);">
1394
+ <span class="toolbar-icon">&#9492;</span>
1395
+ <span class="toolbar-label">Flange</span>
1396
+ </button>
1397
+ <button class="toolbar-button" id="tool-unfold" title="Unfold Sheet Metal" style="background:rgba(210,153,34,0.08);">
1398
+ <span class="toolbar-icon">&#9645;</span>
1399
+ <span class="toolbar-label">Unfold</span>
1400
+ </button>
1401
+ </div>
1402
+
1403
+ <!-- Export Tools -->
1404
+ <div class="toolbar-group">
1405
+ <button class="toolbar-button" id="export-stl" title="Export STL">
1406
+ <span class="toolbar-icon">💾</span>
1407
+ <span class="toolbar-label">STL</span>
1408
+ </button>
1409
+ <button class="toolbar-button" id="export-step" title="Export STEP">
1410
+ <span class="toolbar-icon">💾</span>
1045
1411
  <span class="toolbar-label">STEP</span>
1046
1412
  </button>
1413
+ <button class="toolbar-button" id="export-dxf" title="Export DXF (2D Drawing)">
1414
+ <span class="toolbar-icon">📐</span>
1415
+ <span class="toolbar-label">DXF</span>
1416
+ </button>
1417
+ <button class="toolbar-button" id="export-multiview" title="Multi-View Engineering Drawing (DXF)">
1418
+ <span class="toolbar-icon">📄</span>
1419
+ <span class="toolbar-label">Drawing</span>
1420
+ </button>
1421
+ </div>
1422
+
1423
+ <!-- Assembly Tools -->
1424
+ <div class="toolbar-group">
1425
+ <button class="toolbar-button" id="tool-assembly" title="Assembly Workspace" style="background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.3);">
1426
+ <span class="toolbar-icon">&#9881;</span>
1427
+ <span class="toolbar-label">Assembly</span>
1428
+ </button>
1429
+ <button class="toolbar-button" id="tool-explode" title="Explode/Collapse Assembly">
1430
+ <span class="toolbar-icon">&#11043;</span>
1431
+ <span class="toolbar-label">Explode</span>
1432
+ </button>
1047
1433
  </div>
1048
1434
 
1049
1435
  <!-- Reverse Engineer -->
@@ -1091,11 +1477,26 @@
1091
1477
 
1092
1478
  <!-- Main Content Area -->
1093
1479
  <div id="content">
1094
- <!-- Left Panel: Feature Tree -->
1480
+ <!-- Left Panel: Feature Tree + Project Browser -->
1095
1481
  <div id="left-panel">
1096
- <div id="left-panel-header">Model Tree</div>
1097
- <div id="feature-tree">
1098
- <!-- Populated by JavaScript -->
1482
+ <div id="left-panel-tabs">
1483
+ <button class="left-tab active" data-left-tab="tree">Model Tree</button>
1484
+ <button class="left-tab" data-left-tab="browser">Project Browser</button>
1485
+ </div>
1486
+ <div id="left-tab-tree" class="left-tab-content" style="display:flex;flex-direction:column;flex:1;min-height:0;">
1487
+ <div id="feature-tree">
1488
+ <!-- Populated by JavaScript -->
1489
+ </div>
1490
+ </div>
1491
+ <div id="left-tab-browser" class="left-tab-content" style="display:none;flex-direction:column;flex:1;min-height:0;">
1492
+ <div id="inline-project-browser">
1493
+ <!-- Inline project browser tree (populated when DUO manifest loads) -->
1494
+ <div class="ipb-search-box">
1495
+ <input type="text" id="ipb-search" placeholder="Search 473 parts..." style="width:100%;padding:6px 8px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:3px;color:var(--text-primary);font-size:11px;">
1496
+ </div>
1497
+ <div id="ipb-stats" style="display:flex;gap:8px;padding:4px 8px;font-size:10px;color:var(--text-secondary);border-bottom:1px solid var(--border-color);"></div>
1498
+ <div id="ipb-tree" style="flex:1;overflow-y:auto;padding:4px 0;min-height:0;"></div>
1499
+ </div>
1099
1500
  </div>
1100
1501
  </div>
1101
1502
 
@@ -1133,6 +1534,7 @@
1133
1534
  <div id="properties-tabs">
1134
1535
  <button class="properties-tab active" data-tab="properties">Properties</button>
1135
1536
  <button class="properties-tab" data-tab="chat">Chat</button>
1537
+ <button class="properties-tab" data-tab="guide">Guide</button>
1136
1538
  </div>
1137
1539
 
1138
1540
  <!-- Properties Content -->
@@ -1143,12 +1545,18 @@
1143
1545
  <div id="tab-chat" style="display: none;">
1144
1546
  <!-- Chat tab populated by JavaScript -->
1145
1547
  </div>
1548
+ <div id="tab-guide" style="display: none;">
1549
+ <!-- Rebuild guide populated by JavaScript -->
1550
+ </div>
1146
1551
  </div>
1147
1552
  </div>
1148
1553
  </div>
1149
1554
 
1150
1555
  <!-- Bottom Status Bar -->
1151
1556
  <div id="statusbar">
1557
+ <div class="statusbar-item" id="status-bar-item">
1558
+ <span id="status-bar">Ready</span>
1559
+ </div>
1152
1560
  <div class="statusbar-item">
1153
1561
  <span class="status-indicator" id="kernel-status"></span>
1154
1562
  <span class="statusbar-label">Kernel:</span>
@@ -1183,6 +1591,7 @@
1183
1591
 
1184
1592
  <!-- Module Loader -->
1185
1593
  <script type="module">
1594
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1186
1595
  import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject } from './js/viewport.js';
1187
1596
  import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js';
1188
1597
  import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js';
@@ -1193,6 +1602,13 @@
1193
1602
  import { initShortcuts } from './js/shortcuts.js';
1194
1603
  import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js';
1195
1604
  import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js';
1605
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js';
1606
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js';
1607
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js';
1608
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js';
1609
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js';
1610
+ import Assembly from './js/assembly.js';
1611
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js';
1196
1612
 
1197
1613
  // ========== Application State ==========
1198
1614
  const APP = {
@@ -1202,6 +1618,8 @@
1202
1618
  features: [],
1203
1619
  history: [],
1204
1620
  historyIndex: -1,
1621
+ project: null, // Current Inventor project
1622
+ assembly: null, // Assembly workspace instance
1205
1623
  };
1206
1624
 
1207
1625
  // ========== Initialization ==========
@@ -1214,6 +1632,9 @@
1214
1632
  document.getElementById('kernel-status').classList.add('ready');
1215
1633
  document.getElementById('kernel-status-text').textContent = 'Ready';
1216
1634
 
1635
+ // 1b. Initialize assembly workspace
1636
+ APP.assembly = new Assembly(getScene());
1637
+
1217
1638
  // 2. Initialize feature tree
1218
1639
  const treeContainer = document.getElementById('feature-tree');
1219
1640
  if (treeContainer) initTree(treeContainer);
@@ -1272,8 +1693,8 @@
1272
1693
  circle: () => setTool('circle'),
1273
1694
  arc: () => setTool('arc'),
1274
1695
  extrude: () => doExtrude(),
1275
- undo: () => { /* TODO */ },
1276
- redo: () => { /* TODO */ },
1696
+ undo: () => undo(),
1697
+ redo: () => redo(),
1277
1698
  delete: () => deleteSelected(),
1278
1699
  escape: () => cancelOperation(),
1279
1700
  enter: () => confirmOperation(),
@@ -1285,7 +1706,7 @@
1285
1706
  viewBottom: () => setView('bottom'),
1286
1707
  viewIso: () => setView('iso'),
1287
1708
  toggleGrid: () => vpToggleGrid(),
1288
- fitAll: () => { /* TODO */ },
1709
+ fitAll: () => fitAll(),
1289
1710
  save: () => saveProject(),
1290
1711
  exportSTL: () => doExportSTL(),
1291
1712
  });
@@ -1293,7 +1714,46 @@
1293
1714
  // 9. Setup welcome splash
1294
1715
  setupWelcome();
1295
1716
 
1296
- // 10. Hard Refresh button nukes all caches, service workers, and reloads
1717
+ // 9b. Setup left panel tabs and inline browser
1718
+ setupLeftTabs();
1719
+ setupInlineBrowserClicks();
1720
+
1721
+ // 10. Initialize project browser
1722
+ initProjectBrowser(document.body, {
1723
+ onFileOpen: async (file) => {
1724
+ try {
1725
+ const buffer = file.buffer || await file.arrayBuffer();
1726
+ const result = parseInventorFile(new Uint8Array(buffer));
1727
+ // Add features to tree
1728
+ if (result.features) {
1729
+ result.features.forEach((f, i) => {
1730
+ addFeature({
1731
+ id: `inv-${Date.now()}-${i}`,
1732
+ type: f.type || 'Unknown',
1733
+ name: f.name || `Feature ${i+1}`,
1734
+ params: f.parameters || {},
1735
+ icon: f.icon || '📦'
1736
+ });
1737
+ });
1738
+ }
1739
+ // Generate rebuild guide
1740
+ const guide = generateGuide(result);
1741
+ // Show guide in right panel
1742
+ const propsContent = document.getElementById('tab-guide');
1743
+ if (propsContent) renderGuide(propsContent, guide);
1744
+ updateStatus(`Opened: ${result.metadata?.fileName || file.name} — ${result.features?.length || 0} features`);
1745
+ } catch (err) {
1746
+ console.error('Failed to open file:', err);
1747
+ updateStatus('Failed to open file: ' + err.message);
1748
+ }
1749
+ },
1750
+ onProjectLoad: (project) => {
1751
+ APP.project = project;
1752
+ updateStatus(`Project loaded: ${project.stats.parts} parts, ${project.stats.assemblies} assemblies`);
1753
+ }
1754
+ });
1755
+
1756
+ // 11. Hard Refresh button — nukes all caches, service workers, and reloads
1297
1757
  const hardRefreshBtn = document.getElementById('btn-hard-refresh');
1298
1758
  if (hardRefreshBtn) hardRefreshBtn.addEventListener('click', async () => {
1299
1759
  hardRefreshBtn.textContent = 'Clearing...';
@@ -1324,6 +1784,217 @@
1324
1784
  }
1325
1785
  }
1326
1786
 
1787
+ // ========== Left Panel Tab Switching ==========
1788
+ function switchLeftTab(tab) {
1789
+ document.querySelectorAll('.left-tab').forEach(t => {
1790
+ t.classList.toggle('active', t.dataset.leftTab === tab);
1791
+ });
1792
+ document.getElementById('left-tab-tree').style.display = tab === 'tree' ? 'flex' : 'none';
1793
+ document.getElementById('left-tab-browser').style.display = tab === 'browser' ? 'flex' : 'none';
1794
+ }
1795
+
1796
+ function setupLeftTabs() {
1797
+ document.querySelectorAll('.left-tab').forEach(tab => {
1798
+ tab.addEventListener('click', () => switchLeftTab(tab.dataset.leftTab));
1799
+ });
1800
+ }
1801
+
1802
+ // ========== Inline Project Browser (Left Panel) ==========
1803
+ let ipbState = { tree: null, expanded: new Set(), searchQuery: '', selectedPath: null };
1804
+
1805
+ function populateInlineBrowser(treeData) {
1806
+ ipbState.tree = treeData;
1807
+ ipbState.expanded.clear();
1808
+ ipbState.searchQuery = '';
1809
+
1810
+ // Auto-expand the first level
1811
+ if (treeData.children) {
1812
+ treeData.children.forEach((child, i) => {
1813
+ if (child.type === 'folder') ipbState.expanded.add('root-' + i);
1814
+ });
1815
+ }
1816
+
1817
+ renderInlineBrowser();
1818
+ updateInlineStats();
1819
+
1820
+ // Wire search
1821
+ const searchEl = document.getElementById('ipb-search');
1822
+ if (searchEl) {
1823
+ let timeout;
1824
+ searchEl.addEventListener('input', (e) => {
1825
+ clearTimeout(timeout);
1826
+ timeout = setTimeout(() => {
1827
+ ipbState.searchQuery = e.target.value.toLowerCase();
1828
+ renderInlineBrowser();
1829
+ }, 200);
1830
+ });
1831
+ }
1832
+ }
1833
+
1834
+ function renderInlineBrowser() {
1835
+ const container = document.getElementById('ipb-tree');
1836
+ if (!container || !ipbState.tree) return;
1837
+ container.innerHTML = renderIPBItems(ipbState.tree.children || [], 'root');
1838
+ }
1839
+
1840
+ function renderIPBItems(items, prefix) {
1841
+ if (!items || items.length === 0) return '';
1842
+ let html = '';
1843
+
1844
+ for (let i = 0; i < items.length; i++) {
1845
+ const item = items[i];
1846
+ const nodeId = prefix + '-' + i;
1847
+
1848
+ // Search filter
1849
+ if (ipbState.searchQuery && item.type !== 'folder') {
1850
+ if (!item.name.toLowerCase().includes(ipbState.searchQuery)) continue;
1851
+ }
1852
+
1853
+ // If folder, check if any children match search
1854
+ if (ipbState.searchQuery && item.type === 'folder') {
1855
+ if (!folderHasMatch(item, ipbState.searchQuery)) continue;
1856
+ }
1857
+
1858
+ const icon = getIPBIcon(item);
1859
+ const badge = getIPBBadge(item.category);
1860
+
1861
+ if (item.type === 'folder' && item.children && item.children.length > 0) {
1862
+ const isExp = ipbState.expanded.has(nodeId);
1863
+ html += `<div class="ipb-folder" data-ipb-id="${nodeId}" data-ipb-type="folder">
1864
+ <span class="ipb-toggle ${isExp ? 'expanded' : ''}">&#9654;</span>
1865
+ <span class="ipb-icon">${icon}</span>
1866
+ <span class="ipb-label">${escHTML(item.name)}</span>
1867
+ <span style="margin-left:auto;font-size:9px;color:var(--text-muted);">${item.children.length}</span>
1868
+ </div>`;
1869
+ if (isExp) {
1870
+ html += `<div class="ipb-children">${renderIPBItems(item.children, nodeId)}</div>`;
1871
+ }
1872
+ } else if (item.type !== 'folder') {
1873
+ const sel = ipbState.selectedPath === item.path ? ' selected' : '';
1874
+ html += `<div class="ipb-file${sel}" data-ipb-id="${nodeId}" data-ipb-type="${item.type}" data-ipb-path="${item.path || ''}" data-ipb-name="${escHTML(item.name)}">
1875
+ <span class="ipb-toggle" style="visibility:hidden;">&#8226;</span>
1876
+ <span class="ipb-icon">${icon}</span>
1877
+ <span class="ipb-label">${escHTML(item.name)}</span>
1878
+ ${badge}
1879
+ </div>`;
1880
+ }
1881
+ }
1882
+ return html;
1883
+ }
1884
+
1885
+ function folderHasMatch(folder, query) {
1886
+ if (!folder.children) return false;
1887
+ return folder.children.some(child => {
1888
+ if (child.type === 'folder') return folderHasMatch(child, query);
1889
+ return child.name.toLowerCase().includes(query);
1890
+ });
1891
+ }
1892
+
1893
+ function getIPBIcon(item) {
1894
+ const icons = { ipt: '&#128230;', iam: '&#127959;', idw: '&#128208;', ipj: '&#128203;', folder: '&#128193;' };
1895
+ return icons[item.type] || '&#128196;';
1896
+ }
1897
+
1898
+ function getIPBBadge(category) {
1899
+ if (!category) return '';
1900
+ const map = {
1901
+ custom: '<span class="ipb-badge ipb-badge-green">CUSTOM</span>',
1902
+ standard: '<span class="ipb-badge ipb-badge-blue">STD</span>',
1903
+ vendor: '<span class="ipb-badge ipb-badge-yellow">VENDOR</span>',
1904
+ };
1905
+ return map[category] || '';
1906
+ }
1907
+
1908
+ function escHTML(s) {
1909
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1910
+ }
1911
+
1912
+ function updateInlineStats() {
1913
+ const el = document.getElementById('ipb-stats');
1914
+ if (!el || !APP.project) return;
1915
+ const s = APP.project.stats || {};
1916
+ el.innerHTML = `<span>&#128230; ${s.parts || 0} parts</span><span>&#127959; ${s.assemblies || 0} asms</span><span>&#128196; ${s.total || 0} total</span>`;
1917
+ }
1918
+
1919
+ // Delegated click handler for inline browser
1920
+ function setupInlineBrowserClicks() {
1921
+ const container = document.getElementById('ipb-tree');
1922
+ if (!container) return;
1923
+ container.addEventListener('click', (e) => {
1924
+ const row = e.target.closest('[data-ipb-id]');
1925
+ if (!row) return;
1926
+ const nodeId = row.dataset.ipbId;
1927
+ const nodeType = row.dataset.ipbType;
1928
+
1929
+ if (nodeType === 'folder') {
1930
+ // Toggle expand
1931
+ if (ipbState.expanded.has(nodeId)) {
1932
+ ipbState.expanded.delete(nodeId);
1933
+ } else {
1934
+ ipbState.expanded.add(nodeId);
1935
+ }
1936
+ renderInlineBrowser();
1937
+ } else if (['ipt', 'iam'].includes(nodeType)) {
1938
+ // Select file → show info in guide tab
1939
+ ipbState.selectedPath = row.dataset.ipbPath;
1940
+ renderInlineBrowser();
1941
+ handleFileSelect(row.dataset.ipbName, row.dataset.ipbPath, nodeType);
1942
+ }
1943
+ });
1944
+ }
1945
+
1946
+ function handleFileSelect(name, path, type) {
1947
+ updateStatus(`Selected: ${name}`);
1948
+
1949
+ // Switch right panel to Guide tab and show file info
1950
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
1951
+ const guideTabBtn = document.querySelector('[data-tab="guide"]');
1952
+ if (guideTabBtn) guideTabBtn.classList.add('active');
1953
+ document.getElementById('tab-properties').style.display = 'none';
1954
+ document.getElementById('tab-chat').style.display = 'none';
1955
+ document.getElementById('tab-guide').style.display = 'block';
1956
+
1957
+ const guideContainer = document.getElementById('tab-guide');
1958
+ if (!guideContainer) return;
1959
+
1960
+ // Show file info + feature guide placeholder
1961
+ const ext = type.toUpperCase();
1962
+ const category = getFileCategoryFromPath(path);
1963
+ guideContainer.innerHTML = `
1964
+ <div style="padding:12px;display:flex;flex-direction:column;gap:12px;">
1965
+ <div style="font-weight:600;font-size:13px;color:var(--accent-blue);">${escHTML(name)}</div>
1966
+ <div style="font-size:11px;color:var(--text-secondary);">
1967
+ <div><strong>Type:</strong> Inventor ${ext} ${type === 'ipt' ? '(Part)' : '(Assembly)'}</div>
1968
+ <div><strong>Path:</strong> ${escHTML(path)}</div>
1969
+ <div><strong>Category:</strong> ${category}</div>
1970
+ </div>
1971
+ <div style="border-top:1px solid var(--border-color);padding-top:12px;">
1972
+ <div style="font-weight:600;font-size:12px;margin-bottom:8px;">Rebuild Guide</div>
1973
+ <p style="font-size:11px;color:var(--text-secondary);line-height:1.6;">
1974
+ To generate a detailed rebuild guide, import this file using the
1975
+ <strong>Import Inventor</strong> button in the toolbar. The parser will extract
1976
+ features, dimensions, and constraints, then generate step-by-step
1977
+ instructions for recreating the part in cycleCAD or Fusion 360.
1978
+ </p>
1979
+ </div>
1980
+ <div style="border-top:1px solid var(--border-color);padding-top:12px;">
1981
+ <div style="font-weight:600;font-size:12px;margin-bottom:8px;">Quick Actions</div>
1982
+ <div style="display:flex;flex-direction:column;gap:6px;">
1983
+ <button onclick="navigator.clipboard.writeText('${escHTML(path)}')" style="padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:3px;color:var(--text-primary);font-size:11px;text-align:left;cursor:pointer;">&#128203; Copy path</button>
1984
+ </div>
1985
+ </div>
1986
+ </div>
1987
+ `;
1988
+ }
1989
+
1990
+ function getFileCategoryFromPath(path) {
1991
+ if (!path) return 'Unknown';
1992
+ const lp = path.toLowerCase();
1993
+ if (lp.includes('content center') || lp.includes('libraries')) return 'Standard (DIN/ISO)';
1994
+ if (lp.includes('zukaufteile') || lp.includes('igus') || lp.includes('interroll') || lp.includes('rittal')) return 'Vendor / Buy-out';
1995
+ return 'Custom';
1996
+ }
1997
+
1327
1998
  // ========== Toolbar Wiring ==========
1328
1999
  function setupToolbar() {
1329
2000
  const bind = (id, fn) => {
@@ -1341,26 +2012,46 @@
1341
2012
 
1342
2013
  // 3D operations
1343
2014
  bind('tool-extrude', () => doExtrude());
1344
- bind('tool-revolve', () => updateStatus('Revolve: coming soon'));
1345
- bind('tool-fillet', () => updateStatus('Fillet: coming soon'));
1346
- bind('tool-chamfer', () => updateStatus('Chamfer: coming soon'));
1347
- bind('tool-cut', () => updateStatus('Boolean Cut: coming soon'));
1348
- bind('tool-union', () => updateStatus('Boolean Union: coming soon'));
2015
+ bind('tool-revolve', () => openDialog('revolve'));
2016
+ bind('tool-fillet', () => openDialog('fillet'));
2017
+ bind('tool-chamfer', () => openDialog('chamfer'));
2018
+ bind('tool-cut', () => openDialog('boolean'));
2019
+ bind('tool-union', () => { document.getElementById('bool-union').checked = true; openDialog('boolean'); });
1349
2020
 
1350
2021
  // Export
1351
2022
  bind('export-stl', () => doExportSTL());
1352
- bind('export-step', () => updateStatus('STEP export: coming soon'));
2023
+ bind('export-step', () => updateStatus('STEP export requires OpenCascade.js — coming soon'));
2024
+ bind('export-dxf', () => {
2025
+ if (APP.features.length === 0) { updateStatus('No geometry to export'); return; }
2026
+ try {
2027
+ const mesh = APP.features[APP.features.length - 1].mesh;
2028
+ if (!mesh) { updateStatus('Select a feature with geometry'); return; }
2029
+ const dxf = exportProjectionToDXF(mesh, 'front', { hiddenLines: true });
2030
+ downloadDXF(dxf, 'cyclecad-export.dxf');
2031
+ updateStatus('DXF exported: front view projection');
2032
+ } catch (err) { updateStatus('DXF export failed: ' + err.message); console.error(err); }
2033
+ });
2034
+ bind('export-multiview', () => {
2035
+ if (APP.features.length === 0) { updateStatus('No geometry to export'); return; }
2036
+ try {
2037
+ const mesh = APP.features[APP.features.length - 1].mesh;
2038
+ if (!mesh) { updateStatus('Select a feature with geometry'); return; }
2039
+ const dxf = exportMultiViewDXF(mesh, { titleBlock: true, title: 'cycleCAD Part', author: 'cycleCAD', company: 'cycleWASH' });
2040
+ downloadDXF(dxf, 'cyclecad-drawing.dxf');
2041
+ updateStatus('Multi-view engineering drawing exported');
2042
+ } catch (err) { updateStatus('Drawing export failed: ' + err.message); console.error(err); }
2043
+ });
1353
2044
 
1354
2045
  // Edit
1355
- bind('btn-undo', () => updateStatus('Undo: coming soon'));
1356
- bind('btn-redo', () => updateStatus('Redo: coming soon'));
2046
+ bind('btn-undo', () => { undo(); });
2047
+ bind('btn-redo', () => { redo(); });
1357
2048
 
1358
2049
  // Views
1359
2050
  bind('view-front', () => setView('front'));
1360
2051
  bind('view-top', () => setView('top'));
1361
2052
  bind('view-right', () => setView('right'));
1362
2053
  bind('view-iso', () => setView('iso'));
1363
- bind('view-fit', () => updateStatus('Fit All'));
2054
+ bind('view-fit', () => fitAll());
1364
2055
 
1365
2056
  // Reverse Engineer
1366
2057
  bind('btn-reverse-engineer', () => {
@@ -1387,9 +2078,103 @@
1387
2078
  });
1388
2079
  });
1389
2080
  }
2081
+
2082
+ // Generate rebuild guide
2083
+ try {
2084
+ const guide = generateGuide(parsedData);
2085
+ // Show guide in right panel
2086
+ const guideTab = document.getElementById('tab-guide');
2087
+ if (guideTab) {
2088
+ renderGuide(guideTab, guide);
2089
+ // Switch to guide tab
2090
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
2091
+ const guideTabBtn = document.querySelector('[data-tab="guide"]');
2092
+ if (guideTabBtn) guideTabBtn.classList.add('active');
2093
+ document.getElementById('tab-properties').style.display = 'none';
2094
+ document.getElementById('tab-chat').style.display = 'none';
2095
+ guideTab.style.display = 'block';
2096
+ }
2097
+ } catch (err) {
2098
+ console.warn('Failed to generate rebuild guide:', err);
2099
+ }
2100
+
1390
2101
  updateStatus(`Inventor file loaded: ${parsedData.metadata?.fileName || 'unknown'} — ${parsedData.features?.length || 0} features found`);
1391
2102
  });
1392
2103
  });
2104
+
2105
+ // Advanced Operations
2106
+ bind('tool-sweep', () => openDialog('sweep'));
2107
+ bind('tool-loft', () => openDialog('loft'));
2108
+ bind('tool-spring', () => {
2109
+ const splash = document.getElementById('welcome-splash');
2110
+ if (splash) splash.classList.add('hidden');
2111
+ const radius = parseFloat(prompt('Spring outer radius (mm):', '10') || '0');
2112
+ const wireR = parseFloat(prompt('Wire radius (mm):', '1.5') || '0');
2113
+ const height = parseFloat(prompt('Spring height (mm):', '40') || '0');
2114
+ const turns = parseFloat(prompt('Number of turns:', '8') || '0');
2115
+ if (!radius || !wireR || !height || !turns) { updateStatus('Cancelled'); return; }
2116
+ try {
2117
+ const mesh = createSpring(radius, wireR, height, turns);
2118
+ addToScene(mesh);
2119
+ const feature = { id: 'feature_' + Date.now(), name: `Spring (R${radius} H${height})`, type: 'spring', mesh, params: { radius, wireR, height, turns } };
2120
+ APP.features.push(feature);
2121
+ addFeature(feature);
2122
+ pushHistory();
2123
+ updateStatus(`Created spring: R${radius}mm, ${turns} turns, H${height}mm`);
2124
+ } catch (err) { updateStatus('Spring failed: ' + err.message); }
2125
+ });
2126
+ bind('tool-thread', () => {
2127
+ const splash = document.getElementById('welcome-splash');
2128
+ if (splash) splash.classList.add('hidden');
2129
+ const outerR = parseFloat(prompt('Thread outer radius (mm):', '5') || '0');
2130
+ const innerR = parseFloat(prompt('Thread inner radius (mm):', '4') || '0');
2131
+ const pitch = parseFloat(prompt('Thread pitch (mm):', '1') || '0');
2132
+ const length = parseFloat(prompt('Thread length (mm):', '20') || '0');
2133
+ if (!outerR || !innerR || !pitch || !length) { updateStatus('Cancelled'); return; }
2134
+ try {
2135
+ const mesh = createThread(outerR, innerR, pitch, length);
2136
+ addToScene(mesh);
2137
+ const feature = { id: 'feature_' + Date.now(), name: `Thread M${outerR*2}x${pitch}`, type: 'thread', mesh, params: { outerR, innerR, pitch, length } };
2138
+ APP.features.push(feature);
2139
+ addFeature(feature);
2140
+ pushHistory();
2141
+ updateStatus(`Created thread: M${outerR*2}x${pitch}, L${length}mm`);
2142
+ } catch (err) { updateStatus('Thread failed: ' + err.message); }
2143
+ });
2144
+
2145
+ // Sheet Metal
2146
+ bind('tool-bend', () => openDialog('bend'));
2147
+ bind('tool-flange', () => openDialog('flange'));
2148
+ bind('tool-unfold', () => {
2149
+ if (APP.features.length === 0) { updateStatus('No features to unfold'); return; }
2150
+ updateStatus('Sheet metal unfold: select a bent part, then define bend lines');
2151
+ });
2152
+
2153
+ // Assembly
2154
+ let assemblyMode = false;
2155
+ bind('tool-assembly', () => {
2156
+ assemblyMode = !assemblyMode;
2157
+ if (assemblyMode) {
2158
+ updateStatus('Assembly mode ON — click parts to add to assembly. Right-click for mate constraints.');
2159
+ document.getElementById('tool-assembly').style.background = 'rgba(139,92,246,0.3)';
2160
+ } else {
2161
+ updateStatus('Assembly mode OFF');
2162
+ document.getElementById('tool-assembly').style.background = 'rgba(139,92,246,0.1)';
2163
+ }
2164
+ });
2165
+
2166
+ let exploded = false;
2167
+ bind('tool-explode', () => {
2168
+ if (!APP.assembly) return;
2169
+ exploded = !exploded;
2170
+ if (exploded) {
2171
+ APP.assembly.explodeAssembly(2.0);
2172
+ updateStatus('Assembly exploded — click again to collapse');
2173
+ } else {
2174
+ APP.assembly.collapseAssembly();
2175
+ updateStatus('Assembly collapsed');
2176
+ }
2177
+ });
1393
2178
  }
1394
2179
 
1395
2180
  // ========== Tab Switching ==========
@@ -1401,6 +2186,7 @@
1401
2186
  const target = tab.getAttribute('data-tab');
1402
2187
  document.getElementById('tab-properties').style.display = target === 'properties' ? 'block' : 'none';
1403
2188
  document.getElementById('tab-chat').style.display = target === 'chat' ? 'flex' : 'none';
2189
+ document.getElementById('tab-guide').style.display = target === 'guide' ? 'block' : 'none';
1404
2190
  });
1405
2191
  });
1406
2192
  }
@@ -1457,10 +2243,73 @@
1457
2243
  if (chatInput) chatInput.focus();
1458
2244
  });
1459
2245
 
1460
- // DUO Project Browser — open in new tab
2246
+ // DUO Project Browser — load manifest and show browser
1461
2247
  const browserBtn = document.getElementById('btn-open-browser');
1462
- if (browserBtn) browserBtn.addEventListener('click', () => {
1463
- window.open('duo-project-browser.html', '_blank');
2248
+ if (browserBtn) browserBtn.addEventListener('click', async () => {
2249
+ try {
2250
+ hide();
2251
+ const spinner = document.getElementById('kernel-spinner');
2252
+ if (spinner) spinner.classList.add('active');
2253
+ updateStatus('Loading DUO project manifest...');
2254
+
2255
+ // Fetch pre-built manifest (local dev only — not in public repo)
2256
+ const resp = await fetch('duo-manifest.json');
2257
+ if (!resp.ok) {
2258
+ // Manifest not available (e.g. on public site) — fall back to folder picker
2259
+ if (spinner) spinner.classList.remove('active');
2260
+ updateStatus('DUO manifest not found — use File System Access to open a project folder');
2261
+ try {
2262
+ const handle = await showFolderPicker();
2263
+ if (handle) {
2264
+ const project = await loadProject(handle);
2265
+ APP.project = project;
2266
+ setProject(project);
2267
+ populateInlineBrowser(project);
2268
+ showBrowser();
2269
+ switchLeftTab('browser');
2270
+ updateStatus(`Project loaded: ${project.stats?.parts || 0} parts`);
2271
+ }
2272
+ } catch (e) { updateStatus('Folder selection cancelled'); }
2273
+ return;
2274
+ }
2275
+ const manifest = await resp.json();
2276
+
2277
+ // Transform file types: manifest uses type:"file" + ext:".ipt"
2278
+ // but project-browser expects type:"ipt" directly
2279
+ function transformTree(node) {
2280
+ if (node.type === 'file' && node.ext) {
2281
+ node.type = node.ext.replace('.', ''); // ".ipt" → "ipt"
2282
+ }
2283
+ if (node.children) node.children.forEach(transformTree);
2284
+ return node;
2285
+ }
2286
+ transformTree(manifest.tree);
2287
+
2288
+ // Store project data
2289
+ APP.project = manifest;
2290
+
2291
+ // Pass tree root to overlay browser (it needs .children at top level)
2292
+ setProject(manifest.tree);
2293
+
2294
+ // Also populate the inline left-panel browser
2295
+ populateInlineBrowser(manifest.tree);
2296
+
2297
+ // Show browser overlay (dismiss with close button, then left panel has it too)
2298
+ showBrowser();
2299
+
2300
+ // Switch left panel to Project Browser tab
2301
+ switchLeftTab('browser');
2302
+
2303
+ const stats = manifest.stats || {};
2304
+ updateStatus(`DUO project loaded: ${stats.parts || 0} parts, ${stats.assemblies || 0} assemblies, ${stats.total || 0} total files`);
2305
+
2306
+ if (spinner) spinner.classList.remove('active');
2307
+ } catch (err) {
2308
+ console.error('Failed to load DUO project:', err);
2309
+ updateStatus('Failed to load project: ' + err.message);
2310
+ const spinner = document.getElementById('kernel-spinner');
2311
+ if (spinner) spinner.classList.remove('active');
2312
+ }
1464
2313
  });
1465
2314
  }
1466
2315
 
@@ -1512,6 +2361,7 @@
1512
2361
  };
1513
2362
  APP.features.push(feature);
1514
2363
  addFeature(feature);
2364
+ pushHistory();
1515
2365
 
1516
2366
  updateStatus(`Created extrusion: ${h}mm`);
1517
2367
  document.getElementById('mode-indicator').textContent = 'Ready';
@@ -1539,6 +2389,7 @@
1539
2389
  };
1540
2390
  APP.features.push(feature);
1541
2391
  addFeature(feature);
2392
+ pushHistory();
1542
2393
  updateStatus(`Created: ${feature.name}`);
1543
2394
  } catch (err) {
1544
2395
  console.error('AI create failed:', err);
@@ -1569,9 +2420,90 @@
1569
2420
  removeFromScene(APP.selectedFeature.mesh);
1570
2421
  APP.features = APP.features.filter(f => f.id !== APP.selectedFeature.id);
1571
2422
  APP.selectedFeature = null;
2423
+ pushHistory();
1572
2424
  updateStatus('Feature deleted');
1573
2425
  }
1574
2426
 
2427
+ function undo() {
2428
+ if (APP.historyIndex > 0) {
2429
+ APP.historyIndex--;
2430
+ restoreFromHistory();
2431
+ updateStatus('Undo');
2432
+ } else {
2433
+ updateStatus('Nothing to undo');
2434
+ }
2435
+ }
2436
+
2437
+ function redo() {
2438
+ if (APP.historyIndex < APP.history.length - 1) {
2439
+ APP.historyIndex++;
2440
+ restoreFromHistory();
2441
+ updateStatus('Redo');
2442
+ } else {
2443
+ updateStatus('Nothing to redo');
2444
+ }
2445
+ }
2446
+
2447
+ function restoreFromHistory() {
2448
+ const state = APP.history[APP.historyIndex];
2449
+ if (state) {
2450
+ // Clear current scene
2451
+ APP.features.forEach((f) => {
2452
+ if (f.mesh) removeFromScene(f.mesh);
2453
+ });
2454
+
2455
+ // Restore features from history state
2456
+ APP.features = [];
2457
+ if (state.features && Array.isArray(state.features)) {
2458
+ state.features.forEach((featureData) => {
2459
+ try {
2460
+ const primitive = createPrimitive(featureData.type, featureData.params);
2461
+ addToScene(primitive.mesh);
2462
+
2463
+ const feature = {
2464
+ id: featureData.id,
2465
+ name: featureData.name,
2466
+ type: featureData.type,
2467
+ mesh: primitive.mesh,
2468
+ params: featureData.params,
2469
+ };
2470
+
2471
+ APP.features.push(feature);
2472
+ addFeature(feature);
2473
+ } catch (err) {
2474
+ console.warn(`Failed to restore feature ${featureData.name}:`, err);
2475
+ }
2476
+ });
2477
+ }
2478
+ }
2479
+ }
2480
+
2481
+ function fitAll() {
2482
+ if (APP.features.length === 0) {
2483
+ updateStatus('Nothing to fit');
2484
+ return;
2485
+ }
2486
+
2487
+ // Create a temporary group of all features to fit camera
2488
+ const group = new THREE.Group();
2489
+ APP.features.forEach((f) => {
2490
+ if (f.mesh) {
2491
+ group.add(f.mesh);
2492
+ }
2493
+ });
2494
+
2495
+ // Create a bounding box to check if there's anything to show
2496
+ const box = new THREE.Box3().setFromObject(group);
2497
+ if (box.isEmpty()) {
2498
+ updateStatus('No visible features to fit');
2499
+ return;
2500
+ }
2501
+
2502
+ // Fit camera to all features with padding
2503
+ fitToObject(group, 1.3);
2504
+ updateStatus('Fit all features');
2505
+ }
2506
+
1575
2507
  function doExportSTL() {
1576
2508
  if (APP.features.length === 0) { updateStatus('Nothing to export'); return; }
1577
2509
  try {
@@ -1606,6 +2538,32 @@
1606
2538
  if (modeEl && APP.mode === 'idle') modeEl.textContent = 'Ready';
1607
2539
  }
1608
2540
 
2541
+ function pushHistory() {
2542
+ // Trim redo stack if not at end
2543
+ if (APP.historyIndex < APP.history.length - 1) {
2544
+ APP.history = APP.history.slice(0, APP.historyIndex + 1);
2545
+ }
2546
+
2547
+ // Save state snapshot
2548
+ APP.history.push({
2549
+ features: JSON.parse(JSON.stringify(APP.features.map((f) => ({
2550
+ id: f.id,
2551
+ name: f.name,
2552
+ type: f.type,
2553
+ params: f.params,
2554
+ })))),
2555
+ timestamp: Date.now(),
2556
+ });
2557
+
2558
+ APP.historyIndex = APP.history.length - 1;
2559
+
2560
+ // Keep history limited to 50 entries
2561
+ if (APP.history.length > 50) {
2562
+ APP.history.shift();
2563
+ APP.historyIndex--;
2564
+ }
2565
+ }
2566
+
1609
2567
  // ========== FPS Counter ==========
1610
2568
  let frameCount = 0;
1611
2569
  let lastFPSTime = performance.now();
@@ -1630,6 +2588,640 @@
1630
2588
  }
1631
2589
 
1632
2590
  window.cycleCAD = { version: '1.0.0', APP, init };
2591
+
2592
+ // ========== Operation Dialog Management ==========
2593
+ let currentDialogId = null;
2594
+
2595
+ function openDialog(dialogId) {
2596
+ // Close any open dialog first
2597
+ if (currentDialogId) closeDialog(currentDialogId);
2598
+
2599
+ const dialog = document.getElementById(`dialog-${dialogId}`);
2600
+ const backdrop = document.getElementById('dialog-backdrop');
2601
+
2602
+ if (dialog && backdrop) {
2603
+ currentDialogId = dialogId;
2604
+ dialog.classList.add('visible');
2605
+ backdrop.classList.add('visible');
2606
+ updateStatus(`${dialogId.charAt(0).toUpperCase() + dialogId.slice(1)} dialog opened`);
2607
+ }
2608
+ }
2609
+
2610
+ function closeDialog(dialogId) {
2611
+ const dialog = document.getElementById(`dialog-${dialogId}`);
2612
+ const backdrop = document.getElementById('dialog-backdrop');
2613
+
2614
+ if (dialog) {
2615
+ dialog.classList.remove('visible');
2616
+ }
2617
+
2618
+ // Check if any dialog is still visible
2619
+ const visibleDialogs = document.querySelectorAll('.operation-dialog.visible');
2620
+ if (visibleDialogs.length === 0) {
2621
+ if (backdrop) backdrop.classList.remove('visible');
2622
+ currentDialogId = null;
2623
+ }
2624
+ }
2625
+
2626
+ function applyRevolve() {
2627
+ const axis = document.getElementById('revolve-axis').value;
2628
+ const angle = parseFloat(document.getElementById('revolve-angle').value);
2629
+ const direction = document.querySelector('input[name="revolve-direction"]:checked').value;
2630
+
2631
+ try {
2632
+ revolveProfile({ axis, angle, direction });
2633
+ updateStatus(`Revolved: ${angle}° around ${axis.toUpperCase()} axis`);
2634
+ pushHistory();
2635
+ closeDialog('revolve');
2636
+ } catch (err) {
2637
+ updateStatus('Revolve failed: ' + err.message);
2638
+ }
2639
+ }
2640
+
2641
+ function applyFillet() {
2642
+ const radius = parseFloat(document.getElementById('fillet-radius').value);
2643
+ const preview = document.getElementById('fillet-preview').checked;
2644
+
2645
+ try {
2646
+ filletEdges({ radius, preview });
2647
+ updateStatus(`Fillet applied: ${radius}mm radius`);
2648
+ pushHistory();
2649
+ closeDialog('fillet');
2650
+ } catch (err) {
2651
+ updateStatus('Fillet failed: ' + err.message);
2652
+ }
2653
+ }
2654
+
2655
+ function applyChamer() {
2656
+ const distance = parseFloat(document.getElementById('chamfer-distance').value);
2657
+
2658
+ try {
2659
+ chamferEdges({ distance });
2660
+ updateStatus(`Chamfer applied: ${distance}mm`);
2661
+ pushHistory();
2662
+ closeDialog('chamfer');
2663
+ } catch (err) {
2664
+ updateStatus('Chamfer failed: ' + err.message);
2665
+ }
2666
+ }
2667
+
2668
+ function applyBoolean() {
2669
+ const operation = document.querySelector('input[name="boolean-op"]:checked').value;
2670
+
2671
+ try {
2672
+ booleanOperation({ operation });
2673
+ updateStatus(`Boolean ${operation} applied`);
2674
+ pushHistory();
2675
+ closeDialog('boolean');
2676
+ } catch (err) {
2677
+ updateStatus('Boolean operation failed: ' + err.message);
2678
+ }
2679
+ }
2680
+
2681
+ function applyShell() {
2682
+ const thickness = parseFloat(document.getElementById('shell-thickness').value);
2683
+
2684
+ try {
2685
+ shellOperation({ thickness });
2686
+ updateStatus(`Shell created: ${thickness}mm wall thickness`);
2687
+ pushHistory();
2688
+ closeDialog('shell');
2689
+ } catch (err) {
2690
+ updateStatus('Shell failed: ' + err.message);
2691
+ }
2692
+ }
2693
+
2694
+ function updatePatternUI() {
2695
+ const patternType = document.querySelector('input[name="pattern-type"]:checked').value;
2696
+ const rectFields = document.getElementById('pattern-rectangular');
2697
+ const circFields = document.getElementById('pattern-circular');
2698
+
2699
+ if (patternType === 'rectangular') {
2700
+ rectFields.style.display = 'block';
2701
+ circFields.style.display = 'none';
2702
+ } else {
2703
+ rectFields.style.display = 'none';
2704
+ circFields.style.display = 'block';
2705
+ }
2706
+ }
2707
+
2708
+ function applyPattern() {
2709
+ const patternType = document.querySelector('input[name="pattern-type"]:checked').value;
2710
+ let params = { type: patternType };
2711
+
2712
+ try {
2713
+ if (patternType === 'rectangular') {
2714
+ params.countX = parseInt(document.getElementById('pattern-count-x').value);
2715
+ params.countY = parseInt(document.getElementById('pattern-count-y').value);
2716
+ params.spacingX = parseFloat(document.getElementById('pattern-spacing-x').value);
2717
+ params.spacingY = parseFloat(document.getElementById('pattern-spacing-y').value);
2718
+ patternFeature(params);
2719
+ updateStatus(`Rectangular pattern: ${params.countX}×${params.countY}`);
2720
+ } else {
2721
+ params.count = parseInt(document.getElementById('pattern-count').value);
2722
+ params.radius = parseFloat(document.getElementById('pattern-radius').value);
2723
+ params.axis = document.getElementById('pattern-axis').value;
2724
+ patternFeature(params);
2725
+ updateStatus(`Circular pattern: ${params.count} instances`);
2726
+ }
2727
+ pushHistory();
2728
+ closeDialog('pattern');
2729
+ } catch (err) {
2730
+ updateStatus('Pattern failed: ' + err.message);
2731
+ }
2732
+ }
2733
+
2734
+ // ========== Advanced Operation Apply Functions ==========
2735
+ function generateProfilePoints(shape, size, segments = 32) {
2736
+ const pts = [];
2737
+ if (shape === 'circle') {
2738
+ for (let i = 0; i < segments; i++) {
2739
+ const a = (i / segments) * Math.PI * 2;
2740
+ pts.push({ x: Math.cos(a) * size, y: Math.sin(a) * size });
2741
+ }
2742
+ } else if (shape === 'rectangle') {
2743
+ const h = size * 0.6;
2744
+ pts.push({ x: -size, y: -h }, { x: size, y: -h }, { x: size, y: h }, { x: -size, y: h });
2745
+ } else if (shape === 'hexagon') {
2746
+ for (let i = 0; i < 6; i++) {
2747
+ const a = (i / 6) * Math.PI * 2;
2748
+ pts.push({ x: Math.cos(a) * size, y: Math.sin(a) * size });
2749
+ }
2750
+ }
2751
+ return pts;
2752
+ }
2753
+
2754
+ function applySweep() {
2755
+ const profileShape = document.getElementById('sweep-profile').value;
2756
+ const profileSize = parseFloat(document.getElementById('sweep-size').value);
2757
+ const pathType = document.getElementById('sweep-path').value;
2758
+ const length = parseFloat(document.getElementById('sweep-length').value);
2759
+ const segments = parseInt(document.getElementById('sweep-segments').value);
2760
+ const twist = parseFloat(document.getElementById('sweep-twist').value);
2761
+
2762
+ const profile = generateProfilePoints(profileShape, profileSize);
2763
+ let path = [];
2764
+
2765
+ if (pathType === 'helix') {
2766
+ const turns = 3;
2767
+ const radius = length / 4;
2768
+ for (let i = 0; i <= segments; i++) {
2769
+ const t = i / segments;
2770
+ const a = t * Math.PI * 2 * turns;
2771
+ path.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius, z: t * length });
2772
+ }
2773
+ } else if (pathType === 'line') {
2774
+ path = [{ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: length }];
2775
+ } else if (pathType === 'arc') {
2776
+ const arcRadius = length / 2;
2777
+ for (let i = 0; i <= segments; i++) {
2778
+ const a = (i / segments) * Math.PI;
2779
+ path.push({ x: Math.cos(a) * arcRadius - arcRadius, y: 0, z: Math.sin(a) * arcRadius });
2780
+ }
2781
+ }
2782
+
2783
+ try {
2784
+ const mesh = createSweep(profile, path, { segments, twist });
2785
+ addToScene(mesh);
2786
+ const feature = { id: 'feature_' + Date.now(), name: `Sweep (${profileShape})`, type: 'sweep', mesh, params: { profileShape, profileSize, pathType, length, segments, twist } };
2787
+ APP.features.push(feature);
2788
+ addFeature(feature);
2789
+ pushHistory();
2790
+ updateStatus(`Created sweep: ${profileShape} profile along ${pathType} path`);
2791
+ closeDialog('sweep');
2792
+ } catch (err) { updateStatus('Sweep failed: ' + err.message); console.error(err); }
2793
+ }
2794
+
2795
+ function applyLoft() {
2796
+ const startShape = document.getElementById('loft-start').value;
2797
+ const startSize = parseFloat(document.getElementById('loft-start-size').value);
2798
+ const endShape = document.getElementById('loft-end').value;
2799
+ const endSize = parseFloat(document.getElementById('loft-end-size').value);
2800
+ const height = parseFloat(document.getElementById('loft-height').value);
2801
+ const segments = parseInt(document.getElementById('loft-segments').value);
2802
+
2803
+ const profiles = [
2804
+ { points: generateProfilePoints(startShape, startSize), position: { x: 0, y: 0, z: 0 } },
2805
+ { points: generateProfilePoints(endShape, endSize), position: { x: 0, y: 0, z: height } }
2806
+ ];
2807
+
2808
+ try {
2809
+ const mesh = createLoft(profiles, { segments });
2810
+ addToScene(mesh);
2811
+ const feature = { id: 'feature_' + Date.now(), name: `Loft (${startShape}→${endShape})`, type: 'loft', mesh, params: { startShape, startSize, endShape, endSize, height, segments } };
2812
+ APP.features.push(feature);
2813
+ addFeature(feature);
2814
+ pushHistory();
2815
+ updateStatus(`Created loft: ${startShape} to ${endShape}, H${height}mm`);
2816
+ closeDialog('loft');
2817
+ } catch (err) { updateStatus('Loft failed: ' + err.message); console.error(err); }
2818
+ }
2819
+
2820
+ function applyBend() {
2821
+ const angle = parseFloat(document.getElementById('bend-angle').value);
2822
+ const radius = parseFloat(document.getElementById('bend-radius').value);
2823
+ const kFactor = parseFloat(document.getElementById('bend-kfactor').value);
2824
+
2825
+ if (!APP.selectedFeature || !APP.selectedFeature.mesh) {
2826
+ updateStatus('Select a flat plate first, then apply bend');
2827
+ return;
2828
+ }
2829
+
2830
+ try {
2831
+ const bendLine = { start: { x: 0, y: 0, z: 0 }, end: { x: 0, y: 20, z: 0 } };
2832
+ const mesh = createBend(APP.selectedFeature.mesh, bendLine, angle, radius, { kFactor });
2833
+ addToScene(mesh);
2834
+ const feature = { id: 'feature_' + Date.now(), name: `Bend (${angle}°, R${radius})`, type: 'bend', mesh, params: { angle, radius, kFactor } };
2835
+ APP.features.push(feature);
2836
+ addFeature(feature);
2837
+ pushHistory();
2838
+ updateStatus(`Applied bend: ${angle}° with R${radius}mm inner radius`);
2839
+ closeDialog('bend');
2840
+ } catch (err) { updateStatus('Bend failed: ' + err.message); console.error(err); }
2841
+ }
2842
+
2843
+ function applyFlange() {
2844
+ const length = parseFloat(document.getElementById('flange-length').value);
2845
+ const angle = parseFloat(document.getElementById('flange-angle').value);
2846
+
2847
+ if (!APP.selectedFeature || !APP.selectedFeature.mesh) {
2848
+ updateStatus('Select a sheet metal part first, then apply flange');
2849
+ return;
2850
+ }
2851
+
2852
+ try {
2853
+ const edge = { start: { x: -10, y: 0, z: 0 }, end: { x: 10, y: 0, z: 0 } };
2854
+ const mesh = createFlange(APP.selectedFeature.mesh, edge, length, angle);
2855
+ addToScene(mesh);
2856
+ const feature = { id: 'feature_' + Date.now(), name: `Flange (L${length}, ${angle}°)`, type: 'flange', mesh, params: { length, angle } };
2857
+ APP.features.push(feature);
2858
+ addFeature(feature);
2859
+ pushHistory();
2860
+ updateStatus(`Applied flange: L${length}mm at ${angle}°`);
2861
+ closeDialog('flange');
2862
+ } catch (err) { updateStatus('Flange failed: ' + err.message); console.error(err); }
2863
+ }
2864
+
2865
+ // Close dialog when backdrop is clicked
2866
+ document.getElementById('dialog-backdrop').addEventListener('click', () => {
2867
+ if (currentDialogId) closeDialog(currentDialogId);
2868
+ });
2869
+
2870
+ // Close dialog on Escape key
2871
+ document.addEventListener('keydown', (e) => {
2872
+ if (e.key === 'Escape' && currentDialogId) {
2873
+ closeDialog(currentDialogId);
2874
+ }
2875
+ });
2876
+
1633
2877
  </script>
2878
+
2879
+ <!-- Operation Dialogs -->
2880
+ <div class="dialog-backdrop" id="dialog-backdrop"></div>
2881
+
2882
+ <!-- Revolve Dialog -->
2883
+ <div class="operation-dialog" id="dialog-revolve">
2884
+ <div class="dialog-header">
2885
+ <div class="dialog-title">Revolve Profile</div>
2886
+ <div class="dialog-close-btn" onclick="closeDialog('revolve')">✕</div>
2887
+ </div>
2888
+ <div class="dialog-content">
2889
+ <div class="dialog-form-group">
2890
+ <label class="dialog-label">Axis</label>
2891
+ <select class="dialog-select" id="revolve-axis">
2892
+ <option value="z">Z Axis</option>
2893
+ <option value="x">X Axis</option>
2894
+ <option value="y">Y Axis</option>
2895
+ </select>
2896
+ </div>
2897
+ <div class="dialog-form-group">
2898
+ <label class="dialog-label">Angle: <span id="revolve-angle-value">360</span>°</label>
2899
+ <input type="range" class="dialog-input" id="revolve-angle" min="1" max="360" value="360" oninput="document.getElementById('revolve-angle-value').textContent = this.value">
2900
+ </div>
2901
+ <div class="dialog-form-group">
2902
+ <label class="dialog-label">Direction</label>
2903
+ <div class="dialog-radio-group">
2904
+ <div class="dialog-radio-item">
2905
+ <input type="radio" id="revolve-dir-ccw" name="revolve-direction" value="ccw" checked>
2906
+ <label for="revolve-dir-ccw">Counter-Clockwise</label>
2907
+ </div>
2908
+ <div class="dialog-radio-item">
2909
+ <input type="radio" id="revolve-dir-cw" name="revolve-direction" value="cw">
2910
+ <label for="revolve-dir-cw">Clockwise</label>
2911
+ </div>
2912
+ </div>
2913
+ </div>
2914
+ </div>
2915
+ <div class="dialog-footer">
2916
+ <button class="dialog-button secondary" onclick="closeDialog('revolve')">Cancel</button>
2917
+ <button class="dialog-button primary" onclick="applyRevolve()">OK</button>
2918
+ </div>
2919
+ </div>
2920
+
2921
+ <!-- Fillet Dialog -->
2922
+ <div class="operation-dialog" id="dialog-fillet">
2923
+ <div class="dialog-header">
2924
+ <div class="dialog-title">Fillet Edges</div>
2925
+ <div class="dialog-close-btn" onclick="closeDialog('fillet')">✕</div>
2926
+ </div>
2927
+ <div class="dialog-content">
2928
+ <div class="dialog-form-group">
2929
+ <label class="dialog-label">Radius (mm)</label>
2930
+ <input type="number" class="dialog-input" id="fillet-radius" value="3" min="0.1" step="0.1">
2931
+ </div>
2932
+ <div class="dialog-form-group">
2933
+ <div class="dialog-checkbox">
2934
+ <input type="checkbox" id="fillet-preview" checked>
2935
+ <label for="fillet-preview">Preview</label>
2936
+ </div>
2937
+ </div>
2938
+ </div>
2939
+ <div class="dialog-footer">
2940
+ <button class="dialog-button secondary" onclick="closeDialog('fillet')">Cancel</button>
2941
+ <button class="dialog-button primary" onclick="applyFillet()">OK</button>
2942
+ </div>
2943
+ </div>
2944
+
2945
+ <!-- Chamfer Dialog -->
2946
+ <div class="operation-dialog" id="dialog-chamfer">
2947
+ <div class="dialog-header">
2948
+ <div class="dialog-title">Chamfer Edges</div>
2949
+ <div class="dialog-close-btn" onclick="closeDialog('chamfer')">✕</div>
2950
+ </div>
2951
+ <div class="dialog-content">
2952
+ <div class="dialog-form-group">
2953
+ <label class="dialog-label">Distance (mm)</label>
2954
+ <input type="number" class="dialog-input" id="chamfer-distance" value="2" min="0.1" step="0.1">
2955
+ </div>
2956
+ </div>
2957
+ <div class="dialog-footer">
2958
+ <button class="dialog-button secondary" onclick="closeDialog('chamfer')">Cancel</button>
2959
+ <button class="dialog-button primary" onclick="applyChamer()">OK</button>
2960
+ </div>
2961
+ </div>
2962
+
2963
+ <!-- Boolean Dialog -->
2964
+ <div class="operation-dialog" id="dialog-boolean">
2965
+ <div class="dialog-header">
2966
+ <div class="dialog-title">Boolean Operation</div>
2967
+ <div class="dialog-close-btn" onclick="closeDialog('boolean')">✕</div>
2968
+ </div>
2969
+ <div class="dialog-content">
2970
+ <div class="dialog-form-group">
2971
+ <label class="dialog-label">Operation</label>
2972
+ <div class="dialog-radio-group">
2973
+ <div class="dialog-radio-item">
2974
+ <input type="radio" id="bool-union" name="boolean-op" value="union" checked>
2975
+ <label for="bool-union">Union (Combine)</label>
2976
+ </div>
2977
+ <div class="dialog-radio-item">
2978
+ <input type="radio" id="bool-cut" name="boolean-op" value="cut">
2979
+ <label for="bool-cut">Cut (Subtract)</label>
2980
+ </div>
2981
+ <div class="dialog-radio-item">
2982
+ <input type="radio" id="bool-intersect" name="boolean-op" value="intersect">
2983
+ <label for="bool-intersect">Intersect (Common)</label>
2984
+ </div>
2985
+ </div>
2986
+ </div>
2987
+ <div class="dialog-form-group">
2988
+ <p style="font-size: 11px; color: var(--text-secondary); line-height: 1.6;">
2989
+ <strong>Instructions:</strong> Select the first body, then click OK. Select the second body, then click OK again.
2990
+ </p>
2991
+ </div>
2992
+ </div>
2993
+ <div class="dialog-footer">
2994
+ <button class="dialog-button secondary" onclick="closeDialog('boolean')">Cancel</button>
2995
+ <button class="dialog-button primary" onclick="applyBoolean()">OK</button>
2996
+ </div>
2997
+ </div>
2998
+
2999
+ <!-- Shell Dialog -->
3000
+ <div class="operation-dialog" id="dialog-shell">
3001
+ <div class="dialog-header">
3002
+ <div class="dialog-title">Shell (Hollow)</div>
3003
+ <div class="dialog-close-btn" onclick="closeDialog('shell')">✕</div>
3004
+ </div>
3005
+ <div class="dialog-content">
3006
+ <div class="dialog-form-group">
3007
+ <label class="dialog-label">Wall Thickness (mm)</label>
3008
+ <input type="number" class="dialog-input" id="shell-thickness" value="2" min="0.1" step="0.1">
3009
+ </div>
3010
+ </div>
3011
+ <div class="dialog-footer">
3012
+ <button class="dialog-button secondary" onclick="closeDialog('shell')">Cancel</button>
3013
+ <button class="dialog-button primary" onclick="applyShell()">OK</button>
3014
+ </div>
3015
+ </div>
3016
+
3017
+ <!-- Pattern Dialog -->
3018
+ <div class="operation-dialog" id="dialog-pattern">
3019
+ <div class="dialog-header">
3020
+ <div class="dialog-title">Pattern Features</div>
3021
+ <div class="dialog-close-btn" onclick="closeDialog('pattern')">✕</div>
3022
+ </div>
3023
+ <div class="dialog-content">
3024
+ <div class="dialog-form-group">
3025
+ <label class="dialog-label">Pattern Type</label>
3026
+ <div class="dialog-radio-group">
3027
+ <div class="dialog-radio-item">
3028
+ <input type="radio" id="pattern-rect" name="pattern-type" value="rectangular" checked onchange="updatePatternUI()">
3029
+ <label for="pattern-rect">Rectangular</label>
3030
+ </div>
3031
+ <div class="dialog-radio-item">
3032
+ <input type="radio" id="pattern-circ" name="pattern-type" value="circular" onchange="updatePatternUI()">
3033
+ <label for="pattern-circ">Circular</label>
3034
+ </div>
3035
+ </div>
3036
+ </div>
3037
+
3038
+ <!-- Rectangular Pattern Fields -->
3039
+ <div id="pattern-rectangular">
3040
+ <div class="dialog-form-group">
3041
+ <label class="dialog-label">Count X</label>
3042
+ <input type="number" class="dialog-input" id="pattern-count-x" value="3" min="1" step="1">
3043
+ </div>
3044
+ <div class="dialog-form-group">
3045
+ <label class="dialog-label">Count Y</label>
3046
+ <input type="number" class="dialog-input" id="pattern-count-y" value="3" min="1" step="1">
3047
+ </div>
3048
+ <div class="dialog-form-group">
3049
+ <label class="dialog-label">Spacing X (mm)</label>
3050
+ <input type="number" class="dialog-input" id="pattern-spacing-x" value="10" min="0.1" step="0.1">
3051
+ </div>
3052
+ <div class="dialog-form-group">
3053
+ <label class="dialog-label">Spacing Y (mm)</label>
3054
+ <input type="number" class="dialog-input" id="pattern-spacing-y" value="10" min="0.1" step="0.1">
3055
+ </div>
3056
+ </div>
3057
+
3058
+ <!-- Circular Pattern Fields -->
3059
+ <div id="pattern-circular" style="display:none;">
3060
+ <div class="dialog-form-group">
3061
+ <label class="dialog-label">Count</label>
3062
+ <input type="number" class="dialog-input" id="pattern-count" value="6" min="1" step="1">
3063
+ </div>
3064
+ <div class="dialog-form-group">
3065
+ <label class="dialog-label">Radius (mm)</label>
3066
+ <input type="number" class="dialog-input" id="pattern-radius" value="20" min="0.1" step="0.1">
3067
+ </div>
3068
+ <div class="dialog-form-group">
3069
+ <label class="dialog-label">Axis</label>
3070
+ <select class="dialog-select" id="pattern-axis">
3071
+ <option value="z">Z Axis</option>
3072
+ <option value="x">X Axis</option>
3073
+ <option value="y">Y Axis</option>
3074
+ </select>
3075
+ </div>
3076
+ </div>
3077
+ </div>
3078
+ <div class="dialog-footer">
3079
+ <button class="dialog-button secondary" onclick="closeDialog('pattern')">Cancel</button>
3080
+ <button class="dialog-button primary" onclick="applyPattern()">OK</button>
3081
+ </div>
3082
+ </div>
3083
+
3084
+ <!-- Sweep Dialog -->
3085
+ <div class="operation-dialog" id="dialog-sweep">
3086
+ <div class="dialog-header">
3087
+ <div class="dialog-title">Sweep (Profile Along Path)</div>
3088
+ <div class="dialog-close-btn" onclick="closeDialog('sweep')">&#10005;</div>
3089
+ </div>
3090
+ <div class="dialog-content">
3091
+ <div class="dialog-form-group">
3092
+ <label class="dialog-label">Profile Shape</label>
3093
+ <select class="dialog-select" id="sweep-profile">
3094
+ <option value="circle">Circle</option>
3095
+ <option value="rectangle">Rectangle</option>
3096
+ <option value="sketch">From Sketch</option>
3097
+ </select>
3098
+ </div>
3099
+ <div class="dialog-form-group">
3100
+ <label class="dialog-label">Profile Radius / Width (mm)</label>
3101
+ <input type="number" class="dialog-input" id="sweep-size" value="3" min="0.1" step="0.1">
3102
+ </div>
3103
+ <div class="dialog-form-group">
3104
+ <label class="dialog-label">Path Shape</label>
3105
+ <select class="dialog-select" id="sweep-path">
3106
+ <option value="helix">Helix</option>
3107
+ <option value="line">Straight Line</option>
3108
+ <option value="arc">Arc</option>
3109
+ </select>
3110
+ </div>
3111
+ <div class="dialog-form-group">
3112
+ <label class="dialog-label">Path Length (mm)</label>
3113
+ <input type="number" class="dialog-input" id="sweep-length" value="40" min="1" step="1">
3114
+ </div>
3115
+ <div class="dialog-form-group">
3116
+ <label class="dialog-label">Segments</label>
3117
+ <input type="range" class="dialog-range" id="sweep-segments" min="8" max="128" value="64">
3118
+ </div>
3119
+ <div class="dialog-form-group">
3120
+ <label class="dialog-label">Twist (deg/mm)</label>
3121
+ <input type="number" class="dialog-input" id="sweep-twist" value="0" step="0.1">
3122
+ </div>
3123
+ </div>
3124
+ <div class="dialog-footer">
3125
+ <button class="dialog-button secondary" onclick="closeDialog('sweep')">Cancel</button>
3126
+ <button class="dialog-button primary" onclick="applySweep()">OK</button>
3127
+ </div>
3128
+ </div>
3129
+
3130
+ <!-- Loft Dialog -->
3131
+ <div class="operation-dialog" id="dialog-loft">
3132
+ <div class="dialog-header">
3133
+ <div class="dialog-title">Loft (Between Profiles)</div>
3134
+ <div class="dialog-close-btn" onclick="closeDialog('loft')">&#10005;</div>
3135
+ </div>
3136
+ <div class="dialog-content">
3137
+ <div class="dialog-form-group">
3138
+ <label class="dialog-label">Start Profile</label>
3139
+ <select class="dialog-select" id="loft-start">
3140
+ <option value="circle">Circle</option>
3141
+ <option value="rectangle">Rectangle</option>
3142
+ <option value="hexagon">Hexagon</option>
3143
+ </select>
3144
+ </div>
3145
+ <div class="dialog-form-group">
3146
+ <label class="dialog-label">Start Size (mm)</label>
3147
+ <input type="number" class="dialog-input" id="loft-start-size" value="10" min="0.1" step="0.1">
3148
+ </div>
3149
+ <div class="dialog-form-group">
3150
+ <label class="dialog-label">End Profile</label>
3151
+ <select class="dialog-select" id="loft-end">
3152
+ <option value="circle">Circle</option>
3153
+ <option value="rectangle">Rectangle</option>
3154
+ <option value="hexagon">Hexagon</option>
3155
+ </select>
3156
+ </div>
3157
+ <div class="dialog-form-group">
3158
+ <label class="dialog-label">End Size (mm)</label>
3159
+ <input type="number" class="dialog-input" id="loft-end-size" value="5" min="0.1" step="0.1">
3160
+ </div>
3161
+ <div class="dialog-form-group">
3162
+ <label class="dialog-label">Height (mm)</label>
3163
+ <input type="number" class="dialog-input" id="loft-height" value="30" min="1" step="1">
3164
+ </div>
3165
+ <div class="dialog-form-group">
3166
+ <label class="dialog-label">Segments</label>
3167
+ <input type="range" class="dialog-range" id="loft-segments" min="4" max="64" value="32">
3168
+ </div>
3169
+ </div>
3170
+ <div class="dialog-footer">
3171
+ <button class="dialog-button secondary" onclick="closeDialog('loft')">Cancel</button>
3172
+ <button class="dialog-button primary" onclick="applyLoft()">OK</button>
3173
+ </div>
3174
+ </div>
3175
+
3176
+ <!-- Bend Dialog -->
3177
+ <div class="operation-dialog" id="dialog-bend">
3178
+ <div class="dialog-header">
3179
+ <div class="dialog-title">Sheet Metal Bend</div>
3180
+ <div class="dialog-close-btn" onclick="closeDialog('bend')">&#10005;</div>
3181
+ </div>
3182
+ <div class="dialog-content">
3183
+ <div class="dialog-form-group">
3184
+ <label class="dialog-label">Bend Angle (degrees)</label>
3185
+ <input type="number" class="dialog-input" id="bend-angle" value="90" min="1" max="180" step="1">
3186
+ </div>
3187
+ <div class="dialog-form-group">
3188
+ <label class="dialog-label">Inner Bend Radius (mm)</label>
3189
+ <input type="number" class="dialog-input" id="bend-radius" value="2" min="0.1" step="0.1">
3190
+ </div>
3191
+ <div class="dialog-form-group">
3192
+ <label class="dialog-label">K-Factor</label>
3193
+ <input type="number" class="dialog-input" id="bend-kfactor" value="0.44" min="0.1" max="0.9" step="0.01">
3194
+ </div>
3195
+ <p style="font-size:10px;color:var(--text-muted);margin-top:4px;">Select a flat plate first, then define the bend line by clicking two points.</p>
3196
+ </div>
3197
+ <div class="dialog-footer">
3198
+ <button class="dialog-button secondary" onclick="closeDialog('bend')">Cancel</button>
3199
+ <button class="dialog-button primary" onclick="applyBend()">OK</button>
3200
+ </div>
3201
+ </div>
3202
+
3203
+ <!-- Flange Dialog -->
3204
+ <div class="operation-dialog" id="dialog-flange">
3205
+ <div class="dialog-header">
3206
+ <div class="dialog-title">Sheet Metal Flange</div>
3207
+ <div class="dialog-close-btn" onclick="closeDialog('flange')">&#10005;</div>
3208
+ </div>
3209
+ <div class="dialog-content">
3210
+ <div class="dialog-form-group">
3211
+ <label class="dialog-label">Flange Length (mm)</label>
3212
+ <input type="number" class="dialog-input" id="flange-length" value="15" min="0.1" step="0.1">
3213
+ </div>
3214
+ <div class="dialog-form-group">
3215
+ <label class="dialog-label">Flange Angle (degrees)</label>
3216
+ <input type="number" class="dialog-input" id="flange-angle" value="90" min="1" max="180" step="1">
3217
+ </div>
3218
+ <p style="font-size:10px;color:var(--text-muted);margin-top:4px;">Select an edge on a sheet metal part, then apply the flange.</p>
3219
+ </div>
3220
+ <div class="dialog-footer">
3221
+ <button class="dialog-button secondary" onclick="closeDialog('flange')">Cancel</button>
3222
+ <button class="dialog-button primary" onclick="applyFlange()">OK</button>
3223
+ </div>
3224
+ </div>
3225
+
1634
3226
  </body>
1635
3227
  </html>