cyclecad 0.1.3 → 0.1.4

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>
@@ -1091,11 +1421,26 @@
1091
1421
 
1092
1422
  <!-- Main Content Area -->
1093
1423
  <div id="content">
1094
- <!-- Left Panel: Feature Tree -->
1424
+ <!-- Left Panel: Feature Tree + Project Browser -->
1095
1425
  <div id="left-panel">
1096
- <div id="left-panel-header">Model Tree</div>
1097
- <div id="feature-tree">
1098
- <!-- Populated by JavaScript -->
1426
+ <div id="left-panel-tabs">
1427
+ <button class="left-tab active" data-left-tab="tree">Model Tree</button>
1428
+ <button class="left-tab" data-left-tab="browser">Project Browser</button>
1429
+ </div>
1430
+ <div id="left-tab-tree" class="left-tab-content" style="display:flex;flex-direction:column;flex:1;min-height:0;">
1431
+ <div id="feature-tree">
1432
+ <!-- Populated by JavaScript -->
1433
+ </div>
1434
+ </div>
1435
+ <div id="left-tab-browser" class="left-tab-content" style="display:none;flex-direction:column;flex:1;min-height:0;">
1436
+ <div id="inline-project-browser">
1437
+ <!-- Inline project browser tree (populated when DUO manifest loads) -->
1438
+ <div class="ipb-search-box">
1439
+ <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;">
1440
+ </div>
1441
+ <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>
1442
+ <div id="ipb-tree" style="flex:1;overflow-y:auto;padding:4px 0;min-height:0;"></div>
1443
+ </div>
1099
1444
  </div>
1100
1445
  </div>
1101
1446
 
@@ -1133,6 +1478,7 @@
1133
1478
  <div id="properties-tabs">
1134
1479
  <button class="properties-tab active" data-tab="properties">Properties</button>
1135
1480
  <button class="properties-tab" data-tab="chat">Chat</button>
1481
+ <button class="properties-tab" data-tab="guide">Guide</button>
1136
1482
  </div>
1137
1483
 
1138
1484
  <!-- Properties Content -->
@@ -1143,12 +1489,18 @@
1143
1489
  <div id="tab-chat" style="display: none;">
1144
1490
  <!-- Chat tab populated by JavaScript -->
1145
1491
  </div>
1492
+ <div id="tab-guide" style="display: none;">
1493
+ <!-- Rebuild guide populated by JavaScript -->
1494
+ </div>
1146
1495
  </div>
1147
1496
  </div>
1148
1497
  </div>
1149
1498
 
1150
1499
  <!-- Bottom Status Bar -->
1151
1500
  <div id="statusbar">
1501
+ <div class="statusbar-item" id="status-bar-item">
1502
+ <span id="status-bar">Ready</span>
1503
+ </div>
1152
1504
  <div class="statusbar-item">
1153
1505
  <span class="status-indicator" id="kernel-status"></span>
1154
1506
  <span class="statusbar-label">Kernel:</span>
@@ -1183,6 +1535,7 @@
1183
1535
 
1184
1536
  <!-- Module Loader -->
1185
1537
  <script type="module">
1538
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1186
1539
  import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject } from './js/viewport.js';
1187
1540
  import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js';
1188
1541
  import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js';
@@ -1193,6 +1546,9 @@
1193
1546
  import { initShortcuts } from './js/shortcuts.js';
1194
1547
  import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js';
1195
1548
  import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js';
1549
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js';
1550
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js';
1551
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js';
1196
1552
 
1197
1553
  // ========== Application State ==========
1198
1554
  const APP = {
@@ -1202,6 +1558,7 @@
1202
1558
  features: [],
1203
1559
  history: [],
1204
1560
  historyIndex: -1,
1561
+ project: null, // Current Inventor project
1205
1562
  };
1206
1563
 
1207
1564
  // ========== Initialization ==========
@@ -1272,8 +1629,8 @@
1272
1629
  circle: () => setTool('circle'),
1273
1630
  arc: () => setTool('arc'),
1274
1631
  extrude: () => doExtrude(),
1275
- undo: () => { /* TODO */ },
1276
- redo: () => { /* TODO */ },
1632
+ undo: () => undo(),
1633
+ redo: () => redo(),
1277
1634
  delete: () => deleteSelected(),
1278
1635
  escape: () => cancelOperation(),
1279
1636
  enter: () => confirmOperation(),
@@ -1285,7 +1642,7 @@
1285
1642
  viewBottom: () => setView('bottom'),
1286
1643
  viewIso: () => setView('iso'),
1287
1644
  toggleGrid: () => vpToggleGrid(),
1288
- fitAll: () => { /* TODO */ },
1645
+ fitAll: () => fitAll(),
1289
1646
  save: () => saveProject(),
1290
1647
  exportSTL: () => doExportSTL(),
1291
1648
  });
@@ -1293,7 +1650,46 @@
1293
1650
  // 9. Setup welcome splash
1294
1651
  setupWelcome();
1295
1652
 
