cyclecad 0.5.0 → 0.8.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
@@ -41,6 +41,57 @@
41
41
  --properties-width: 300px;
42
42
  }
43
43
 
44
+ /* ===== ViewCube ===== */
45
+ .vc-face {
46
+ position: absolute;
47
+ width: 80px;
48
+ height: 80px;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ font-size: 10px;
53
+ font-weight: 700;
54
+ color: rgba(255,255,255,0.85);
55
+ background: rgba(40,44,52,0.8);
56
+ border: 1px solid rgba(88,166,255,0.4);
57
+ backface-visibility: hidden;
58
+ user-select: none;
59
+ text-transform: uppercase;
60
+ letter-spacing: 0.5px;
61
+ }
62
+ .vc-face:hover {
63
+ background: rgba(88,166,255,0.5);
64
+ color: #fff;
65
+ }
66
+ :root.light-mode .vc-face {
67
+ background: rgba(220,225,235,0.85);
68
+ color: rgba(0,0,0,0.7);
69
+ border: 1px solid rgba(0,102,204,0.4);
70
+ }
71
+ :root.light-mode .vc-face:hover {
72
+ background: rgba(0,102,204,0.5);
73
+ color: #fff;
74
+ }
75
+
76
+ /* ===== Light Mode ===== */
77
+ :root.light-mode {
78
+ --bg-primary: #f5f5f5;
79
+ --bg-secondary: #ffffff;
80
+ --bg-tertiary: #e8e8e8;
81
+ --border-color: #d0d0d0;
82
+ --text-primary: #1a1a1a;
83
+ --text-secondary: #555555;
84
+ --text-muted: #888888;
85
+ --accent-blue: #0066cc;
86
+ --accent-blue-dark: #004499;
87
+ --accent-green: #22863a;
88
+ --accent-red: #d73a49;
89
+ --accent-yellow: #b08800;
90
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
91
+ --shadow-md: 0 8px 16px rgba(0, 0, 0, 0.12);
92
+ --shadow-lg: 0 16px 32px rgba(0, 0, 0, 0.15);
93
+ }
94
+
44
95
  /* ===== Reset & Globals ===== */
45
96
  * {
46
97
  margin: 0;
@@ -92,11 +143,67 @@
92
143
  display: flex;
93
144
  align-items: center;
94
145
  padding: 0 8px;
95
- gap: 8px;
146
+ gap: 4px;
96
147
  user-select: none;
97
148
  z-index: 100;
98
149
  box-shadow: var(--shadow-sm);
150
+ overflow: visible;
151
+ scrollbar-width: thin;
152
+ scrollbar-color: rgba(255,255,255,0.2) transparent;
153
+ }
154
+ #toolbar::-webkit-scrollbar { height: 3px; }
155
+ #toolbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
156
+ #toolbar::-webkit-scrollbar-track { background: transparent; }
157
+
158
+ /* Dropdown menu for grouped tools */
159
+ .toolbar-dropdown {
160
+ position: relative;
161
+ }
162
+ .toolbar-dropdown-btn {
163
+ padding: 4px 6px;
164
+ height: 30px;
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 3px;
168
+ background: transparent;
169
+ border: 1px solid transparent;
170
+ border-radius: 4px;
171
+ color: var(--text-secondary);
172
+ cursor: pointer;
173
+ font-size: 11px;
174
+ white-space: nowrap;
175
+ }
176
+ .toolbar-dropdown-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
177
+ .toolbar-dropdown-btn .caret { font-size: 9px; opacity: 0.7; margin-left: 2px; }
178
+ .toolbar-dropdown-menu {
179
+ display: none;
180
+ position: fixed;
181
+ background: var(--bg-secondary);
182
+ border: 1px solid var(--border-color);
183
+ border-radius: 6px;
184
+ padding: 4px;
185
+ min-width: 160px;
186
+ z-index: 9999;
187
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
188
+ max-height: 70vh;
189
+ overflow-y: auto;
190
+ }
191
+ .toolbar-dropdown.open .toolbar-dropdown-menu { display: block; }
192
+ .toolbar-dropdown-menu button {
193
+ display: flex;
194
+ align-items: center;
195
+ gap: 6px;
196
+ width: 100%;
197
+ padding: 5px 8px;
198
+ background: transparent;
199
+ border: none;
200
+ border-radius: 4px;
201
+ color: var(--text-secondary);
202
+ cursor: pointer;
203
+ font-size: 11px;
204
+ white-space: nowrap;
99
205
  }
206
+ .toolbar-dropdown-menu button:hover { background: var(--bg-tertiary); color: var(--text-primary); }
100
207
 
101
208
  #content {
102
209
  flex: 1;
@@ -1280,13 +1387,13 @@
1280
1387
  <!-- Token Engine — Initialize early so window.cycleCAD.tokens is available -->
1281
1388
  <script src="./js/token-engine.js"></script>
1282
1389
  <!-- New Architecture Modules (ES modules need type="module") -->
1283
- <script type="module" src="./js/material-library.js"></script>
1390
+ <script type="module" src="./js/material-library.js?v=62"></script>
1284
1391
  <script src="./js/dfm-analyzer.js"></script>
1285
- <script type="module" src="./js/cam-pipeline.js"></script>
1392
+ <script type="module" src="./js/cam-pipeline.js?v=62"></script>
1286
1393
  <script src="./js/connected-fabs.js"></script>
1287
- <script type="module" src="./js/ai-copilot.js"></script>
1288
- <script type="module" src="./js/collaboration.js"></script>
1289
- <script type="module" src="./js/collaboration-ui.js"></script>
1394
+ <script type="module" src="./js/ai-copilot.js?v=62"></script>
1395
+ <script type="module" src="./js/collaboration.js?v=62"></script>
1396
+ <script type="module" src="./js/collaboration-ui.js?v=62"></script>
1290
1397
  <!-- CadXStudio-killer modules (IIFE, no imports) -->
1291
1398
  <script src="./js/text-to-cad.js"></script>
1292
1399
  <script src="./js/cam-operations.js"></script>
@@ -1312,6 +1419,7 @@
1312
1419
  <span class="splash-logo-cycle">cycle</span><span class="splash-logo-cad">CAD</span>
1313
1420
  </div>
1314
1421
  <p class="splash-subtitle">Parametric 3D CAD Modeler for the Mechanical Designer</p>
1422
+ <p style="color: rgba(255,255,255,0.45); font-size: 0.8rem; margin: 4px 0 0 0; letter-spacing: 1px;">v0.8.5</p>
1315
1423
  </div>
1316
1424
  <div class="splash-options">
1317
1425
  <button class="splash-button splash-button-primary" id="btn-empty-project" style="grid-column: 1 / -1;">
@@ -1330,278 +1438,144 @@
1330
1438
  <span>📂</span> DUO Project Browser (473 Parts)
1331
1439
  </button>
1332
1440
  </div>
1441
+ <button id="btn-splash-reset" style="margin-top: 12px; padding: 6px 16px; background: rgba(255,80,80,0.15); border: 1px solid rgba(255,80,80,0.3); color: #f88; border-radius: 6px; cursor: pointer; font-size: 0.75rem; letter-spacing: 0.5px;">
1442
+ 🔄 Clear Cache & Reload
1443
+ </button>
1333
1444
  </div>
1334
1445
 
1335
1446
  <!-- Top Toolbar -->
1336
1447
  <div id="toolbar">
1337
- <!-- Sketch Tools -->
1338
- <div class="toolbar-group">
1339
- <button class="toolbar-button" id="tool-sketch" title="Start Sketch (S)">
1340
- <span class="toolbar-icon">📐</span>
1341
- <span class="toolbar-label">Sketch</span>
1342
- </button>
1343
- <button class="toolbar-button" id="tool-line" title="Draw Line (L)">
1344
- <span class="toolbar-icon">/</span>
1345
- <span class="toolbar-label">Line</span>
1346
- </button>
1347
- <button class="toolbar-button" id="tool-rect" title="Draw Rectangle (R)">
1348
- <span class="toolbar-icon">▭</span>
1349
- <span class="toolbar-label">Rect</span>
1350
- </button>
1351
- <button class="toolbar-button" id="tool-circle" title="Draw Circle (C)">
1352
- <span class="toolbar-icon">⭕</span>
1353
- <span class="toolbar-label">Circle</span>
1354
- </button>
1355
- <button class="toolbar-button" id="tool-arc" title="Draw Arc (A)">
1356
- <span class="toolbar-icon">⌢</span>
1357
- <span class="toolbar-label">Arc</span>
1358
- </button>
1359
- <button class="toolbar-button" id="tool-dimension" title="Add Dimension (D)">
1360
- <span class="toolbar-icon">↔</span>
1361
- <span class="toolbar-label">Dim</span>
1362
- </button>
1363
- </div>
1364
-
1365
- <!-- Modeling Tools -->
1448
+ <!-- Pointer / Select -->
1366
1449
  <div class="toolbar-group">
1367
- <button class="toolbar-button" id="tool-extrude" title="Extrude (E)">
1368
- <span class="toolbar-icon">⬆</span>
1369
- <span class="toolbar-label">Extrude</span>
1370
- </button>
1371
- <button class="toolbar-button" id="tool-revolve" title="Revolve">
1372
- <span class="toolbar-icon">🔄</span>
1373
- <span class="toolbar-label">Revolve</span>
1374
- </button>
1375
- <button class="toolbar-button" id="tool-fillet" title="Fillet">
1376
- <span class="toolbar-icon">⌒</span>
1377
- <span class="toolbar-label">Fillet</span>
1378
- </button>
1379
- <button class="toolbar-button" id="tool-chamfer" title="Chamfer">
1380
- <span class="toolbar-icon">⌉</span>
1381
- <span class="toolbar-label">Chamfer</span>
1382
- </button>
1383
- <button class="toolbar-button" id="tool-cut" title="Boolean Cut">
1384
- <span class="toolbar-icon">−</span>
1385
- <span class="toolbar-label">Cut</span>
1386
- </button>
1387
- <button class="toolbar-button" id="tool-union" title="Boolean Union">
1388
- <span class="toolbar-icon">+</span>
1389
- <span class="toolbar-label">Union</span>
1390
- </button>
1450
+ <button class="toolbar-button active" id="tool-pointer" title="Select / Pointer (Esc)" style="background:rgba(88,166,255,0.15);border:1px solid rgba(88,166,255,0.4);"><span class="toolbar-icon">↖</span><span class="toolbar-label">Select</span></button>
1391
1451
  </div>
1392
1452
 
1393
- <!-- Advanced Operations -->
1453
+ <!-- Sketch Tools (always visible) -->
1394
1454
  <div class="toolbar-group">