1296
- // 10. Hard Refresh button nukes all caches, service workers, and reloads
1653
+ // 9b. Setup left panel tabs and inline browser
1654
+ setupLeftTabs();
1655
+ setupInlineBrowserClicks();
1656
+
1657
+ // 10. Initialize project browser
1658
+ initProjectBrowser(document.body, {
1659
+ onFileOpen: async (file) => {
1660
+ try {
1661
+ const buffer = file.buffer || await file.arrayBuffer();
1662
+ const result = parseInventorFile(new Uint8Array(buffer));
1663
+ // Add features to tree
1664
+ if (result.features) {
1665
+ result.features.forEach((f, i) => {
1666
+ addFeature({
1667
+ id: `inv-${Date.now()}-${i}`,
1668
+ type: f.type || 'Unknown',
1669
+ name: f.name || `Feature ${i+1}`,
1670
+ params: f.parameters || {},
1671
+ icon: f.icon || '📦'
1672
+ });
1673
+ });
1674
+ }
1675
+ // Generate rebuild guide
1676
+ const guide = generateGuide(result);
1677
+ // Show guide in right panel
1678
+ const propsContent = document.getElementById('tab-guide');
1679
+ if (propsContent) renderGuide(propsContent, guide);
1680
+ updateStatus(`Opened: ${result.metadata?.fileName || file.name} — ${result.features?.length || 0} features`);
1681
+ } catch (err) {
1682
+ console.error('Failed to open file:', err);
1683
+ updateStatus('Failed to open file: ' + err.message);
1684
+ }
1685
+ },
1686
+ onProjectLoad: (project) => {
1687
+ APP.project = project;
1688
+ updateStatus(`Project loaded: ${project.stats.parts} parts, ${project.stats.assemblies} assemblies`);
1689
+ }
1690
+ });
1691
+
1692
+ // 11. Hard Refresh button — nukes all caches, service workers, and reloads
1297
1693
  const hardRefreshBtn = document.getElementById('btn-hard-refresh');
1298
1694
  if (hardRefreshBtn) hardRefreshBtn.addEventListener('click', async () => {
1299
1695
  hardRefreshBtn.textContent = 'Clearing...';
@@ -1324,6 +1720,217 @@
1324
1720
  }
1325
1721
  }
1326
1722
 
1723
+ // ========== Left Panel Tab Switching ==========
1724
+ function switchLeftTab(tab) {
1725
+ document.querySelectorAll('.left-tab').forEach(t => {
1726
+ t.classList.toggle('active', t.dataset.leftTab === tab);
1727
+ });
1728
+ document.getElementById('left-tab-tree').style.display = tab === 'tree' ? 'flex' : 'none';
1729
+ document.getElementById('left-tab-browser').style.display = tab === 'browser' ? 'flex' : 'none';
1730
+ }
1731
+
1732
+ function setupLeftTabs() {
1733
+ document.querySelectorAll('.left-tab').forEach(tab => {
1734
+ tab.addEventListener('click', () => switchLeftTab(tab.dataset.leftTab));
1735
+ });
1736
+ }
1737
+
1738
+ // ========== Inline Project Browser (Left Panel) ==========
1739
+ let ipbState = { tree: null, expanded: new Set(), searchQuery: '', selectedPath: null };
1740
+
1741
+ function populateInlineBrowser(treeData) {
1742
+ ipbState.tree = treeData;
1743
+ ipbState.expanded.clear();
1744
+ ipbState.searchQuery = '';
1745
+
1746
+ // Auto-expand the first level
1747
+ if (treeData.children) {
1748
+ treeData.children.forEach((child, i) => {
1749
+ if (child.type === 'folder') ipbState.expanded.add('root-' + i);
1750
+ });
1751
+ }
1752
+
1753
+ renderInlineBrowser();
1754
+ updateInlineStats();
1755
+
1756
+ // Wire search
1757
+ const searchEl = document.getElementById('ipb-search');
1758
+ if (searchEl) {
1759
+ let timeout;
1760
+ searchEl.addEventListener('input', (e) => {
1761
+ clearTimeout(timeout);
1762
+ timeout = setTimeout(() => {
1763
+ ipbState.searchQuery = e.target.value.toLowerCase();
1764
+ renderInlineBrowser();
1765
+ }, 200);
1766
+ });
1767
+ }
1768
+ }
1769
+
1770
+ function renderInlineBrowser() {
1771
+ const container = document.getElementById('ipb-tree');
1772
+ if (!container || !ipbState.tree) return;
1773
+ container.innerHTML = renderIPBItems(ipbState.tree.children || [], 'root');
1774
+ }
1775
+
1776
+ function renderIPBItems(items, prefix) {
1777
+ if (!items || items.length === 0) return '';
1778
+ let html = '';
1779
+
1780
+ for (let i = 0; i < items.length; i++) {
1781
+ const item = items[i];
1782
+ const nodeId = prefix + '-' + i;
1783
+
1784
+ // Search filter
1785
+ if (ipbState.searchQuery && item.type !== 'folder') {
1786
+ if (!item.name.toLowerCase().includes(ipbState.searchQuery)) continue;
1787
+ }
1788
+
1789
+ // If folder, check if any children match search
1790
+ if (ipbState.searchQuery && item.type === 'folder') {
1791
+ if (!folderHasMatch(item, ipbState.searchQuery)) continue;
1792
+ }
1793
+
1794
+ const icon = getIPBIcon(item);
1795
+ const badge = getIPBBadge(item.category);
1796
+
1797
+ if (item.type === 'folder' && item.children && item.children.length > 0) {
1798
+ const isExp = ipbState.expanded.has(nodeId);
1799
+ html += `<div class="ipb-folder" data-ipb-id="${nodeId}" data-ipb-type="folder">
1800
+ <span class="ipb-toggle ${isExp ? 'expanded' : ''}">&#9654;</span>
1801
+ <span class="ipb-icon">${icon}</span>
1802
+ <span class="ipb-label">${escHTML(item.name)}</span>
1803
+ <span style="margin-left:auto;font-size:9px;color:var(--text-muted);">${item.children.length}</span>
1804
+ </div>`;
1805
+ if (isExp) {
1806
+ html += `<div class="ipb-children">${renderIPBItems(item.children, nodeId)}</div>`;
1807
+ }
1808
+ } else if (item.type !== 'folder') {
1809
+ const sel = ipbState.selectedPath === item.path ? ' selected' : '';
1810
+ 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)}">
1811
+ <span class="ipb-toggle" style="visibility:hidden;">&#8226;</span>
1812
+ <span class="ipb-icon">${icon}</span>
1813
+ <span class="ipb-label">${escHTML(item.name)}</span>
1814
+ ${badge}
1815
+ </div>`;
1816
+ }
1817
+ }
1818
+ return html;
1819
+ }
1820
+
1821
+ function folderHasMatch(folder, query) {
1822
+ if (!folder.children) return false;
1823
+ return folder.children.some(child => {
1824
+ if (child.type === 'folder') return folderHasMatch(child, query);
1825
+ return child.name.toLowerCase().includes(query);
1826
+ });
1827
+ }
1828
+
1829
+ function getIPBIcon(item) {
1830
+ const icons = { ipt: '&#128230;', iam: '&#127959;', idw: '&#128208;', ipj: '&#128203;', folder: '&#128193;' };
1831
+ return icons[item.type] || '&#128196;';
1832
+ }
1833
+
1834
+ function getIPBBadge(category) {
1835
+ if (!category) return '';
1836
+ const map = {
1837
+ custom: '<span class="ipb-badge ipb-badge-green">CUSTOM</span>',
1838
+ standard: '<span class="ipb-badge ipb-badge-blue">STD</span>',
1839
+ vendor: '<span class="ipb-badge ipb-badge-yellow">VENDOR</span>',
1840
+ };
1841
+ return map[category] || '';
1842
+ }
1843
+
1844
+ function escHTML(s) {
1845
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1846
+ }
1847
+
1848
+ function updateInlineStats() {
1849
+ const el = document.getElementById('ipb-stats');
1850
+ if (!el || !APP.project) return;
1851
+ const s = APP.project.stats || {};
1852
+ el.innerHTML = `<span>&#128230; ${s.parts || 0} parts</span><span>&#127959; ${s.assemblies || 0} asms</span><span>&#128196; ${s.total || 0} total</span>`;
1853
+ }
1854
+
1855
+ // Delegated click handler for inline browser
1856
+ function setupInlineBrowserClicks() {
1857
+ const container = document.getElementById('ipb-tree');
1858
+ if (!container) return;
1859
+ container.addEventListener('click', (e) => {
1860
+ const row = e.target.closest('[data-ipb-id]');
1861
+ if (!row) return;
1862
+ const nodeId = row.dataset.ipbId;
1863
+ const nodeType = row.dataset.ipbType;
1864
+
1865
+ if (nodeType === 'folder') {
1866
+ // Toggle expand
1867
+ if (ipbState.expanded.has(nodeId)) {
1868
+ ipbState.expanded.delete(nodeId);
1869
+ } else {
1870
+ ipbState.expanded.add(nodeId);
1871
+ }
1872
+ renderInlineBrowser();
1873
+ } else if (['ipt', 'iam'].includes(nodeType)) {
1874
+ // Select file → show info in guide tab
1875
+ ipbState.selectedPath = row.dataset.ipbPath;
1876
+ renderInlineBrowser();
1877
+ handleFileSelect(row.dataset.ipbName, row.dataset.ipbPath, nodeType);
1878
+ }
1879
+ });
1880
+ }
1881
+
1882
+ function handleFileSelect(name, path, type) {
1883
+ updateStatus(`Selected: ${name}`);
1884
+
1885
+ // Switch right panel to Guide tab and show file info
1886
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
1887
+ const guideTabBtn = document.querySelector('[data-tab="guide"]');
1888
+ if (guideTabBtn) guideTabBtn.classList.add('active');
1889
+ document.getElementById('tab-properties').style.display = 'none';
1890
+ document.getElementById('tab-chat').style.display = 'none';
1891
+ document.getElementById('tab-guide').style.display = 'block';
1892
+
1893
+ const guideContainer = document.getElementById('tab-guide');
1894
+ if (!guideContainer) return;
1895
+
1896
+ // Show file info + feature guide placeholder
1897
+ const ext = type.toUpperCase();
1898
+ const category = getFileCategoryFromPath(path);
1899
+ guideContainer.innerHTML = `
1900
+ <div style="padding:12px;display:flex;flex-direction:column;gap:12px;">
1901
+ <div style="font-weight:600;font-size:13px;color:var(--accent-blue);">${escHTML(name)}</div>
1902
+ <div style="font-size:11px;color:var(--text-secondary);">
1903
+ <div><strong>Type:</strong> Inventor ${ext} ${type === 'ipt' ? '(Part)' : '(Assembly)'}</div>
1904
+ <div><strong>Path:</strong> ${escHTML(path)}</div>
1905
+ <div><strong>Category:</strong> ${category}</div>
1906
+ </div>
1907
+ <div style="border-top:1px solid var(--border-color);padding-top:12px;">
1908
+ <div style="font-weight:600;font-size:12px;margin-bottom:8px;">Rebuild Guide</div>
1909
+ <p style="font-size:11px;color:var(--text-secondary);line-height:1.6;">
1910
+ To generate a detailed rebuild guide, import this file using the
1911
+ <strong>Import Inventor</strong> button in the toolbar. The parser will extract
1912
+ features, dimensions, and constraints, then generate step-by-step
1913
+ instructions for recreating the part in cycleCAD or Fusion 360.
1914
+ </p>
1915
+ </div>
1916
+ <div style="border-top:1px solid var(--border-color);padding-top:12px;">
1917
+ <div style="font-weight:600;font-size:12px;margin-bottom:8px;">Quick Actions</div>
1918
+ <div style="display:flex;flex-direction:column;gap:6px;">
1919
+ <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>
1920
+ </div>
1921
+ </div>
1922
+ </div>
1923
+ `;
1924
+ }
1925
+
1926
+ function getFileCategoryFromPath(path) {
1927
+ if (!path) return 'Unknown';
1928
+ const lp = path.toLowerCase();
1929
+ if (lp.includes('content center') || lp.includes('libraries')) return 'Standard (DIN/ISO)';
1930
+ if (lp.includes('zukaufteile') || lp.includes('igus') || lp.includes('interroll') || lp.includes('rittal')) return 'Vendor / Buy-out';
1931
+ return 'Custom';
1932
+ }
1933
+
1327
1934
  // ========== Toolbar Wiring ==========