1395
- <button class="toolbar-button" id="tool-sweep" title="Sweep (Profile Along Path)">
1396
- <span class="toolbar-icon">&#8634;</span>
1397
- <span class="toolbar-label">Sweep</span>
1398
- </button>
1399
- <button class="toolbar-button" id="tool-loft" title="Loft (Between Profiles)">
1400
- <span class="toolbar-icon">&#9651;</span>
1401
- <span class="toolbar-label">Loft</span>
1402
- </button>
1403
- <button class="toolbar-button" id="tool-spring" title="Generate Spring" style="background:rgba(63,185,80,0.08);">
1404
- <span class="toolbar-icon">&#8635;</span>
1405
- <span class="toolbar-label">Spring</span>
1406
- </button>
1407
- <button class="toolbar-button" id="tool-thread" title="Generate Thread" style="background:rgba(63,185,80,0.08);">
1408
- <span class="toolbar-icon">&#10038;</span>
1409
- <span class="toolbar-label">Thread</span>
1410
- </button>
1455
+ <button class="toolbar-button" id="tool-sketch" title="Start Sketch (S)"><span class="toolbar-icon">📐</span><span class="toolbar-label">Sketch</span></button>
1456
+ <button class="toolbar-button" id="tool-line" title="Line (L)"><span class="toolbar-icon">/</span></button>
1457
+ <button class="toolbar-button" id="tool-rect" title="Rect (R)"><span class="toolbar-icon">▭</span></button>
1458
+ <button class="toolbar-button" id="tool-circle" title="Circle (C)"><span class="toolbar-icon">⭕</span></button>
1459
+ <button class="toolbar-button" id="tool-arc" title="Arc (A)"><span class="toolbar-icon">⌢</span></button>
1460
+ <button class="toolbar-button" id="tool-dimension" title="Dimension (D)"><span class="toolbar-icon">↔</span></button>
1411
1461
  </div>
1412
1462
 
1413
- <!-- Sheet Metal -->
1463
+ <!-- Modeling (always visible) -->
1414
1464
  <div class="toolbar-group">
1415
- <button class="toolbar-button" id="tool-bend" title="Sheet Metal Bend" style="background:rgba(210,153,34,0.08);">
1416
- <span class="toolbar-icon">&#9484;</span>
1417
- <span class="toolbar-label">Bend</span>
1418
- </button>
1419
- <button class="toolbar-button" id="tool-flange" title="Sheet Metal Flange" style="background:rgba(210,153,34,0.08);">
1420
- <span class="toolbar-icon">&#9492;</span>
1421
- <span class="toolbar-label">Flange</span>
1422
- </button>
1423
- <button class="toolbar-button" id="tool-unfold" title="Unfold Sheet Metal" style="background:rgba(210,153,34,0.08);">
1424
- <span class="toolbar-icon">&#9645;</span>
1425
- <span class="toolbar-label">Unfold</span>
1426
- </button>
1465
+ <button class="toolbar-button" id="tool-extrude" title="Extrude (E)"><span class="toolbar-icon">⬆</span><span class="toolbar-label">Extrude</span></button>
1466
+ <button class="toolbar-button" id="tool-revolve" title="Revolve"><span class="toolbar-icon">🔄</span></button>
1467
+ <button class="toolbar-button" id="tool-fillet" title="Fillet"><span class="toolbar-icon">⌒</span></button>
1468
+ <button class="toolbar-button" id="tool-chamfer" title="Chamfer"><span class="toolbar-icon">⌉</span></button>
1469
+ <button class="toolbar-button" id="tool-cut" title="Cut"><span class="toolbar-icon">−</span></button>
1470
+ <button class="toolbar-button" id="tool-union" title="Union"><span class="toolbar-icon">+</span></button>
1427
1471
  </div>
1428
1472
 
1429
- <!-- Export Tools -->
1430
- <div class="toolbar-group">
1431
- <button class="toolbar-button" id="export-stl" title="Export STL">
1432
- <span class="toolbar-icon">💾</span>
1433
- <span class="toolbar-label">STL</span>
1434
- </button>
1435
- <button class="toolbar-button" id="export-step" title="Export STEP">
1436
- <span class="toolbar-icon">💾</span>
1437
- <span class="toolbar-label">STEP</span>
1438
- </button>
1439
- <button class="toolbar-button" id="export-dxf" title="Export DXF (2D Drawing)">
1440
- <span class="toolbar-icon">📐</span>
1441
- <span class="toolbar-label">DXF</span>
1442
- </button>
1443
- <button class="toolbar-button" id="export-multiview" title="Multi-View Engineering Drawing (DXF)">
1444
- <span class="toolbar-icon">📄</span>
1445
- <span class="toolbar-label">Drawing</span>
1446
- </button>
1473
+ <!-- Advanced Ops dropdown -->
1474
+ <div class="toolbar-dropdown">
1475
+ <button class="toolbar-dropdown-btn" title="Advanced Operations">⚙ More <span class="caret">▼</span></button>
1476
+ <div class="toolbar-dropdown-menu">
1477
+ <button id="tool-sweep" title="Sweep">↺ Sweep</button>
1478
+ <button id="tool-loft" title="Loft">△ Loft</button>
1479
+ <button id="tool-spring" title="Spring">↻ Spring</button>
1480
+ <button id="tool-thread" title="Thread">✦ Thread</button>
1481
+ <button id="tool-bend" title="Sheet Metal Bend">┌ Bend</button>
1482
+ <button id="tool-flange" title="Flange">└ Flange</button>
1483
+ <button id="tool-unfold" title="Unfold">▭ Unfold</button>
1484
+ <button id="tool-shell" title="Shell (Hollow)">◻ Shell</button>
1485
+ <button id="tool-pattern" title="Pattern (Array)">▦ Pattern</button>
1486
+ <button id="btn-sketch-enhance" title="Sketch+">✏ Sketch+</button>
1487
+ <button id="btn-section" title="Section View">🔪 Section</button>
1488
+ <button id="tool-measure" title="Measure (M key)">📏 Measure</button>
1489
+ </div>
1447
1490
  </div>
1448
1491
 
1449
- <!-- Assembly Tools -->
1450
- <div class="toolbar-group">
1451
- <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);">
1452
- <span class="toolbar-icon">&#9881;</span>
1453
- <span class="toolbar-label">Assembly</span>
1454
- </button>
1455
- <button class="toolbar-button" id="tool-explode" title="Explode/Collapse Assembly">
1456
- <span class="toolbar-icon">&#11043;</span>
1457
- <span class="toolbar-label">Explode</span>
1458
- </button>
1492
+ <!-- Export dropdown -->
1493
+ <div class="toolbar-dropdown">
1494
+ <button class="toolbar-dropdown-btn" title="Export">💾 Export <span class="caret">▼</span></button>
1495
+ <div class="toolbar-dropdown-menu">
1496
+ <button id="export-stl">💾 STL</button>
1497
+ <button id="export-step">💾 STEP</button>
1498
+ <button id="export-dxf">📐 DXF</button>
1499
+ <button id="export-multiview">📄 Drawing</button>
1500
+ </div>
1459
1501
  </div>
1460
1502
 
1461
- <!-- Reverse Engineer -->
1503
+ <!-- Assembly -->
1462
1504
  <div class="toolbar-group">
1463
- <button class="toolbar-button" id="btn-reverse-engineer" title="Reverse Engineer a Part (Import STL → Detect Features → Step-by-Step Rebuild Guide)" style="background:rgba(88,166,255,0.12);border:1px solid rgba(88,166,255,0.3);">
1464
- <span class="toolbar-icon">🔍</span>
1465
- <span class="toolbar-label">Reverse Engineer</span>
1466
- </button>
1467
- <button class="toolbar-button" id="btn-inventor-import" title="Import Autodesk Inventor .ipt/.iam — Parse Native Format" style="background:rgba(255,140,0,0.12);border:1px solid rgba(255,140,0,0.3);">
1468
- <span class="toolbar-icon">🏭</span>
1469
- <span class="toolbar-label">Inventor</span>
1470
- </button>
1505
+ <button class="toolbar-button" id="tool-assembly" title="Assembly" style="background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.3);"><span class="toolbar-icon">⚙</span><span class="toolbar-label">Assembly</span></button>
1506
+ <button class="toolbar-button" id="tool-explode" title="Explode"><span class="toolbar-icon">⊙</span></button>
1471
1507
  </div>
1472
1508
 
1473
- <!-- Token Balance Indicator -->
1474
- <div class="toolbar-group">
1475
- <button class="toolbar-button" id="btn-token-balance" title="View Token Balance & Billing" style="background: linear-gradient(135deg, rgba(88,166,255,0.1), rgba(63,185,80,0.1)); border: 1px solid rgba(88,166,255,0.3);" onclick="document.querySelector('[data-tab=tokens]')?.click()">
1476
- <span class="toolbar-icon">💰</span>
1477
- <span class="toolbar-label" id="token-balance-label">1K Tokens</span>
1478
- </button>
1509
+ <!-- Import dropdown -->
1510
+ <div class="toolbar-dropdown">
1511
+ <button class="toolbar-dropdown-btn" title="Import">📂 Import <span class="caret">▼</span></button>
1512
+ <div class="toolbar-dropdown-menu">
1513
+ <button id="btn-reverse-engineer">🔍 Reverse Engineer</button>
1514
+ <button id="btn-inventor-import">🏭 Inventor Import</button>
1515
+ </div>
1479
1516
  </div>
1480
1517
 