1328
1935
  function setupToolbar() {
1329
1936
  const bind = (id, fn) => {
@@ -1341,26 +1948,26 @@
1341
1948
 
1342
1949
  // 3D operations
1343
1950
  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'));
1951
+ bind('tool-revolve', () => openDialog('revolve'));
1952
+ bind('tool-fillet', () => openDialog('fillet'));
1953
+ bind('tool-chamfer', () => openDialog('chamfer'));
1954
+ bind('tool-cut', () => openDialog('boolean'));
1955
+ bind('tool-union', () => { document.getElementById('bool-union').checked = true; openDialog('boolean'); });
1349
1956
 
1350
1957
  // Export
1351
1958
  bind('export-stl', () => doExportSTL());
1352
1959
  bind('export-step', () => updateStatus('STEP export: coming soon'));
1353
1960
 
1354
1961
  // Edit
1355
- bind('btn-undo', () => updateStatus('Undo: coming soon'));
1356
- bind('btn-redo', () => updateStatus('Redo: coming soon'));
1962
+ bind('btn-undo', () => { undo(); });
1963
+ bind('btn-redo', () => { redo(); });
1357
1964
 
1358
1965
  // Views
1359
1966
  bind('view-front', () => setView('front'));
1360
1967
  bind('view-top', () => setView('top'));
1361
1968
  bind('view-right', () => setView('right'));
1362
1969
  bind('view-iso', () => setView('iso'));
1363
- bind('view-fit', () => updateStatus('Fit All'));
1970
+ bind('view-fit', () => fitAll());
1364
1971
 
1365
1972
  // Reverse Engineer
1366
1973
  bind('btn-reverse-engineer', () => {
@@ -1387,6 +1994,26 @@
1387
1994
  });
1388
1995
  });
1389
1996
  }
1997
+
1998
+ // Generate rebuild guide
1999
+ try {
2000
+ const guide = generateGuide(parsedData);
2001
+ // Show guide in right panel
2002
+ const guideTab = document.getElementById('tab-guide');
2003
+ if (guideTab) {
2004
+ renderGuide(guideTab, guide);
2005
+ // Switch to guide tab
2006
+ document.querySelectorAll('.properties-tab').forEach(t => t.classList.remove('active'));
2007
+ const guideTabBtn = document.querySelector('[data-tab="guide"]');
2008
+ if (guideTabBtn) guideTabBtn.classList.add('active');
2009
+ document.getElementById('tab-properties').style.display = 'none';
2010
+ document.getElementById('tab-chat').style.display = 'none';
2011
+ guideTab.style.display = 'block';
2012
+ }
2013
+ } catch (err) {
2014
+ console.warn('Failed to generate rebuild guide:', err);
2015
+ }
2016
+
1390
2017
  updateStatus(`Inventor file loaded: ${parsedData.metadata?.fileName || 'unknown'} — ${parsedData.features?.length || 0} features found`);
1391
2018
  });
1392
2019
  });
@@ -1401,6 +2028,7 @@
1401
2028
  const target = tab.getAttribute('data-tab');
1402
2029
  document.getElementById('tab-properties').style.display = target === 'properties' ? 'block' : 'none';
1403
2030
  document.getElementById('tab-chat').style.display = target === 'chat' ? 'flex' : 'none';
2031
+ document.getElementById('tab-guide').style.display = target === 'guide' ? 'block' : 'none';
1404
2032
  });
1405
2033
  });
1406
2034
  }
@@ -1457,10 +2085,56 @@
1457
2085
  if (chatInput) chatInput.focus();
1458
2086
  });
1459
2087
 
1460
- // DUO Project Browser — open in new tab
2088
+ // DUO Project Browser — load manifest and show browser
1461
2089
  const browserBtn = document.getElementById('btn-open-browser');
1462
- if (browserBtn) browserBtn.addEventListener('click', () => {
1463
- window.open('duo-project-browser.html', '_blank');
2090
+ if (browserBtn) browserBtn.addEventListener('click', async () => {
2091
+ try {
2092
+ hide();
2093
+ const spinner = document.getElementById('kernel-spinner');
2094
+ if (spinner) spinner.classList.add('active');
2095
+ updateStatus('Loading DUO project manifest...');
2096
+
2097
+ // Fetch pre-built manifest (no File System Access API needed)
2098
+ const resp = await fetch('duo-manifest.json');
2099
+ if (!resp.ok) throw new Error('Failed to load manifest: ' + resp.status);
2100
+ const manifest = await resp.json();
2101
+
2102
+ // Transform file types: manifest uses type:"file" + ext:".ipt"
2103
+ // but project-browser expects type:"ipt" directly
2104
+ function transformTree(node) {
2105
+ if (node.type === 'file' && node.ext) {
2106
+ node.type = node.ext.replace('.', ''); // ".ipt" → "ipt"
2107
+ }
2108
+ if (node.children) node.children.forEach(transformTree);
2109
+ return node;
2110
+ }
2111
+ transformTree(manifest.tree);
2112
+
2113
+ // Store project data
2114
+ APP.project = manifest;
2115
+
2116
+ // Pass tree root to overlay browser (it needs .children at top level)
2117
+ setProject(manifest.tree);
2118
+
2119
+ // Also populate the inline left-panel browser
2120
+ populateInlineBrowser(manifest.tree);
2121
+
2122
+ // Show browser overlay (dismiss with close button, then left panel has it too)
2123
+ showBrowser();
2124
+
2125
+ // Switch left panel to Project Browser tab
2126
+ switchLeftTab('browser');
2127
+
2128
+ const stats = manifest.stats || {};
2129
+ updateStatus(`DUO project loaded: ${stats.parts || 0} parts, ${stats.assemblies || 0} assemblies, ${stats.total || 0} total files`);
2130
+
2131
+ if (spinner) spinner.classList.remove('active');
2132
+ } catch (err) {
2133
+ console.error('Failed to load DUO project:', err);
2134
+ updateStatus('Failed to load project: ' + err.message);
2135
+ const spinner = document.getElementById('kernel-spinner');
2136
+ if (spinner) spinner.classList.remove('active');
2137
+ }
1464
2138
  });
1465
2139
  }
1466
2140
 
@@ -1512,6 +2186,7 @@
1512
2186
  };