1481
- <!-- AI & Analysis -->
1482
- <div class="toolbar-group">
1483
- <button class="toolbar-button" id="btn-ai-copilot" title="AI Copilot — Text-to-CAD, Voice Commands" style="background:rgba(139,92,246,0.1);border:1px solid rgba(139,92,246,0.3);">
1484
- <span class="toolbar-icon">✨</span>
1485
- <span class="toolbar-label">Copilot</span>
1486
- </button>
1487
- <button class="toolbar-button" id="btn-dfm" title="DFM Analyzer — Manufacturability Check" style="background:rgba(248,113,113,0.1);border:1px solid rgba(248,113,113,0.3);">
1488
- <span class="toolbar-icon">🏭</span>
1489
- <span class="toolbar-label">DFM</span>
1490
- </button>
1491
- <button class="toolbar-button" id="btn-materials" title="Material Library — 40+ PBR Materials" style="background:rgba(34,211,238,0.1);border:1px solid rgba(34,211,238,0.3);">
1492
- <span class="toolbar-icon">🎨</span>
1493
- <span class="toolbar-label">Materials</span>
1494
- </button>
1495
- <button class="toolbar-button" id="btn-cam" title="CAM Pipeline — Slicer, Toolpath, G-code" style="background:rgba(251,191,36,0.1);border:1px solid rgba(251,191,36,0.3);">
1496
- <span class="toolbar-icon">⚙</span>
1497
- <span class="toolbar-label">CAM</span>
1498
- </button>
1499
- <button class="toolbar-button" id="btn-fabs" title="Connected Fabs — Send to Fabrication" style="background:rgba(74,222,128,0.1);border:1px solid rgba(74,222,128,0.3);">
1500
- <span class="toolbar-icon">🏗</span>
1501
- <span class="toolbar-label">Fabs</span>
1502
- </button>
1503
- <button class="toolbar-button" id="btn-collab" title="Collaboration — Multi-User Sessions" style="background:rgba(96,165,250,0.1);border:1px solid rgba(96,165,250,0.3);">
1504
- <span class="toolbar-icon">👥</span>
1505
- <span class="toolbar-label">Collab</span>
1506
- </button>
1507
- </div>
1508
- <!-- CadXStudio-Killer Features -->
1509
- <div class="toolbar-group">
1510
- <button class="toolbar-button" id="btn-text2cad" title="Text-to-CAD — Natural Language 3D Modeling" style="background:rgba(168,85,247,0.1);border:1px solid rgba(168,85,247,0.3);">
1511
- <span class="toolbar-icon">💬</span>
1512
- <span class="toolbar-label">Text2CAD</span>
1513
- </button>
1514
- <button class="toolbar-button" id="btn-camops" title="CAM Operations — Outline, Rough, Pocket, Drill" style="background:rgba(234,179,8,0.1);border:1px solid rgba(234,179,8,0.3);">
1515
- <span class="toolbar-icon">🔧</span>
1516
- <span class="toolbar-label">CAM Ops</span>
1517
- </button>
1518
- <button class="toolbar-button" id="btn-gcode" title="G-Code Generator — CNC, FDM, Laser" style="background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.3);">
1519
- <span class="toolbar-icon">📄</span>
1520
- <span class="toolbar-label">G-Code</span>
1521
- </button>
1522
- <button class="toolbar-button" id="btn-machines" title="Machine Profiles — 15+ CNC/FDM/Laser Presets" style="background:rgba(107,114,128,0.1);border:1px solid rgba(107,114,128,0.3);">
1523
- <span class="toolbar-icon">🖥</span>
1524
- <span class="toolbar-label">Machines</span>
1525
- </button>
1526
- <button class="toolbar-button" id="btn-tools" title="Tool Library — 30+ End Mills, Drills, V-Bits" style="background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);">
1527
- <span class="toolbar-icon">🔩</span>
1528
- <span class="toolbar-label">Tools</span>
1529
- </button>
1530
- <button class="toolbar-button" id="btn-stock" title="Stock Manager — Workpiece Definition" style="background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);">
1531
- <span class="toolbar-icon">📦</span>
1532
- <span class="toolbar-label">Stock</span>
1533
- </button>
1534
- <button class="toolbar-button" id="btn-sketch-enhance" title="Sketch Tools+ — Spline, Polygon, Text, Trim, Offset" style="background:rgba(59,130,246,0.1);border:1px solid rgba(59,130,246,0.3);">
1535
- <span class="toolbar-icon">✏</span>
1536
- <span class="toolbar-label">Sketch+</span>
1537
- </button>
1538
- <button class="toolbar-button" id="btn-section" title="Section View — Cut Planes (X)" style="background:rgba(220,38,38,0.1);border:1px solid rgba(220,38,38,0.3);">
1539
- <span class="toolbar-icon">🔪</span>
1540
- <span class="toolbar-label">Section</span>
1541
- </button>
1518
+ <!-- AI & Tools dropdown -->
1519
+ <div class="toolbar-dropdown">
1520
+ <button class="toolbar-dropdown-btn" title="AI & Tools" style="color:rgba(168,85,247,0.9);">✨ AI <span class="caret">▼</span></button>
1521
+ <div class="toolbar-dropdown-menu">
1522
+ <button id="btn-ai-copilot">✨ Copilot</button>
1523
+ <button id="btn-text2cad">💬 Text2CAD</button>
1524
+ <button id="btn-dfm">🏭 DFM Check</button>
1525
+ <button id="btn-materials">🎨 Materials</button>
1526
+ <button id="btn-generative">🧬 Generative</button>
1527
+ <button id="btn-agent-panel">🤖 Agent API</button>
1528
+ </div>
1542
1529
  </div>
1543
1530
 
1544
- <!-- Marketplace V2 + GD&T + MISUMI + VR -->
1545
- <div class="toolbar-group">
1546
- <button class="toolbar-button" id="btn-marketplace-v2" title="Model Marketplace (GrabCAD-style)" style="background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);">
1547
- <span class="toolbar-icon">🏪</span>
1548
- <span class="toolbar-label">Market</span>
1549
- </button>
1550
- <button class="toolbar-button" id="btn-gdt" title="GD&T Training & Reference" style="background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);">
1551
- <span class="toolbar-icon">📐</span>
1552
- <span class="toolbar-label">GD&T</span>
1553
- </button>
1554
- <button class="toolbar-button" id="btn-misumi" title="MISUMI Component Catalog" style="background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);">
1555
- <span class="toolbar-icon">🔩</span>
1556
- <span class="toolbar-label">MISUMI</span>
1557
- </button>
1558
- <button class="toolbar-button" id="btn-vr" title="CAD2VR — View in Virtual Reality" style="background:rgba(168,85,247,0.1);border:1px solid rgba(168,85,247,0.3);">
1559
- <span class="toolbar-icon">🥽</span>
1560
- <span class="toolbar-label">VR</span>
1561
- </button>
1562
- <button class="toolbar-button" id="btn-generative" title="Generative Design — Topology Optimization" style="background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.3);">
1563
- <span class="toolbar-icon">🧬</span>
1564
- <span class="toolbar-label">GenDes</span>
1565
- </button>
1531
+ <!-- CAM dropdown -->
1532
+ <div class="toolbar-dropdown">
1533
+ <button class="toolbar-dropdown-btn" title="CAM & Manufacturing">⚙ CAM <span class="caret">▼</span></button>
1534
+ <div class="toolbar-dropdown-menu">
1535
+ <button id="btn-cam">⚙ CAM Pipeline</button>
1536
+ <button id="btn-camops">🔧 CAM Ops</button>
1537
+ <button id="btn-gcode">📄 G-Code</button>
1538
+ <button id="btn-machines">🖥 Machines</button>
1539
+ <button id="btn-tools">🔩 Tools</button>
1540
+ <button id="btn-stock">📦 Stock</button>
1541
+ <button id="btn-fabs">🏗 Fabs</button>
1542
+ </div>
1566
1543
  </div>
1567
1544
 
1568
- <!-- Agent API Panel -->
1569
- <div class="toolbar-group">
1570
- <button class="toolbar-button" id="btn-agent-panel" title="Agent Command Panel (Test API)">
1571
- <span class="toolbar-icon">🤖</span>
1572
- <span class="toolbar-label">Agent</span>
1573
- </button>
1545
+ <!-- Reference dropdown -->
1546
+ <div class="toolbar-dropdown">
1547
+ <button class="toolbar-dropdown-btn" title="Reference & Catalog">📚 Ref <span class="caret">▼</span></button>
1548
+ <div class="toolbar-dropdown-menu">
1549
+ <button id="btn-marketplace-v2">🏪 Marketplace</button>
1550
+ <button id="btn-gdt">📐 GD&T Training</button>
1551
+ <button id="btn-misumi">🔩 MISUMI Catalog</button>
1552
+ <button id="btn-vr">🥽 VR Mode</button>
1553
+ <button id="btn-collab">👥 Collaboration</button>
1554
+ </div>
1574
1555
  </div>
1575
1556
 
1576
- <!-- Help & Tutorials -->
1557
+ <!-- Undo/Redo -->
1577
1558
  <div class="toolbar-group">
1578
- <button class="toolbar-button" id="btn-help" title="Help & Tutorials (?)" style="background:rgba(88,166,255,0.1);border:1px solid rgba(88,166,255,0.3);">
1579
- <span class="toolbar-icon">?</span>
1580
- <span class="toolbar-label">Help</span>
1581
- </button>
1559
+ <button class="toolbar-button" id="btn-undo" title="Undo (Ctrl+Z)"><span class="toolbar-icon">↶</span></button>
1560
+ <button class="toolbar-button" id="btn-redo" title="Redo (Ctrl+Y)"><span class="toolbar-icon">↷</span></button>
1582
1561
  </div>
1583
1562
 
1584
- <!-- Hard Reset -->
1585
- <div class="toolbar-group">
1586
- <button class="toolbar-button" id="btn-hard-reset" title="Clear cache and reload" style="color:#f55;">
1587
- <span class="toolbar-icon">🔄</span>
1588
- <span class="toolbar-label">Reset</span>
1589
- </button>
1590
- </div>
1563
+ <!-- Token balance -->
1564
+ <button class="toolbar-button" id="btn-token-balance" title="Tokens" onclick="document.querySelector('[data-tab=tokens]')?.click()" style="background:linear-gradient(135deg,rgba(88,166,255,0.1),rgba(63,185,80,0.1));border:1px solid rgba(88,166,255,0.3);">
1565
+ <span class="toolbar-icon">💰</span><span class="toolbar-label" id="token-balance-label">1K</span>
1566
+ </button>
1591
1567
 
1592
- <!-- Edit Operations -->
1593
- <div class="toolbar-group">
1594
- <button class="toolbar-button" id="btn-undo" title="Undo (Ctrl+Z)">
1595
- <span class="toolbar-icon">↶</span>
1596
- </button>
1597
- <button class="toolbar-button" id="btn-redo" title="Redo (Ctrl+Y)">
1598
- <span class="toolbar-icon">↷</span>
1599
- </button>
1568
+ <!-- Right-side: Theme + Help + Reset -->
1569
+ <div style="margin-left:auto;display:flex;gap:2px;">
1570
+ <button class="toolbar-button" id="btn-theme-toggle" title="Toggle Light/Dark Mode" style="background:rgba(255,200,50,0.12);border:1px solid rgba(255,200,50,0.3);"><span class="toolbar-icon" id="theme-icon">☀</span></button>
1571
+ <button class="toolbar-button" id="btn-help" title="Help (?)" style="background:rgba(88,166,255,0.1);"><span class="toolbar-icon">?</span></button>
1572
+ <button class="toolbar-button" id="btn-hard-reset" title="Clear cache & reload" style="color:#f55;"><span class="toolbar-icon">🔄</span></button>
1600
1573
  </div>
1574
+ </div><!-- end toolbar -->
1601
1575
 
1602
- <!-- View Buttons -->
1603
- <div class="toolbar-group">
1604
- <button class="toolbar-button" id="view-front" title="Front View (V+F)">
1576
+ <!-- Hidden: View buttons used by keyboard shortcuts only -->
1577
+ <div style="display:none;">
1578
+ <button id="view-front" title="Front View (V+F)">
1605
1579
  <span class="toolbar-label">Front</span>
1606
1580
  </button>
1607
1581
  <button class="toolbar-button" id="view-top" title="Top View (V+T)">
@@ -1616,9 +1590,7 @@
1616
1590
  <button class="toolbar-button" id="view-fit" title="Fit All (V)">
1617
1591
  <span class="toolbar-label">Fit</span>
1618
1592
  </button>
1619
- </div>
1620
-
1621
- </div>
1593
+ </div><!-- end hidden view buttons -->
1622
1594
 
1623
1595
  <!-- Main Content Area -->
1624
1596
  <div id="content">
@@ -1637,10 +1609,16 @@
1637
1609
  <div id="inline-project-browser">
1638
1610
  <!-- Inline project browser tree (populated when DUO manifest loads) -->
1639
1611
  <div class="ipb-search-box">
1640
- <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;">
1612
+ <input type="text" id="ipb-search" placeholder="Search 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;">
1641
1613
  </div>