1513
2187
  APP.features.push(feature);
1514
2188
  addFeature(feature);
2189
+ pushHistory();
1515
2190
 
1516
2191
  updateStatus(`Created extrusion: ${h}mm`);
1517
2192
  document.getElementById('mode-indicator').textContent = 'Ready';
@@ -1539,6 +2214,7 @@
1539
2214
  };
1540
2215
  APP.features.push(feature);
1541
2216
  addFeature(feature);
2217
+ pushHistory();
1542
2218
  updateStatus(`Created: ${feature.name}`);
1543
2219
  } catch (err) {
1544
2220
  console.error('AI create failed:', err);
@@ -1569,9 +2245,90 @@
1569
2245
  removeFromScene(APP.selectedFeature.mesh);
1570
2246
  APP.features = APP.features.filter(f => f.id !== APP.selectedFeature.id);
1571
2247
  APP.selectedFeature = null;
2248
+ pushHistory();
1572
2249
  updateStatus('Feature deleted');
1573
2250
  }
1574
2251
 
2252
+ function undo() {
2253
+ if (APP.historyIndex > 0) {
2254
+ APP.historyIndex--;
2255
+ restoreFromHistory();
2256
+ updateStatus('Undo');
2257
+ } else {
2258
+ updateStatus('Nothing to undo');
2259
+ }
2260
+ }
2261
+
2262
+ function redo() {
2263
+ if (APP.historyIndex < APP.history.length - 1) {
2264
+ APP.historyIndex++;
2265
+ restoreFromHistory();
2266
+ updateStatus('Redo');
2267
+ } else {
2268
+ updateStatus('Nothing to redo');
2269
+ }
2270
+ }
2271
+
2272
+ function restoreFromHistory() {
2273
+ const state = APP.history[APP.historyIndex];
2274
+ if (state) {
2275
+ // Clear current scene
2276
+ APP.features.forEach((f) => {
2277
+ if (f.mesh) removeFromScene(f.mesh);
2278
+ });
2279
+
2280
+ // Restore features from history state
2281
+ APP.features = [];
2282
+ if (state.features && Array.isArray(state.features)) {
2283
+ state.features.forEach((featureData) => {
2284
+ try {
2285
+ const primitive = createPrimitive(featureData.type, featureData.params);
2286
+ addToScene(primitive.mesh);
2287
+
2288
+ const feature = {
2289
+ id: featureData.id,
2290
+ name: featureData.name,
2291
+ type: featureData.type,
2292
+ mesh: primitive.mesh,
2293
+ params: featureData.params,
2294
+ };
2295
+
2296
+ APP.features.push(feature);
2297
+ addFeature(feature);
2298
+ } catch (err) {
2299
+ console.warn(`Failed to restore feature ${featureData.name}:`, err);
2300
+ }
2301
+ });
2302
+ }
2303
+ }
2304
+ }
2305
+
2306
+ function fitAll() {
2307
+ if (APP.features.length === 0) {
2308
+ updateStatus('Nothing to fit');
2309
+ return;
2310
+ }
2311
+
2312
+ // Create a temporary group of all features to fit camera
2313
+ const group = new THREE.Group();
2314
+ APP.features.forEach((f) => {
2315
+ if (f.mesh) {
2316
+ group.add(f.mesh);
2317
+ }
2318
+ });
2319
+
2320
+ // Create a bounding box to check if there's anything to show
2321
+ const box = new THREE.Box3().setFromObject(group);
2322
+ if (box.isEmpty()) {
2323
+ updateStatus('No visible features to fit');
2324
+ return;
2325
+ }
2326
+
2327
+ // Fit camera to all features with padding
2328
+ fitToObject(group, 1.3);
2329
+ updateStatus('Fit all features');
2330
+ }
2331
+
1575
2332
  function doExportSTL() {
1576
2333
  if (APP.features.length === 0) { updateStatus('Nothing to export'); return; }
1577
2334
  try {
@@ -1606,6 +2363,32 @@
1606
2363
  if (modeEl && APP.mode === 'idle') modeEl.textContent = 'Ready';
1607
2364
  }
1608
2365
 
2366
+ function pushHistory() {
2367
+ // Trim redo stack if not at end
2368
+ if (APP.historyIndex < APP.history.length - 1) {
2369
+ APP.history = APP.history.slice(0, APP.historyIndex + 1);
2370
+ }
2371
+
2372
+ // Save state snapshot
2373
+ APP.history.push({
2374
+ features: JSON.parse(JSON.stringify(APP.features.map((f) => ({
2375
+ id: f.id,
2376
+ name: f.name,
2377
+ type: f.type,
2378
+ params: f.params,
2379
+ })))),
2380
+ timestamp: Date.now(),
2381
+ });
2382
+
2383
+ APP.historyIndex = APP.history.length - 1;
2384
+
2385
+ // Keep history limited to 50 entries
2386
+ if (APP.history.length > 50) {
2387
+ APP.history.shift();
2388
+ APP.historyIndex--;
2389
+ }
2390
+ }
2391
+
1609
2392
  // ========== FPS Counter ==========
1610
2393
  let frameCount = 0;
1611
2394
  let lastFPSTime = performance.now();
@@ -1630,6 +2413,367 @@
1630
2413
  }
1631
2414
 
1632
2415
  window.cycleCAD = { version: '1.0.0', APP, init };
2416
+
2417
+ // ========== Operation Dialog Management ==========
2418
+ let currentDialogId = null;
2419
+
2420
+ function openDialog(dialogId) {
2421
+ // Close any open dialog first
2422
+ if (currentDialogId) closeDialog(currentDialogId);
2423
+
2424
+ const dialog = document.getElementById(`dialog-${dialogId}`);
2425
+ const backdrop = document.getElementById('dialog-backdrop');
2426
+
2427
+ if (dialog && backdrop) {
2428
+ currentDialogId = dialogId;
2429
+ dialog.classList.add('visible');
2430
+ backdrop.classList.add('visible');
2431
+ updateStatus(`${dialogId.charAt(0).toUpperCase() + dialogId.slice(1)} dialog opened`);
2432
+ }
2433
+ }
2434
+
2435
+ function closeDialog(dialogId) {
2436
+ const dialog = document.getElementById(`dialog-${dialogId}`);
2437
+ const backdrop = document.getElementById('dialog-backdrop');
2438
+
2439
+ if (dialog) {
2440
+ dialog.classList.remove('visible');
2441
+ }
2442
+
2443
+ // Check if any dialog is still visible
2444
+ const visibleDialogs = document.querySelectorAll('.operation-dialog.visible');
2445
+ if (visibleDialogs.length === 0) {
2446
+ if (backdrop) backdrop.classList.remove('visible');
2447
+ currentDialogId = null;
2448
+ }
2449
+ }
2450
+
2451
+ function applyRevolve() {
2452
+ const axis = document.getElementById('revolve-axis').value;
2453
+ const angle = parseFloat(document.getElementById('revolve-angle').value);
2454
+ const direction = document.querySelector('input[name="revolve-direction"]:checked').value;
2455
+
2456
+ try {
2457
+ revolveProfile({ axis, angle, direction });
2458
+ updateStatus(`Revolved: ${angle}° around ${axis.toUpperCase()} axis`);
2459
+ pushHistory();
2460
+ closeDialog('revolve');
2461
+ } catch (err) {
2462
+ updateStatus('Revolve failed: ' + err.message);
2463
+ }
2464
+ }
2465
+
2466
+ function applyFillet() {
2467
+ const radius = parseFloat(document.getElementById('fillet-radius').value);
2468
+ const preview = document.getElementById('fillet-preview').checked;
2469
+
2470
+ try {
2471
+ filletEdges({ radius, preview });
2472
+ updateStatus(`Fillet applied: ${radius}mm radius`);
2473
+ pushHistory();
2474
+ closeDialog('fillet');
2475
+ } catch (err) {
2476
+ updateStatus('Fillet failed: ' + err.message);
2477
+ }
2478
+ }
2479
+
2480
+ function applyChamer() {
2481
+ const distance = parseFloat(document.getElementById('chamfer-distance').value);
2482
+
2483
+ try {
2484
+ chamferEdges({ distance });
2485
+ updateStatus(`Chamfer applied: ${distance}mm`);
2486
+ pushHistory();
2487
+ closeDialog('chamfer');
2488
+ } catch (err) {
2489
+ updateStatus('Chamfer failed: ' + err.message);
2490
+ }
2491
+ }
2492
+
2493
+ function applyBoolean() {
2494
+ const operation = document.querySelector('input[name="boolean-op"]:checked').value;
2495
+
2496
+ try {
2497
+ booleanOperation({ operation });
2498
+ updateStatus(`Boolean ${operation} applied`);
2499
+ pushHistory();
2500
+ closeDialog('boolean');
2501
+ } catch (err) {
2502
+ updateStatus('Boolean operation failed: ' + err.message);
2503
+ }
2504
+ }
2505
+
2506
+ function applyShell() {
2507
+ const thickness = parseFloat(document.getElementById('shell-thickness').value);
2508
+
2509
+ try {
2510
+ shellOperation({ thickness });
2511
+ updateStatus(`Shell created: ${thickness}mm wall thickness`);
2512
+ pushHistory();
2513
+ closeDialog('shell');
2514
+ } catch (err) {
2515
+ updateStatus('Shell failed: ' + err.message);
2516
+ }
2517
+ }
2518
+
2519
+ function updatePatternUI() {
2520
+ const patternType = document.querySelector('input[name="pattern-type"]:checked').value;
2521
+ const rectFields = document.getElementById('pattern-rectangular');
2522
+ const circFields = document.getElementById('pattern-circular');
2523
+
2524
+ if (patternType === 'rectangular') {
2525
+ rectFields.style.display = 'block';
2526
+ circFields.style.display = 'none';
2527
+ } else {
2528
+ rectFields.style.display = 'none';
2529
+ circFields.style.display = 'block';
2530
+ }
2531
+ }
2532
+
2533
+ function applyPattern() {
2534
+ const patternType = document.querySelector('input[name="pattern-type"]:checked').value;
2535
+ let params = { type: patternType };
2536
+
2537
+ try {
2538
+ if (patternType === 'rectangular') {
2539
+ params.countX = parseInt(document.getElementById('pattern-count-x').value);
2540
+ params.countY = parseInt(document.getElementById('pattern-count-y').value);
2541
+ params.spacingX = parseFloat(document.getElementById('pattern-spacing-x').value);
2542
+ params.spacingY = parseFloat(document.getElementById('pattern-spacing-y').value);
2543
+ patternFeature(params);
2544
+ updateStatus(`Rectangular pattern: ${params.countX}×${params.countY}`);
2545
+ } else {
2546
+ params.count = parseInt(document.getElementById('pattern-count').value);
2547
+ params.radius = parseFloat(document.getElementById('pattern-radius').value);
2548
+ params.axis = document.getElementById('pattern-axis').value;
2549
+ patternFeature(params);
2550
+ updateStatus(`Circular pattern: ${params.count} instances`);
2551
+ }
2552
+ pushHistory();
2553
+ closeDialog('pattern');
2554
+ } catch (err) {
2555
+ updateStatus('Pattern failed: ' + err.message);
2556
+ }
2557
+ }
2558
+
2559
+ // Close dialog when backdrop is clicked
2560
+ document.getElementById('dialog-backdrop').addEventListener('click', () => {
2561
+ if (currentDialogId) closeDialog(currentDialogId);
2562
+ });
2563
+
2564
+ // Close dialog on Escape key
2565
+ document.addEventListener('keydown', (e) => {
2566
+ if (e.key === 'Escape' && currentDialogId) {
2567
+ closeDialog(currentDialogId);
2568
+ }
2569
+ });
2570
+
1633
2571
  </script>