1642
1614
  <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>
1643
- <div id="ipb-tree" style="flex:1;overflow-y:auto;padding:4px 0;min-height:0;"></div>
1615
+ <div id="ipb-tree" style="flex:1;overflow-y:auto;padding:4px 0;min-height:0;">
1616
+ <div id="ipb-empty" style="text-align:center;padding:20px 12px;">
1617
+ <div style="font-size:32px;margin-bottom:8px;opacity:0.5;">&#x1F4C2;</div>
1618
+ <p style="font-size:12px;font-weight:600;margin-bottom:6px;color:var(--text-primary);">No project loaded</p>
1619
+ <p style="font-size:11px;color:var(--text-secondary);line-height:1.5;">Use <b>Import &gt; Open Inventor Project</b> to load a project folder, or import STEP/STL files.</p>
1620
+ </div>
1621
+ </div>
1644
1622
  </div>
1645
1623
  </div>
1646
1624
  </div>
@@ -1659,6 +1637,18 @@
1659
1637
  <div class="snap-indicator" id="snap-indicator">Snap: Grid</div>
1660
1638
  </div>
1661
1639
 
1640
+ <!-- ViewCube Navigation -->
1641
+ <div id="viewcube" style="position:absolute;top:12px;right:12px;width:80px;height:80px;z-index:50;perspective:200px;cursor:pointer;">
1642
+ <div id="vc-cube" style="width:80px;height:80px;position:relative;transform-style:preserve-3d;transform:rotateX(-25deg) rotateY(-35deg);transition:transform 0.3s ease;">
1643
+ <div class="vc-face vc-front" data-vc="front" style="transform:translateZ(40px);">Front</div>
1644
+ <div class="vc-face vc-back" data-vc="back" style="transform:rotateY(180deg) translateZ(40px);">Back</div>
1645
+ <div class="vc-face vc-right" data-vc="right" style="transform:rotateY(90deg) translateZ(40px);">Right</div>
1646
+ <div class="vc-face vc-left" data-vc="left" style="transform:rotateY(-90deg) translateZ(40px);">Left</div>
1647
+ <div class="vc-face vc-top" data-vc="top" style="transform:rotateX(90deg) translateZ(40px);">Top</div>
1648
+ <div class="vc-face vc-bottom" data-vc="bottom" style="transform:rotateX(-90deg) translateZ(40px);">Btm</div>
1649
+ </div>
1650
+ </div>
1651
+
1662
1652
  <!-- Sketch Canvas Overlay -->
1663
1653
  <canvas id="sketch-canvas-overlay"></canvas>
1664
1654
 
@@ -1769,25 +1759,26 @@
1769
1759
  <!-- Module Loader -->
1770
1760
  <script type="module">
1771
1761
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
1772
- import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject } from './js/viewport.js';
1773
- import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js';
1774
- import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js';
1775
- import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js';
1776
- import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js';
1777
- import { initParams, showParams, onParamChange } from './js/params.js';
1778
- import { exportSTL, exportOBJ, exportJSON } from './js/export.js';
1779
- import { initShortcuts } from './js/shortcuts.js';
1780
- import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js';
1781
- import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js';
1782
- import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js';
1783
- import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js';
1784
- import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js';
1785
- import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js';
1786
- import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js';
1787
- import Assembly from './js/assembly.js';
1788
- import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js';
1789
- import { initAgentAPI } from './js/agent-api.js';
1790
- import { initTokenDashboard } from './js/token-dashboard.js';
1762
+ const _v = '50';
1763
+ import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject, fitAll } from './js/viewport.js?v=62';
1764
+ import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=62';
1765
+ import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=62';
1766
+ import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=62';
1767
+ import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js?v=62';
1768
+ import { initParams, showParams, onParamChange } from './js/params.js?v=62';
1769
+ import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=62';
1770
+ import { initShortcuts } from './js/shortcuts.js?v=62';
1771
+ import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=62';
1772
+ import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=62';
1773
+ import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=62';
1774
+ import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=62';
1775
+ import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=62';
1776
+ import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=62';
1777
+ import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=62';
1778
+ import Assembly from './js/assembly.js?v=62';
1779
+ import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=62';
1780
+ import { initAgentAPI } from './js/agent-api.js?v=62';
1781
+ import { initTokenDashboard } from './js/token-dashboard.js?v=62';
1791
1782
 
1792
1783
  // ========== Application State ==========
1793
1784
  const APP = {
@@ -1803,101 +1794,108 @@
1803
1794
 
1804
1795
  // ========== Initialization ==========
1805
1796
  async function init() {
1797
+ const errors = [];
1798
+ function tryStep(name, fn) {
1799
+ try { fn(); } catch(e) { console.warn('[init] ' + name + ' failed:', e.message); errors.push(name + ': ' + e.message); }
1800
+ }
1801
+ async function tryStepAsync(name, fn) {
1802
+ try { await fn(); } catch(e) { console.warn('[init] ' + name + ' failed:', e.message); errors.push(name + ': ' + e.message); }
1803
+ }
1806
1804
  try {
1807
1805
  console.log('Initializing cycleCAD...');
1808
1806
 
1809
1807
  // 1. Initialize 3D viewport
1810
- initViewport('viewport-container');
1811
- document.getElementById('kernel-status').classList.add('ready');
1812
- document.getElementById('kernel-status-text').textContent = 'Ready';
1808
+ tryStep('viewport', () => {
1809
+ initViewport('viewport-container');
1810
+ document.getElementById('kernel-status').classList.add('ready');
1811
+ document.getElementById('kernel-status-text').textContent = 'Ready';
1812
+ });
1813
1813
 
1814
1814
  // 1b. Initialize assembly workspace
1815
- APP.assembly = new Assembly(getScene());
1815
+ tryStep('assembly', () => { APP.assembly = new Assembly(getScene()); });
1816
1816
 
1817
1817
  // 2. Initialize feature tree
1818
- const treeContainer = document.getElementById('feature-tree');
1819
- if (treeContainer) initTree(treeContainer);
1818
+ tryStep('tree', () => {
1819
+ const treeContainer = document.getElementById('feature-tree');
1820
+ if (treeContainer) initTree(treeContainer);
1821
+ });
1820
1822
 
1821
1823
  // 3. Initialize properties panel
1822
- const propsContainer = document.getElementById('tab-properties');
1823
- if (propsContainer) initParams(propsContainer);
1824
+ tryStep('params', () => {
1825
+ const propsContainer = document.getElementById('tab-properties');
1826
+ if (propsContainer) initParams(propsContainer);
1827
+ });
1824
1828
 
1825
1829
  // 4. Initialize AI chat
1826
- const chatTab = document.getElementById('tab-chat');
1827
- if (chatTab) {
1828
- // Create chat UI structure
1829
- chatTab.innerHTML = `
1830
- <div id="chat-messages" style="flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:6px;min-height:0;"></div>
1831
- <div style="display:flex;gap:4px;padding:8px;border-top:1px solid var(--border-color);">
1832
- <input id="chat-input" type="text" placeholder="Describe a part..." style="flex:1;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-primary);font-size:12px;">
1833
- <button id="chat-send" style="padding:6px 12px;background:var(--accent-blue);color:#fff;border-radius:4px;font-size:12px;">Send</button>
1834
- </div>
1835
- `;
1836
- chatTab.style.display = 'none';
1837
- chatTab.style.flexDirection = 'column';
1838
- chatTab.style.height = '100%';
1839
-
1840
- initChat(
1841
- document.getElementById('chat-messages'),
1842
- document.getElementById('chat-input'),
1843
- document.getElementById('chat-send'),
1844
- (cmd) => executeParsedPrompt(cmd)
1845
- );
1846
- }
1830
+ tryStep('chat', () => {
1831
+ const chatTab = document.getElementById('tab-chat');
1832
+ if (chatTab) {
1833
+ chatTab.innerHTML = `
1834
+ <div id="chat-messages" style="flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:6px;min-height:0;"></div>
1835
+ <div style="display:flex;gap:4px;padding:8px;border-top:1px solid var(--border-color);">
1836
+ <input id="chat-input" type="text" placeholder="Describe a part..." style="flex:1;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-primary);font-size:12px;">
1837
+ <button id="chat-send" style="padding:6px 12px;background:var(--accent-blue);color:#fff;border-radius:4px;font-size:12px;">Send</button>
1838
+ </div>
1839
+ `;
1840
+ chatTab.style.display = 'none';
1841
+ chatTab.style.flexDirection = 'column';
1842
+ chatTab.style.height = '100%';
1843
+ initChat(
1844
+ document.getElementById('chat-messages'),
1845
+ document.getElementById('chat-input'),
1846
+ document.getElementById('chat-send'),
1847
+ (cmd) => executeParsedPrompt(cmd)
1848
+ );
1849
+ }
1850
+ });
1847
1851
 
1848
1852
  // 4b. Initialize Token Dashboard
1849
- const tokenTab = document.getElementById('tab-tokens');
1850
- if (tokenTab) {
1851
- const tokenDashboard = initTokenDashboard();
1852
- tokenTab.innerHTML = tokenDashboard.html;
1853
- tokenTab.style.display = 'none';
1854
- tokenTab.style.overflowY = 'auto';
1855
- // Initialize the dashboard events
1856
- tokenDashboard.init();
1857
-
1858
- // Update toolbar balance indicator
1859
- function updateTokenBalanceLabel() {
1860
- const balance = window.cycleCAD?.tokens?.getBalance?.() || 0;
1861
- const label = document.getElementById('token-balance-label');
1862
- if (label) {
1863
- if (balance >= 1000) {
1864
- label.textContent = Math.floor(balance / 1000) + 'K Tokens';
1865
- } else {
1866
- label.textContent = balance + ' Tokens';
1853
+ tryStep('tokenDashboard', () => {
1854
+ const tokenTab = document.getElementById('tab-tokens');
1855
+ if (tokenTab) {
1856
+ const tokenDashboard = initTokenDashboard();
1857
+ tokenTab.innerHTML = tokenDashboard.html;
1858
+ tokenTab.style.display = 'none';
1859
+ tokenTab.style.overflowY = 'auto';
1860
+ tokenDashboard.init();
1861
+ function updateTokenBalanceLabel() {
1862
+ const balance = window.cycleCAD?.tokens?.getBalance?.() || 0;
1863
+ const label = document.getElementById('token-balance-label');
1864
+ if (label) {
1865
+ label.textContent = balance >= 1000 ? Math.floor(balance / 1000) + 'K Tokens' : balance + ' Tokens';
1867
1866
  }
1868
1867
  }
1868
+ updateTokenBalanceLabel();
1869
+ if (window.cycleCAD?.tokens?.on) {
1870
+ window.cycleCAD.tokens.on('token-spent', updateTokenBalanceLabel);
1871
+ window.cycleCAD.tokens.on('token-added', updateTokenBalanceLabel);
1872
+ window.cycleCAD.tokens.on('month-reset', updateTokenBalanceLabel);
1873
+ }
1869
1874
  }
1870
- updateTokenBalanceLabel();
1871
-
1872
- // Update label when tokens change
1873
- if (window.cycleCAD?.tokens?.on) {
1874
- window.cycleCAD.tokens.on('token-spent', updateTokenBalanceLabel);
1875
- window.cycleCAD.tokens.on('token-added', updateTokenBalanceLabel);
1876
- window.cycleCAD.tokens.on('month-reset', updateTokenBalanceLabel);
1877
- }
1878
- }
1875
+ });
1879
1876
 
1880
1877
  // 5. Wire up toolbar buttons
1881
- setupToolbar();
1878
+ tryStep('toolbar', () => setupToolbar());
1882
1879
 
1883
1880
  // 6. Wire up tab switching
1884
- setupTabs();
1881
+ tryStep('tabs', () => setupTabs());
1885
1882
 
1886
1883
  // 7. Wire up tree selection → params
1887
- onSelect((featureId) => {
1888
- APP.selectedFeature = APP.features.find(f => f.id === featureId);
1889
- if (APP.selectedFeature) showParams(APP.selectedFeature);
1890
- });
1891
-
1892
- onParamChange((paramName, value) => {
1893
- if (APP.selectedFeature) {
1894
- APP.selectedFeature.params[paramName] = value;
1895
- rebuildFeature(APP.selectedFeature);
1896
- }
1884
+ tryStep('treeSelection', () => {
1885
+ onSelect((featureId) => {
1886
+ APP.selectedFeature = APP.features.find(f => f.id === featureId);
1887
+ if (APP.selectedFeature) showParams(APP.selectedFeature);
1888
+ });
1889
+ onParamChange((paramName, value) => {
1890
+ if (APP.selectedFeature) {
1891
+ APP.selectedFeature.params[paramName] = value;
1892
+ rebuildFeature(APP.selectedFeature);
1893
+ }
1894
+ });
1897
1895
  });
1898
1896
 
1899
1897
  // 8. Initialize keyboard shortcuts
1900
- initShortcuts({
1898
+ tryStep('shortcuts', () => initShortcuts({
1901
1899
  newSketch: () => startNewSketch(),
1902
1900
  line: () => setTool('line'),
1903
1901
  rect: () => setTool('rect'),
@@ -1917,20 +1915,15 @@
1917
1915
  viewBottom: () => setView('bottom'),
1918
1916
  viewIso: () => setView('iso'),
1919
1917
  toggleGrid: () => vpToggleGrid(),
1920
- fitAll: () => fitAll(),
1918
+ fitAll: () => fitAllFeatures(),
1921
1919
  save: () => saveProject(),
1922
1920
  exportSTL: () => doExportSTL(),
1923
- });
1921
+ }));
1924
1922
 
1925
- // 9. Setup welcome splash
1926
- setupWelcome();
1927
-
1928
- // 9b. Setup left panel tabs and inline browser
1929
- setupLeftTabs();
1930
- setupInlineBrowserClicks();
1923
+ // 9. Welcome splash + tabs now run AFTER try/catch (always execute)
1931
1924
 
1932
1925
  // 10. Initialize project browser
1933
- initProjectBrowser(document.body, {
1926
+ tryStep('projectBrowser', () => initProjectBrowser(document.body, {
1934
1927
  onFileOpen: async (file) => {
1935
1928
  try {
1936
1929
  const buffer = file.buffer || await file.arrayBuffer();
@@ -1962,31 +1955,31 @@
1962
1955
  APP.project = project;
1963
1956
  updateStatus(`Project loaded: ${project.stats.parts} parts, ${project.stats.assemblies} assemblies`);
1964
1957
  }
1965
- });
1966
-
1967
- // 11. Hard Refresh button — nukes all caches, service workers, and reloads
1968
- const hardRefreshBtn = document.getElementById('btn-hard-refresh');
1969
- if (hardRefreshBtn) hardRefreshBtn.addEventListener('click', async () => {
1970
- hardRefreshBtn.textContent = 'Clearing...';
1971
- hardRefreshBtn.style.opacity = '0.6';
1972
- // Kill service workers
1973
- if ('serviceWorker' in navigator) {
1974
- const regs = await navigator.serviceWorker.getRegistrations();
1975
- for (const r of regs) await r.unregister();
1976
- }
1977
- // Kill Cache API caches
1978
- if ('caches' in window) {
1979
- const names = await caches.keys();
1980
- for (const n of names) await caches.delete(n);
1981
- }
1982
- // Reload with timestamp cache bust
1983
- const url = new URL(window.location.href);
1984
- url.searchParams.set('v', Date.now());
1985
- window.location.replace(url.toString());
1958
+ }));
1959
+
1960
+ // 11. Hard Refresh button
1961
+ tryStep('hardRefresh', () => {
1962
+ const hardRefreshBtn = document.getElementById('btn-hard-refresh');
1963
+ if (hardRefreshBtn) hardRefreshBtn.addEventListener('click', async () => {
1964
+ hardRefreshBtn.textContent = 'Clearing...';
1965
+ hardRefreshBtn.style.opacity = '0.6';
1966
+ if ('serviceWorker' in navigator) {
1967
+ const regs = await navigator.serviceWorker.getRegistrations();
1968
+ for (const r of regs) await r.unregister();
1969
+ }
1970
+ if ('caches' in window) {
1971
+ const names = await caches.keys();
1972
+ for (const n of names) await caches.delete(n);
1973
+ }
1974
+ const url = new URL(window.location.href);
1975
+ url.searchParams.set('v', Date.now());
1976
+ window.location.replace(url.toString());
1977
+ });
1986
1978
  });
1987
1979
 
1988
1980
  // Initialize Agent API — the primary interface
1989
- const agentImports = await import('./js/agent-api.js');
1981
+ await tryStepAsync('agentAPI', async () => {
1982
+ const agentImports = await import('./js/agent-api.js?v=62');
1990
1983
  const agentSession = initAgentAPI({
1991
1984
  viewport: {
1992
1985
  getCamera,
@@ -2052,41 +2045,57 @@
2052
2045
  console.log(`[Agent API] Ready. Session: ${agentSession.sessionId}`);
2053
2046
  console.log('[Agent API] Usage: window.cycleCAD.execute({ method: "meta.ping" })');
2054
2047
  console.log('[Agent API] Schema: window.cycleCAD.getSchema()');
2048
+ }); // end agentAPI tryStepAsync
2055
2049
 
2056
2050
  // Initialize new architecture modules
2057
- if (window.cycleCAD?.materials?.init) {
2058
- window.cycleCAD.materials.init();
2059
- console.log('[Materials] Library initialized — ' + (window.cycleCAD.materials.getAll?.()?.length || '40+') + ' materials');
2060
- }
2061
- if (window.cycleCAD?.dfm?.init) {
2062
- window.cycleCAD.dfm.init();
2063
- console.log('[DFM] Analyzer initialized — 8 manufacturing processes');
2064
- }
2065
- if (window.cycleCAD?.cam?.init) {
2066
- window.cycleCAD.cam.init();
2067
- console.log('[CAM] Pipeline initialized — slicer, toolpath, G-code');
2068
- }
2069
- if (window.cycleCAD?.fabs?.init) {
2070
- window.cycleCAD.fabs.init();
2071
- console.log('[Fabs] Connected fabrication network initialized');
2072
- }
2073
- if (window.cycleCAD?.copilot?.init) {
2074
- window.cycleCAD.copilot.init();
2075
- console.log('[AI Copilot] Initialized — text-to-CAD, voice commands');
2076
- }
2077
- if (window.cycleCAD?.collab?.init) {
2078
- window.cycleCAD.collab.init();
2079
- console.log('[Collaboration] Session management initialized');
2080
- }
2051
+ tryStep('archModules', () => {
2052
+ if (window.cycleCAD?.materials?.init) window.cycleCAD.materials.init();
2053
+ if (window.cycleCAD?.dfm?.init) window.cycleCAD.dfm.init();
2054
+ if (window.cycleCAD?.cam?.init) window.cycleCAD.cam.init();
2055
+ if (window.cycleCAD?.fabs?.init) window.cycleCAD.fabs.init();
2056
+ if (window.cycleCAD?.copilot?.init) window.cycleCAD.copilot.init();
2057
+ if (window.cycleCAD?.collab?.init) window.cycleCAD.collab.init();
2058
+ });
2081
2059
 
2082
- console.log('cycleCAD initialized successfully — all modules loaded');
2083
- updateStatus('Ready');
2060
+ // Report results
2061
+ if (errors.length > 0) {
2062
+ console.warn('[init] Completed with errors:', errors);
2063
+ updateStatus('Ready (some modules failed — see console)');
2064
+ } else {
2065
+ console.log('cycleCAD initialized successfully — all modules loaded');
2066
+ updateStatus('Ready');
2067
+ }
2084
2068
 
2085
2069
  } catch (error) {
2086
2070
  console.error('Failed to initialize cycleCAD:', error);
2087
2071
  document.getElementById('kernel-status').classList.add('error');
2088
2072
  document.getElementById('kernel-status-text').textContent = 'Error';
2089
2073
  }
2074
+
2075
+ // ALWAYS run splash + toolbar setup even if init partially fails
2076
+ try { setupWelcome(); } catch(e) { console.warn('setupWelcome error:', e); }
2077
+ try { setupLeftTabs(); } catch(e) { console.warn('setupLeftTabs error:', e); }
2078
+ try { setupInlineBrowserClicks(); } catch(e) { console.warn('setupInlineBrowserClicks error:', e); }
2079
+
2080
+ // Auto-load DUO manifest into Project Browser (non-blocking)
2081
+ try {
2082
+ fetch('duo-manifest.json').then(resp => {
2083
+ if (!resp.ok) return;
2084
+ return resp.json();
2085
+ }).then(manifest => {
2086
+ if (!manifest) return;
2087
+ function transformTree(node) {
2088
+ if (node.type === 'file' && node.ext) node.type = node.ext.replace('.', '');
2089
+ if (node.children) node.children.forEach(transformTree);
2090
+ return node;
2091
+ }
2092
+ transformTree(manifest.tree);
2093
+ APP.project = manifest;
2094
+ try { setProject(manifest.tree); } catch(e) {}
2095
+ try { populateInlineBrowser(manifest.tree); } catch(e) {}
2096
+ console.log('[init] DUO manifest auto-loaded:', manifest.stats?.total || 0, 'files');
2097
+ }).catch(() => { /* manifest not available, ok */ });
2098
+ } catch(e) {}
2090
2099
  }
2091
2100
 
2092
2101
  // ========== Left Panel Tab Switching ==========
@@ -2356,7 +2365,7 @@
2356
2365
  bind('view-top', () => setView('top'));
2357
2366
  bind('view-right', () => setView('right'));
2358
2367
  bind('view-iso', () => setView('iso'));
2359
- bind('view-fit', () => fitAll());
2368
+ bind('view-fit', () => fitAllFeatures());
2360
2369
 
2361
2370
  // Reverse Engineer
2362
2371
  bind('btn-reverse-engineer', () => {
@@ -2408,6 +2417,8 @@
2408
2417
  });
2409
2418
 
2410
2419
  // Advanced Operations
2420
+ bind('tool-shell', () => openDialog('shell'));
2421
+ bind('tool-pattern', () => openDialog('pattern'));
2411
2422
  bind('tool-sweep', () => openDialog('sweep'));
2412
2423
  bind('tool-loft', () => openDialog('loft'));
2413
2424
  bind('tool-spring', () => {
@@ -2480,6 +2491,57 @@
2480
2491
  updateStatus('Assembly collapsed');
2481
2492
  }
2482
2493
  });
2494
+
2495
+ // ===== Pointer / Select tool =====
2496
+ bind('tool-pointer', () => {
2497
+ // Exit sketch mode if active
2498
+ if (APP.mode === 'sketch') {
2499
+ try { endSketch(); } catch(e) {}
2500
+ APP.currentSketch = null;
2501
+ APP.mode = 'idle';
2502
+ document.getElementById('mode-indicator').textContent = 'Ready';
2503
+ document.getElementById('mode-value').textContent = 'Normal';
2504
+ }
2505
+ // Deactivate all tool buttons
2506
+ document.querySelectorAll('.toolbar-button.active').forEach(b => b.classList.remove('active'));
2507
+ document.getElementById('tool-pointer').classList.add('active');
2508
+ updateStatus('Select mode');
2509
+ });
2510
+
2511
+ // ===== Dropdown menu positioning (fixed position) =====
2512
+ document.querySelectorAll('.toolbar-dropdown').forEach(dd => {
2513
+ const btn = dd.querySelector('.toolbar-dropdown-btn');
2514
+ const menu = dd.querySelector('.toolbar-dropdown-menu');
2515
+ if (!btn || !menu) return;
2516
+
2517
+ btn.addEventListener('click', (e) => {
2518
+ e.stopPropagation();
2519
+ const wasOpen = dd.classList.contains('open');
2520
+ // Close all other dropdowns
2521
+ document.querySelectorAll('.toolbar-dropdown.open').forEach(d => d.classList.remove('open'));
2522
+ if (!wasOpen) {
2523
+ dd.classList.add('open');
2524
+ // Position menu below button using fixed coordinates
2525
+ const rect = btn.getBoundingClientRect();
2526
+ menu.style.top = (rect.bottom + 2) + 'px';
2527
+ menu.style.left = rect.left + 'px';
2528
+ }
2529
+ });
2530
+
2531
+ // Close dropdown when clicking a menu item
2532
+ menu.querySelectorAll('button').forEach(item => {
2533
+ item.addEventListener('click', () => {
2534
+ dd.classList.remove('open');
2535
+ });
2536
+ });
2537
+ });
2538
+
2539
+ // Close dropdowns when clicking outside
2540
+ document.addEventListener('click', (e) => {
2541
+ if (!e.target.closest('.toolbar-dropdown')) {
2542
+ document.querySelectorAll('.toolbar-dropdown.open').forEach(d => d.classList.remove('open'));
2543
+ }
2544
+ });
2483
2545
  }
2484
2546
 
2485
2547
  // ========== Tab Switching ==========
@@ -2642,6 +2704,49 @@
2642
2704
  document.getElementById('mode-value').textContent = 'Sketch';
2643
2705
  }
2644
2706
 
2707
+ // Simple CSG subtract using geometry clipping approach
2708
+ function csgSubtract(baseMesh, cutMesh) {
2709
+ // Use Three.js CSG via manual approach:
2710
+ // 1. Get both geometries in world space
2711
+ // 2. Use Evaluator from three-bvh-csg if available
2712
+ // 3. Fallback: visual-only cut (hide intersecting faces)
2713
+
2714
+ // Try the simple approach: subtract by creating a new geometry
2715
+ // that removes the intersection volume
2716
+ try {
2717
+ const baseGeo = baseMesh.geometry.clone();
2718
+ const cutGeo = cutMesh.geometry.clone();
2719
+
2720
+ // Apply world transforms
2721
+ baseGeo.applyMatrix4(baseMesh.matrixWorld);
2722
+ cutGeo.applyMatrix4(cutMesh.matrixWorld);
2723
+
2724
+ // Use clipping planes to simulate the cut visually
2725
+ // Create 6 clipping planes from the cut geometry bounding box
2726
+ const cutBox = new THREE.Box3().setFromBufferAttribute(cutGeo.getAttribute('position'));
2727
+
2728
+ // For a proper visual cut, we'll use stencil buffer technique:
2729
+ // Make the cut area appear as a hole by rendering the cut mesh
2730
+ // with colorWrite=false to write to the depth buffer only
2731
+ const depthMat = new THREE.MeshBasicMaterial({
2732
+ colorWrite: false,
2733
+ depthWrite: true,
2734
+ side: THREE.DoubleSide
2735
+ });
2736
+ cutMesh.material = depthMat;
2737
+ cutMesh.renderOrder = -1;
2738
+
2739
+ // Make the base mesh render after, so the depth test creates a "hole"
2740
+ baseMesh.material.side = THREE.FrontSide;
2741
+ baseMesh.renderOrder = 0;
2742
+
2743
+ return { base: baseMesh, cut: cutMesh, method: 'depth' };
2744
+ } catch(e) {
2745
+ console.warn('CSG subtract failed:', e);
2746
+ return null;
2747
+ }
2748
+ }
2749
+
2645
2750
  function doExtrude() {
2646
2751
  if (APP.mode !== 'sketch') {
2647
2752
  updateStatus('Start a sketch first');
@@ -2653,30 +2758,89 @@
2653
2758
  return;
2654
2759
  }
2655
2760
 
2656
- const height = prompt('Enter extrusion height (mm):', '20');
2761
+ const height = prompt('Enter extrusion height in mm (negative = cut into existing body):', '20');
2657
2762
  if (height === null) return;
2658
2763
  const h = parseFloat(height);
2659
2764
  if (isNaN(h) || h === 0) { updateStatus('Invalid height'); return; }
2660
2765
 
2766
+ const isCut = h < 0;
2767
+
2661
2768
  try {
2662
- const mesh = extrudeProfile(entities, h);
2769
+ const result = extrudeProfile(entities, Math.abs(h));
2770
+ let mesh = result.mesh || result;
2663
2771
  endSketch();
2664
2772
  APP.currentSketch = null;
2665
2773
  APP.mode = 'idle';
2666
- addToScene(mesh);
2667
2774
 
2668
- const feature = {
2669
- id: 'feature_' + Date.now(),
2670
- name: `Extrusion (${h}mm)`,
2671
- type: 'extrude',
2672
- mesh,
2673
- params: { height: h },
2674
- };
2675
- APP.features.push(feature);
2676
- addFeature(feature);
2677
- pushHistory();
2775
+ if (isCut && APP.features.length > 0) {
2776
+ // Find the last solid feature to cut from
2777
+ const lastSolid = [...APP.features].reverse().find(f => f.mesh && f.type !== 'cut');
2778
+
2779
+ if (lastSolid && lastSolid.mesh) {
2780
+ // Position cut mesh at same location but going into the body
2781
+ mesh.position.copy(lastSolid.mesh.position);
2782
+ mesh.updateMatrixWorld(true);
2678
2783
 
2679
- updateStatus(`Created extrusion: ${h}mm`);
2784
+ // Visual cut: use depth buffer trick
2785
+ const cutResult = csgSubtract(lastSolid.mesh, mesh);
2786
+ if (cutResult) {
2787
+ addToScene(cutResult.cut);
2788
+ const feature = {
2789
+ id: 'feature_' + Date.now(),
2790
+ name: `Cut (${Math.abs(h)}mm)`,
2791
+ type: 'cut',
2792
+ mesh: cutResult.cut,
2793
+ params: { height: h },
2794
+ _geometryJSON: cutResult.cut.geometry ? cutResult.cut.geometry.toJSON() : null,
2795
+ _materialColor: '#f85149',
2796
+ };
2797
+ APP.features.push(feature);
2798
+ addFeature(feature);
2799
+ pushHistory();
2800
+ updateStatus(`Cut ${Math.abs(h)}mm into ${lastSolid.name}`);
2801
+ } else {
2802
+ // Fallback: just show the cut shape as red overlay
2803
+ mesh.material.color.setHex(0xf85149);
2804
+ mesh.material.transparent = true;
2805
+ mesh.material.opacity = 0.5;
2806
+ addToScene(mesh);
2807
+ const feature = {
2808
+ id: 'feature_' + Date.now(),
2809
+ name: `Cut (${Math.abs(h)}mm)`,
2810
+ type: 'cut',
2811
+ mesh,
2812
+ params: { height: h },
2813
+ _geometryJSON: mesh.geometry ? mesh.geometry.toJSON() : null,
2814
+ _materialColor: '#f85149',
2815
+ };
2816
+ APP.features.push(feature);
2817
+ addFeature(feature);
2818
+ pushHistory();
2819
+ updateStatus(`Cut ${Math.abs(h)}mm (visual — real CSG coming soon)`);
2820
+ }
2821
+ } else {
2822
+ updateStatus('No solid body to cut into — create an extrusion first');
2823
+ }
2824
+ } else {
2825
+ // Normal positive extrude
2826
+ addToScene(mesh);
2827
+ if (result.wireframe) addToScene(result.wireframe);
2828
+
2829
+ const feature = {
2830
+ id: 'feature_' + Date.now(),
2831
+ name: `Extrusion (${h}mm)`,
2832
+ type: 'extrude',
2833
+ mesh,
2834
+ params: { height: h },
2835
+ _geometryJSON: mesh.geometry ? mesh.geometry.toJSON() : null,
2836
+ _materialColor: mesh.material ? (mesh.material.color ? '#' + mesh.material.color.getHexString() : '#58a6ff') : '#58a6ff',
2837
+ };
2838
+ APP.features.push(feature);
2839
+ addFeature(feature);
2840
+ pushHistory();
2841
+
2842
+ updateStatus(`Created extrusion: ${h}mm`);
2843
+ }
2680
2844
  document.getElementById('mode-indicator').textContent = 'Ready';
2681
2845
  document.getElementById('mode-value').textContent = 'Normal';
2682
2846
  } catch (err) {
@@ -2691,13 +2855,15 @@
2691
2855
  if (splash) splash.classList.add('hidden');
2692
2856
 
2693
2857
  const result = createPrimitive(prompt.type || prompt.action, prompt.params || prompt);
2694
- addToScene(result.mesh || result);
2858
+ const mesh = result.mesh || result;
2859
+ addToScene(mesh);
2860
+ if (result.wireframe) addToScene(result.wireframe);
2695
2861
 
2696
2862
  const feature = {
2697
2863
  id: 'feature_' + Date.now(),
2698
2864
  name: prompt.name || prompt.type || prompt.action || 'Part',
2699
2865
  type: prompt.type || prompt.action,
2700
- mesh: result.mesh || result,
2866
+ mesh: mesh,
2701
2867
  params: prompt.params || prompt,
2702
2868
  };
2703
2869
  APP.features.push(feature);
@@ -2770,15 +2936,38 @@
2770
2936
  if (state.features && Array.isArray(state.features)) {
2771
2937
  state.features.forEach((featureData) => {
2772
2938
  try {
2773
- const primitive = createPrimitive(featureData.type, featureData.params);
2774
- addToScene(primitive.mesh);
2939
+ let mesh;
2940
+
2941
+ // Try geometry JSON first (works for ALL feature types including extrude)
2942
+ if (featureData._geometryJSON) {
2943
+ const loader = new THREE.BufferGeometryLoader();
2944
+ const geometry = loader.parse(featureData._geometryJSON);
2945
+ const color = featureData._materialColor || '#58a6ff';
2946
+ const mat = new THREE.MeshStandardMaterial({
2947
+ color: color,
2948
+ metalness: 0.3,
2949
+ roughness: 0.6,
2950
+ flatShading: false,
2951
+ });
2952
+ mesh = new THREE.Mesh(geometry, mat);
2953
+ mesh.castShadow = true;
2954
+ mesh.receiveShadow = true;
2955
+ } else {
2956
+ // Fall back to createPrimitive for basic primitives (box, cylinder, etc.)
2957
+ const primitive = createPrimitive(featureData.type, featureData.params);
2958
+ mesh = primitive.mesh;
2959
+ }
2960
+
2961
+ addToScene(mesh);
2775
2962
 
2776
2963
  const feature = {
2777
2964
  id: featureData.id,
2778
2965
  name: featureData.name,
2779
2966
  type: featureData.type,
2780
- mesh: primitive.mesh,
2967
+ mesh: mesh,
2781
2968
  params: featureData.params,
2969
+ _geometryJSON: featureData._geometryJSON,
2970
+ _materialColor: featureData._materialColor,
2782
2971
  };
2783
2972
 
2784
2973
  APP.features.push(feature);
@@ -2791,9 +2980,10 @@
2791
2980
  }
2792
2981
  }
2793
2982
 
2794
- function fitAll() {
2983
+ function fitAllFeatures() {
2795
2984
  if (APP.features.length === 0) {
2796
- updateStatus('Nothing to fit');
2985
+ // No local features — use imported fitAll from viewport
2986
+ fitAll();
2797
2987
  return;
2798
2988
  }
2799
2989
 
@@ -2857,14 +3047,30 @@
2857
3047
  APP.history = APP.history.slice(0, APP.historyIndex + 1);
2858
3048
  }
2859
3049
 
2860
- // Save state snapshot
3050
+ // Save state snapshot — include geometry JSON so undo/redo can recreate meshes
2861
3051
  APP.history.push({
2862
- features: JSON.parse(JSON.stringify(APP.features.map((f) => ({
2863
- id: f.id,
2864
- name: f.name,
2865
- type: f.type,
2866
- params: f.params,
2867
- })))),
3052
+ features: APP.features.map((f) => {
3053
+ const data = {
3054
+ id: f.id,
3055
+ name: f.name,
3056
+ type: f.type,
3057
+ params: f.params,
3058
+ };
3059
+ // Store geometry so we can restore without createPrimitive
3060
+ try {
3061
+ if (f._geometryJSON) {
3062
+ data._geometryJSON = f._geometryJSON;
3063
+ } else if (f.mesh && f.mesh.geometry) {
3064
+ data._geometryJSON = f.mesh.geometry.toJSON();
3065
+ }
3066
+ if (f._materialColor) {
3067
+ data._materialColor = f._materialColor;
3068
+ } else if (f.mesh && f.mesh.material && f.mesh.material.color) {
3069
+ data._materialColor = '#' + f.mesh.material.color.getHexString();
3070
+ }
3071
+ } catch(e) { /* geometry serialization failed, will try createPrimitive on restore */ }
3072
+ return data;
3073
+ }),
2868
3074
  timestamp: Date.now(),
2869
3075
  });
2870
3076
 
@@ -2902,6 +3108,17 @@
2902
3108
 
2903
3109
  window.cycleCAD = Object.assign(window.cycleCAD || {}, { version: '1.0.0', APP, init });
2904
3110
 
3111
+ // Expose dialog functions globally so inline onclick="" handlers work
3112
+ window.openDialog = openDialog;
3113
+ window.closeDialog = closeDialog;
3114
+ window.applyRevolve = typeof applyRevolve === 'function' ? applyRevolve : () => {};
3115
+ window.applyFillet = typeof applyFillet === 'function' ? applyFillet : () => {};
3116
+ window.applyChamfer = typeof applyChamer === 'function' ? applyChamer : () => {};
3117
+ window.applyChamer = window.applyChamfer; // alias for typo in dialog onclick
3118
+ window.applyBoolean = typeof applyBoolean === 'function' ? applyBoolean : () => {};
3119
+ window.applyShell = typeof applyShell === 'function' ? applyShell : () => {};
3120
+ window.applyPattern = typeof applyPattern === 'function' ? applyPattern : () => {};
3121
+
2905
3122
  // ========== Hard Reset (with browser detection) ==========
2906
3123
  const hardResetBtn = document.getElementById('btn-hard-reset');
2907
3124
  if (hardResetBtn) {
@@ -2958,11 +3175,39 @@
2958
3175
  log.push('Storage: cleared');
2959
3176
  console.log('[Hard Reset]', browser, log.join(' | '));
2960
3177
  // 6. Browser-specific reload
2961
- if (browser === 'Safari') {
2962
- window.location.href = window.location.pathname + '?cachebust=' + Date.now();
2963
- } else {
2964
- setTimeout(() => { window.location.reload(true); }, 300);
2965
- }
3178
+ // Always use cache-bust URL to guarantee fresh load
3179
+ setTimeout(() => {
3180
+ window.location.href = window.location.pathname + '?v=' + Date.now();
3181
+ }, 300);
3182
+ });
3183
+ }
3184
+
3185
+ // Splash screen reset button — same logic, no confirm dialog
3186
+ const splashResetBtn = document.getElementById('btn-splash-reset');
3187
+ if (splashResetBtn) {
3188
+ splashResetBtn.addEventListener('click', async () => {
3189
+ splashResetBtn.textContent = '⏳ Clearing...';
3190
+ try {
3191
+ if ('serviceWorker' in navigator) {
3192
+ const regs = await navigator.serviceWorker.getRegistrations();
3193
+ for (const r of regs) await r.unregister();
3194
+ }
3195
+ } catch(e) {}
3196
+ try {
3197
+ if ('caches' in window) {
3198
+ const names = await caches.keys();
3199
+ for (const n of names) await caches.delete(n);
3200
+ }
3201
+ } catch(e) {}
3202
+ try {
3203
+ if (indexedDB.databases) {
3204
+ const allDBs = await indexedDB.databases();
3205
+ for (const db of allDBs) { if (db.name) indexedDB.deleteDatabase(db.name); }
3206
+ }
3207
+ } catch(e) {}
3208
+ try { localStorage.clear(); } catch(e) {}
3209
+ try { sessionStorage.clear(); } catch(e) {}
3210
+ window.location.href = window.location.pathname + '?v=' + Date.now();
2966
3211
  });
2967
3212
  }
2968
3213
 
@@ -3360,7 +3605,10 @@
3360
3605
  body.style.cssText = 'padding:14px;flex:1;min-height:0;overflow-y:auto;font-size:12px;';
3361
3606
 
3362
3607
  const mod = window.cycleCAD?.[moduleKey];
3363
- if (mod?.getUI) {
3608
+ if (mod?.init) {
3609
+ // Use init() which sets innerHTML AND attaches event listeners
3610
+ mod.init(body);
3611
+ } else if (mod?.getUI) {
3364
3612
  body.innerHTML = mod.getUI();
3365
3613
  } else if (mod?.getStatus) {
3366
3614
  const status = mod.getStatus();
@@ -3393,6 +3641,286 @@
3393
3641
  document.getElementById('btn-generative')?.addEventListener('click', () => toggleModulePanel('generativeDesign', 'generative-panel'));
3394
3642
 
3395
3643
  // ========== Help & Tutorials Panel ==========
3644
+ // ========== Light/Dark Mode Toggle ==========
3645
+ document.getElementById('btn-theme-toggle')?.addEventListener('click', () => {
3646
+ const root = document.documentElement;
3647
+ const isLight = root.classList.toggle('light-mode');
3648
+ const icon = document.getElementById('theme-icon');
3649
+ if (icon) icon.textContent = isLight ? '🌙' : '☀';
3650
+
3651
+ // Update Three.js renderer background
3652
+ try {
3653
+ const scene = getScene();
3654
+ const renderer = window._renderer || document.querySelector('#viewport-container canvas')?.getContext('webgl');
3655
+ if (scene) {
3656
+ if (isLight) {
3657
+ scene.background = new THREE.Color(0xe8e8e8);
3658
+ scene.fog = null;
3659
+ } else {
3660
+ scene.background = new THREE.Color(0x1a1a2e);
3661
+ scene.fog = null;
3662
+ }
3663
+ }
3664
+ } catch(e) { console.warn('Theme: renderer update skipped', e); }
3665
+
3666
+ // Persist preference
3667
+ localStorage.setItem('cyclecad_theme', isLight ? 'light' : 'dark');
3668
+ updateStatus(isLight ? 'Light mode' : 'Dark mode');
3669
+ });
3670
+
3671
+ // Auto-restore saved theme
3672
+ if (localStorage.getItem('cyclecad_theme') === 'light') {
3673
+ document.documentElement.classList.add('light-mode');
3674
+ const icon = document.getElementById('theme-icon');
3675
+ if (icon) icon.textContent = '🌙';
3676
+ try {
3677
+ const scene = getScene();
3678
+ if (scene) scene.background = new THREE.Color(0xe8e8e8);
3679
+ } catch(e) {}
3680
+ }
3681
+
3682
+ // ========== ViewCube Navigation ==========
3683
+ document.querySelectorAll('.vc-face').forEach(face => {
3684
+ face.addEventListener('click', () => {
3685
+ const view = face.dataset.vc;
3686
+ if (view) {
3687
+ try { setView(view); } catch(e) {}
3688
+ updateStatus(view.charAt(0).toUpperCase() + view.slice(1) + ' view');
3689
+ }
3690
+ });
3691
+ });
3692
+
3693
+ // Continuously sync ViewCube rotation with camera using rAF
3694
+ (function syncViewCube() {
3695
+ const cube = document.getElementById('vc-cube');
3696
+ if (!cube) return;
3697
+ let lastRotX = null, lastRotY = null;
3698
+ function updateCubeFromCamera() {
3699
+ try {
3700
+ const cam = getCamera();
3701
+ if (cam) {
3702
+ const dir = new THREE.Vector3();
3703
+ cam.getWorldDirection(dir);
3704
+ const rotY = Math.atan2(dir.x, dir.z) * (180 / Math.PI);
3705
+ const rotX = Math.asin(-dir.y) * (180 / Math.PI);
3706
+ const rxi = Math.round(rotX * 10);
3707
+ const ryi = Math.round(rotY * 10);
3708
+ if (rxi !== lastRotX || ryi !== lastRotY) {
3709
+ lastRotX = rxi;
3710
+ lastRotY = ryi;
3711
+ cube.style.transform = `rotateX(${rotX}deg) rotateY(${rotY + 180}deg)`;
3712
+ }
3713
+ }
3714
+ } catch(e) {}
3715
+ requestAnimationFrame(updateCubeFromCamera);
3716
+ }
3717
+ requestAnimationFrame(updateCubeFromCamera);
3718
+ })();
3719
+
3720
+ // ViewCube drag-to-orbit: drag on the cube rotates the 3D camera
3721
+ (function initViewCubeDrag() {
3722
+ const vcEl = document.getElementById('viewcube');
3723
+ if (!vcEl) return;
3724
+ let dragging = false, startX = 0, startY = 0;
3725
+ let startAzimuth = 0, startPolar = 0;
3726
+
3727
+ vcEl.addEventListener('pointerdown', (e) => {
3728
+ // Only start drag if not clicking a face label directly
3729
+ dragging = true;
3730
+ startX = e.clientX;
3731
+ startY = e.clientY;
3732
+ try {
3733
+ const controls = getControls();
3734
+ if (controls) {
3735
+ startAzimuth = controls.getAzimuthalAngle();
3736
+ startPolar = controls.getPolarAngle();
3737
+ }
3738
+ } catch(err) {}
3739
+ vcEl.setPointerCapture(e.pointerId);
3740
+ e.preventDefault();
3741
+ });
3742
+
3743
+ vcEl.addEventListener('pointermove', (e) => {
3744
+ if (!dragging) return;
3745
+ const dx = e.clientX - startX;
3746
+ const dy = e.clientY - startY;
3747
+ // Only orbit if dragged more than 4px (otherwise it's a click)
3748
+ if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
3749
+ try {
3750
+ const controls = getControls();
3751
+ const cam = getCamera();
3752
+ if (controls && cam) {
3753
+ // Spherical orbit: dx → azimuthal, dy → polar
3754
+ const sensitivity = 0.02;
3755
+ const target = controls.target.clone();
3756
+ const offset = cam.position.clone().sub(target);
3757
+ const spherical = new THREE.Spherical().setFromVector3(offset);
3758
+ spherical.theta = startAzimuth - dx * sensitivity;
3759
+ spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, startPolar + dy * sensitivity));
3760
+ offset.setFromSpherical(spherical);
3761
+ cam.position.copy(target).add(offset);
3762
+ cam.lookAt(target);
3763
+ controls.update();
3764
+ }
3765
+ } catch(err) {}
3766
+ });
3767
+
3768
+ vcEl.addEventListener('pointerup', (e) => {
3769
+ const dx = Math.abs(e.clientX - startX);
3770
+ const dy = Math.abs(e.clientY - startY);
3771
+ dragging = false;
3772
+ // If it was a small move, let the face click handler fire
3773
+ });
3774
+ })();
3775
+
3776
+ // ========== Measurement Tool ==========
3777
+ (function initMeasureTool() {
3778
+ let measureMode = false;
3779
+ let measurePoints = [];
3780
+ let measureLines = [];
3781
+ let measureLabels = [];
3782
+
3783
+ function clearMeasurements() {
3784
+ const scene = getScene();
3785
+ if (!scene) return;
3786
+ measureLines.forEach(l => scene.remove(l));
3787
+ measureLabels.forEach(l => l.remove());
3788
+ measureLines = [];
3789
+ measureLabels = [];
3790
+ measurePoints = [];
3791
+ }
3792
+
3793
+ function addMeasureLabel(text, position) {
3794
+ const div = document.createElement('div');
3795
+ div.className = 'measure-label';
3796
+ div.textContent = text;
3797
+ div.style.cssText = 'position:absolute;background:rgba(88,166,255,0.9);color:#fff;padding:2px 6px;border-radius:3px;font-size:11px;font-weight:600;pointer-events:none;z-index:100;white-space:nowrap;';
3798
+ document.getElementById('viewport-container').appendChild(div);
3799
+ measureLabels.push(div);
3800
+
3801
+ function updatePos() {
3802
+ if (!div.parentElement) return;
3803
+ try {
3804
+ const cam = getCamera();
3805
+ const container = document.getElementById('viewport-container');
3806
+ if (!cam || !container) return;
3807
+ const vec = position.clone().project(cam);
3808
+ const rect = container.getBoundingClientRect();
3809
+ div.style.left = ((vec.x + 1) / 2 * rect.width) + 'px';
3810
+ div.style.top = ((-vec.y + 1) / 2 * rect.height) + 'px';
3811
+ } catch(e) {}
3812
+ requestAnimationFrame(updatePos);
3813
+ }
3814
+ requestAnimationFrame(updatePos);
3815
+ return div;
3816
+ }
3817
+
3818
+ function onMeasureClick(e) {
3819
+ if (!measureMode) return;
3820
+ const cam = getCamera();
3821
+ const scene = getScene();
3822
+ if (!cam || !scene) return;
3823
+
3824
+ const container = document.getElementById('viewport-container');
3825
+ const rect = container.getBoundingClientRect();
3826
+ const mouse = new THREE.Vector2(
3827
+ ((e.clientX - rect.left) / rect.width) * 2 - 1,
3828
+ -((e.clientY - rect.top) / rect.height) * 2 + 1
3829
+ );
3830
+
3831
+ const raycaster = new THREE.Raycaster();
3832
+ raycaster.setFromCamera(mouse, cam);
3833
+
3834
+ const meshes = [];
3835
+ scene.traverse(obj => { if (obj.isMesh) meshes.push(obj); });
3836
+ const hits = raycaster.intersectObjects(meshes, true);
3837
+ if (hits.length === 0) return;
3838
+
3839
+ const pt = hits[0].point.clone();
3840
+ measurePoints.push(pt);
3841
+
3842
+ // Visual marker
3843
+ const marker = new THREE.Mesh(
3844
+ new THREE.SphereGeometry(0.3, 8, 8),
3845
+ new THREE.MeshBasicMaterial({ color: 0x58a6ff })
3846
+ );
3847
+ marker.position.copy(pt);
3848
+ scene.add(marker);
3849
+ measureLines.push(marker);
3850
+
3851
+ if (measurePoints.length === 2) {
3852
+ // Distance measurement
3853
+ const p1 = measurePoints[0], p2 = measurePoints[1];
3854
+ const dist = p1.distanceTo(p2);
3855
+ const dx = Math.abs(p2.x - p1.x);
3856
+ const dy = Math.abs(p2.y - p1.y);
3857
+ const dz = Math.abs(p2.z - p1.z);
3858
+
3859
+ // Draw line
3860
+ const lineGeo = new THREE.BufferGeometry().setFromPoints([p1, p2]);
3861
+ const lineMat = new THREE.LineBasicMaterial({ color: 0x58a6ff, linewidth: 2 });
3862
+ const line = new THREE.Line(lineGeo, lineMat);
3863
+ scene.add(line);
3864
+ measureLines.push(line);
3865
+
3866
+ // Label at midpoint
3867
+ const mid = p1.clone().add(p2).multiplyScalar(0.5);
3868
+ addMeasureLabel(`${dist.toFixed(1)}mm (dx:${dx.toFixed(1)} dy:${dy.toFixed(1)} dz:${dz.toFixed(1)})`, mid);
3869
+ updateStatus(`Distance: ${dist.toFixed(2)}mm`);
3870
+
3871
+ } else if (measurePoints.length === 3) {
3872
+ // Angle measurement between 3 points
3873
+ const p1 = measurePoints[0], p2 = measurePoints[1], p3 = measurePoints[2];
3874
+ const v1 = p1.clone().sub(p2).normalize();
3875
+ const v2 = p3.clone().sub(p2).normalize();
3876
+ const angle = Math.acos(Math.max(-1, Math.min(1, v1.dot(v2)))) * (180 / Math.PI);
3877
+
3878
+ // Draw angle lines
3879
+ const lg1 = new THREE.BufferGeometry().setFromPoints([p1, p2]);
3880
+ const lg2 = new THREE.BufferGeometry().setFromPoints([p2, p3]);
3881
+ const lm = new THREE.LineBasicMaterial({ color: 0x3fb950, linewidth: 2 });
3882
+ const l1 = new THREE.Line(lg1, lm);
3883
+ const l2 = new THREE.Line(lg2, lm);
3884
+ scene.add(l1); scene.add(l2);
3885
+ measureLines.push(l1, l2);
3886
+
3887
+ addMeasureLabel(`${angle.toFixed(1)}°`, p2);
3888
+ updateStatus(`Angle: ${angle.toFixed(2)}° | Distance 1→2: ${p1.distanceTo(p2).toFixed(1)}mm | Distance 2→3: ${p2.distanceTo(p3).toFixed(1)}mm`);
3889
+ measurePoints = []; // Reset after angle
3890
+ }
3891
+ }
3892
+
3893
+ // Toggle measure mode with M key or toolbar button
3894
+ document.addEventListener('keydown', (e) => {
3895
+ if (e.key === 'm' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
3896
+ measureMode = !measureMode;
3897
+ if (measureMode) {
3898
+ measurePoints = [];
3899
+ updateStatus('Measure mode: Click 2 points for distance, 3 points for angle. Press M to exit.');
3900
+ document.getElementById('viewport-container').addEventListener('click', onMeasureClick);
3901
+ } else {
3902
+ clearMeasurements();
3903
+ document.getElementById('viewport-container').removeEventListener('click', onMeasureClick);
3904
+ updateStatus('Measure mode off');
3905
+ }
3906
+ }
3907
+ });
3908
+
3909
+ // Also wire up measure button if it exists
3910
+ document.getElementById('tool-measure')?.addEventListener('click', () => {
3911
+ measureMode = !measureMode;
3912
+ if (measureMode) {
3913
+ measurePoints = [];
3914
+ updateStatus('Measure mode: Click 2 points for distance, 3 for angle. Press M to exit.');
3915
+ document.getElementById('viewport-container').addEventListener('click', onMeasureClick);
3916
+ } else {
3917
+ clearMeasurements();
3918
+ document.getElementById('viewport-container').removeEventListener('click', onMeasureClick);
3919
+ updateStatus('Measure mode off');
3920
+ }
3921
+ });
3922
+ })();
3923
+
3396
3924
  document.getElementById('btn-help')?.addEventListener('click', () => {
3397
3925
  let panel = document.getElementById('help-tutorials-panel');
3398
3926
  if (panel) { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; return; }
@@ -3846,6 +4374,6 @@
3846
4374
  </div>
3847
4375
  </div>
3848
4376
 
3849
- <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v0.4.0</span>
4377
+ <span id="version-badge" style="position:fixed;bottom:42px;left:50%;transform:translateX(-50%);z-index:999;font-size:0.9rem;color:rgba(255,255,255,0.9);letter-spacing:0.1em;white-space:nowrap;padding:6px 16px;user-select:all;pointer-events:auto;font-family:monospace;font-weight:700;background:rgba(0,0,0,0.7);border:1px solid rgba(88,166,255,0.4);border-radius:6px;text-shadow:0 1px 3px rgba(0,0,0,0.5);" title="cycleCAD version">cycleCAD v0.8.5</span>
3850
4378
  </body>
3851
4379
  </html>