2572
+
2573
+ <!-- Operation Dialogs -->
2574
+ <div class="dialog-backdrop" id="dialog-backdrop"></div>
2575
+
2576
+ <!-- Revolve Dialog -->
2577
+ <div class="operation-dialog" id="dialog-revolve">
2578
+ <div class="dialog-header">
2579
+ <div class="dialog-title">Revolve Profile</div>
2580
+ <div class="dialog-close-btn" onclick="closeDialog('revolve')">✕</div>
2581
+ </div>
2582
+ <div class="dialog-content">
2583
+ <div class="dialog-form-group">
2584
+ <label class="dialog-label">Axis</label>
2585
+ <select class="dialog-select" id="revolve-axis">
2586
+ <option value="z">Z Axis</option>
2587
+ <option value="x">X Axis</option>
2588
+ <option value="y">Y Axis</option>
2589
+ </select>
2590
+ </div>
2591
+ <div class="dialog-form-group">
2592
+ <label class="dialog-label">Angle: <span id="revolve-angle-value">360</span>°</label>
2593
+ <input type="range" class="dialog-input" id="revolve-angle" min="1" max="360" value="360" oninput="document.getElementById('revolve-angle-value').textContent = this.value">
2594
+ </div>
2595
+ <div class="dialog-form-group">
2596
+ <label class="dialog-label">Direction</label>
2597
+ <div class="dialog-radio-group">
2598
+ <div class="dialog-radio-item">
2599
+ <input type="radio" id="revolve-dir-ccw" name="revolve-direction" value="ccw" checked>
2600
+ <label for="revolve-dir-ccw">Counter-Clockwise</label>
2601
+ </div>
2602
+ <div class="dialog-radio-item">
2603
+ <input type="radio" id="revolve-dir-cw" name="revolve-direction" value="cw">
2604
+ <label for="revolve-dir-cw">Clockwise</label>
2605
+ </div>
2606
+ </div>
2607
+ </div>
2608
+ </div>
2609
+ <div class="dialog-footer">
2610
+ <button class="dialog-button secondary" onclick="closeDialog('revolve')">Cancel</button>
2611
+ <button class="dialog-button primary" onclick="applyRevolve()">OK</button>
2612
+ </div>
2613
+ </div>
2614
+
2615
+ <!-- Fillet Dialog -->
2616
+ <div class="operation-dialog" id="dialog-fillet">
2617
+ <div class="dialog-header">
2618
+ <div class="dialog-title">Fillet Edges</div>
2619
+ <div class="dialog-close-btn" onclick="closeDialog('fillet')">✕</div>
2620
+ </div>
2621
+ <div class="dialog-content">
2622
+ <div class="dialog-form-group">
2623
+ <label class="dialog-label">Radius (mm)</label>
2624
+ <input type="number" class="dialog-input" id="fillet-radius" value="3" min="0.1" step="0.1">
2625
+ </div>
2626
+ <div class="dialog-form-group">
2627
+ <div class="dialog-checkbox">
2628
+ <input type="checkbox" id="fillet-preview" checked>
2629
+ <label for="fillet-preview">Preview</label>
2630
+ </div>
2631
+ </div>
2632
+ </div>
2633
+ <div class="dialog-footer">
2634
+ <button class="dialog-button secondary" onclick="closeDialog('fillet')">Cancel</button>
2635
+ <button class="dialog-button primary" onclick="applyFillet()">OK</button>
2636
+ </div>
2637
+ </div>
2638
+
2639
+ <!-- Chamfer Dialog -->
2640
+ <div class="operation-dialog" id="dialog-chamfer">
2641
+ <div class="dialog-header">
2642
+ <div class="dialog-title">Chamfer Edges</div>
2643
+ <div class="dialog-close-btn" onclick="closeDialog('chamfer')">✕</div>
2644
+ </div>
2645
+ <div class="dialog-content">
2646
+ <div class="dialog-form-group">
2647
+ <label class="dialog-label">Distance (mm)</label>
2648
+ <input type="number" class="dialog-input" id="chamfer-distance" value="2" min="0.1" step="0.1">
2649
+ </div>
2650
+ </div>
2651
+ <div class="dialog-footer">
2652
+ <button class="dialog-button secondary" onclick="closeDialog('chamfer')">Cancel</button>
2653
+ <button class="dialog-button primary" onclick="applyChamer()">OK</button>
2654
+ </div>
2655
+ </div>
2656
+
2657
+ <!-- Boolean Dialog -->
2658
+ <div class="operation-dialog" id="dialog-boolean">
2659
+ <div class="dialog-header">
2660
+ <div class="dialog-title">Boolean Operation</div>
2661
+ <div class="dialog-close-btn" onclick="closeDialog('boolean')">✕</div>
2662
+ </div>
2663
+ <div class="dialog-content">
2664
+ <div class="dialog-form-group">
2665
+ <label class="dialog-label">Operation</label>
2666
+ <div class="dialog-radio-group">
2667
+ <div class="dialog-radio-item">
2668
+ <input type="radio" id="bool-union" name="boolean-op" value="union" checked>
2669
+ <label for="bool-union">Union (Combine)</label>
2670
+ </div>
2671
+ <div class="dialog-radio-item">
2672
+ <input type="radio" id="bool-cut" name="boolean-op" value="cut">
2673
+ <label for="bool-cut">Cut (Subtract)</label>
2674
+ </div>
2675
+ <div class="dialog-radio-item">
2676
+ <input type="radio" id="bool-intersect" name="boolean-op" value="intersect">
2677
+ <label for="bool-intersect">Intersect (Common)</label>
2678
+ </div>
2679
+ </div>
2680
+ </div>
2681
+ <div class="dialog-form-group">
2682
+ <p style="font-size: 11px; color: var(--text-secondary); line-height: 1.6;">
2683
+ <strong>Instructions:</strong> Select the first body, then click OK. Select the second body, then click OK again.
2684
+ </p>
2685
+ </div>
2686
+ </div>
2687
+ <div class="dialog-footer">
2688
+ <button class="dialog-button secondary" onclick="closeDialog('boolean')">Cancel</button>
2689
+ <button class="dialog-button primary" onclick="applyBoolean()">OK</button>
2690
+ </div>
2691
+ </div>
2692
+
2693
+ <!-- Shell Dialog -->
2694
+ <div class="operation-dialog" id="dialog-shell">
2695
+ <div class="dialog-header">
2696
+ <div class="dialog-title">Shell (Hollow)</div>
2697
+ <div class="dialog-close-btn" onclick="closeDialog('shell')">✕</div>
2698
+ </div>
2699
+ <div class="dialog-content">
2700
+ <div class="dialog-form-group">
2701
+ <label class="dialog-label">Wall Thickness (mm)</label>
2702
+ <input type="number" class="dialog-input" id="shell-thickness" value="2" min="0.1" step="0.1">
2703
+ </div>
2704
+ </div>
2705
+ <div class="dialog-footer">
2706
+ <button class="dialog-button secondary" onclick="closeDialog('shell')">Cancel</button>
2707
+ <button class="dialog-button primary" onclick="applyShell()">OK</button>
2708
+ </div>
2709
+ </div>
2710
+
2711
+ <!-- Pattern Dialog -->
2712
+ <div class="operation-dialog" id="dialog-pattern">
2713
+ <div class="dialog-header">
2714
+ <div class="dialog-title">Pattern Features</div>
2715
+ <div class="dialog-close-btn" onclick="closeDialog('pattern')">✕</div>
2716
+ </div>
2717
+ <div class="dialog-content">
2718
+ <div class="dialog-form-group">
2719
+ <label class="dialog-label">Pattern Type</label>
2720
+ <div class="dialog-radio-group">
2721
+ <div class="dialog-radio-item">
2722
+ <input type="radio" id="pattern-rect" name="pattern-type" value="rectangular" checked onchange="updatePatternUI()">
2723
+ <label for="pattern-rect">Rectangular</label>
2724
+ </div>
2725
+ <div class="dialog-radio-item">
2726
+ <input type="radio" id="pattern-circ" name="pattern-type" value="circular" onchange="updatePatternUI()">
2727
+ <label for="pattern-circ">Circular</label>
2728
+ </div>
2729
+ </div>
2730
+ </div>
2731
+
2732
+ <!-- Rectangular Pattern Fields -->
2733
+ <div id="pattern-rectangular">
2734
+ <div class="dialog-form-group">
2735
+ <label class="dialog-label">Count X</label>
2736
+ <input type="number" class="dialog-input" id="pattern-count-x" value="3" min="1" step="1">
2737
+ </div>
2738
+ <div class="dialog-form-group">
2739
+ <label class="dialog-label">Count Y</label>
2740
+ <input type="number" class="dialog-input" id="pattern-count-y" value="3" min="1" step="1">
2741
+ </div>
2742
+ <div class="dialog-form-group">
2743
+ <label class="dialog-label">Spacing X (mm)</label>
2744
+ <input type="number" class="dialog-input" id="pattern-spacing-x" value="10" min="0.1" step="0.1">
2745
+ </div>
2746
+ <div class="dialog-form-group">
2747
+ <label class="dialog-label">Spacing Y (mm)</label>
2748
+ <input type="number" class="dialog-input" id="pattern-spacing-y" value="10" min="0.1" step="0.1">
2749
+ </div>
2750
+ </div>
2751
+
2752
+ <!-- Circular Pattern Fields -->
2753
+ <div id="pattern-circular" style="display:none;">
2754
+ <div class="dialog-form-group">
2755
+ <label class="dialog-label">Count</label>
2756
+ <input type="number" class="dialog-input" id="pattern-count" value="6" min="1" step="1">
2757
+ </div>
2758
+ <div class="dialog-form-group">
2759
+ <label class="dialog-label">Radius (mm)</label>
2760
+ <input type="number" class="dialog-input" id="pattern-radius" value="20" min="0.1" step="0.1">
2761
+ </div>
2762
+ <div class="dialog-form-group">
2763
+ <label class="dialog-label">Axis</label>
2764
+ <select class="dialog-select" id="pattern-axis">
2765
+ <option value="z">Z Axis</option>
2766
+ <option value="x">X Axis</option>
2767
+ <option value="y">Y Axis</option>
2768
+ </select>
2769
+ </div>
2770
+ </div>
2771
+ </div>
2772
+ <div class="dialog-footer">
2773
+ <button class="dialog-button secondary" onclick="closeDialog('pattern')">Cancel</button>
2774
+ <button class="dialog-button primary" onclick="applyPattern()">OK</button>
2775
+ </div>
2776
+ </div>
2777
+
1634
2778
  </body>
1635
2779
  </html>