cyclecad 0.5.0 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -27
- package/app/index.html +984 -431
- package/app/js/agent-api.js +1 -6
- package/app/js/operations.js +117 -55
- package/app/js/sketch.js +44 -14
- package/app/js/tree.js +6 -1
- package/app/js/viewport.js +8 -0
- package/package.json +2 -2
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:
|
|
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=63"></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=63"></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=63"></script>
|
|
1395
|
+
<script type="module" src="./js/collaboration.js?v=63"></script>
|
|
1396
|
+
<script type="module" src="./js/collaboration-ui.js?v=63"></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.6</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
|
-
<!--
|
|
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-
|
|
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
|
-
<!--
|
|
1453
|
+
<!-- Sketch Tools (always visible) -->
|
|
1394
1454
|
<div class="toolbar-group">
|
|
1395
|
-
<button class="toolbar-button" id="tool-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
<button class="toolbar-button" id="tool-
|
|
1400
|
-
|
|
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">↻</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">✶</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
|
-
<!--
|
|
1463
|
+
<!-- Modeling (always visible) -->
|
|
1414
1464
|
<div class="toolbar-group">
|
|
1415
|
-
<button class="toolbar-button" id="tool-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
<button class="toolbar-button" id="tool-
|
|
1420
|
-
|
|
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">▭</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
|
-
<!--
|
|
1430
|
-
<div class="toolbar-
|
|
1431
|
-
<button class="toolbar-
|
|
1432
|
-
|
|
1433
|
-
<
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
<
|
|
1437
|
-
<
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
<
|
|
1441
|
-
<
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
<
|
|
1445
|
-
|
|
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
|
-
<!--
|
|
1450
|
-
<div class="toolbar-
|
|
1451
|
-
<button class="toolbar-
|
|
1452
|
-
|
|
1453
|
-
<
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
<
|
|
1457
|
-
|
|
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
|
-
<!--
|
|
1503
|
+
<!-- Assembly -->
|
|
1462
1504
|
<div class="toolbar-group">
|
|
1463
|
-
<button class="toolbar-button" id="
|
|
1464
|
-
|
|
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
|
-
<!--
|
|
1474
|
-
<div class="toolbar-
|
|
1475
|
-
<button class="toolbar-
|
|
1476
|
-
|
|
1477
|
-
<
|
|
1478
|
-
|
|
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 &
|
|
1482
|
-
<div class="toolbar-
|
|
1483
|
-
<button class="toolbar-
|
|
1484
|
-
|
|
1485
|
-
<
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
<
|
|
1489
|
-
<
|
|
1490
|
-
|
|
1491
|
-
|
|
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
|
-
<!--
|
|
1545
|
-
<div class="toolbar-
|
|
1546
|
-
<button class="toolbar-
|
|
1547
|
-
|
|
1548
|
-
<
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
<
|
|
1552
|
-
<
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
<!--
|
|
1569
|
-
<div class="toolbar-
|
|
1570
|
-
<button class="toolbar-
|
|
1571
|
-
|
|
1572
|
-
<
|
|
1573
|
-
|
|
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
|
-
<!--
|
|
1557
|
+
<!-- Undo/Redo -->
|
|
1577
1558
|
<div class="toolbar-group">
|
|
1578
|
-
<button class="toolbar-button" id="btn-
|
|
1579
|
-
|
|
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
|
-
<!--
|
|
1585
|
-
<
|
|
1586
|
-
<
|
|
1587
|
-
|
|
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
|
-
<!--
|
|
1593
|
-
<div
|
|
1594
|
-
<button class="toolbar-button" id="btn-
|
|
1595
|
-
|
|
1596
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
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
|
|
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;"
|
|
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;">📂</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 > 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
|
-
|
|
1773
|
-
import {
|
|
1774
|
-
import {
|
|
1775
|
-
import {
|
|
1776
|
-
import {
|
|
1777
|
-
import {
|
|
1778
|
-
import {
|
|
1779
|
-
import {
|
|
1780
|
-
import {
|
|
1781
|
-
import {
|
|
1782
|
-
import {
|
|
1783
|
-
import {
|
|
1784
|
-
import {
|
|
1785
|
-
import {
|
|
1786
|
-
import {
|
|
1787
|
-
import
|
|
1788
|
-
import
|
|
1789
|
-
import {
|
|
1790
|
-
import {
|
|
1762
|
+
const _v = '50';
|
|
1763
|
+
import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, getControls, toggleGrid as vpToggleGrid, fitToObject, fitAll } from './js/viewport.js?v=63';
|
|
1764
|
+
import { startSketch, endSketch, setTool, getEntities, clearSketch } from './js/sketch.js?v=63';
|
|
1765
|
+
import { extrudeProfile, createPrimitive, rebuildFeature, createMaterial } from './js/operations.js?v=63';
|
|
1766
|
+
import { initChat, parseCADPrompt, addMessage } from './js/ai-chat.js?v=63';
|
|
1767
|
+
import { initTree, addFeature, selectFeature, onSelect } from './js/tree.js?v=63';
|
|
1768
|
+
import { initParams, showParams, onParamChange } from './js/params.js?v=63';
|
|
1769
|
+
import { exportSTL, exportOBJ, exportJSON } from './js/export.js?v=63';
|
|
1770
|
+
import { initShortcuts } from './js/shortcuts.js?v=63';
|
|
1771
|
+
import { createReverseEngineerPanel, importFile, analyzeGeometry, reconstructFeatureTree, createWalkthrough } from './js/reverse-engineer.js?v=63';
|
|
1772
|
+
import { createInventorPanel, parseInventorFile } from './js/inventor-parser.js?v=63';
|
|
1773
|
+
import { loadProject, showFolderPicker, parseIPJ } from './js/project-loader.js?v=63';
|
|
1774
|
+
import { initProjectBrowser, showBrowser, hideBrowser, setProject, onFileSelect } from './js/project-browser.js?v=63';
|
|
1775
|
+
import { generateGuide, renderGuide, exportGuideHTML } from './js/rebuild-guide.js?v=63';
|
|
1776
|
+
import { solveConstraints, addConstraint, removeConstraint, autoDetectConstraints, isFullyConstrained, getAllConstraints, clearAllConstraints } from './js/constraint-solver.js?v=63';
|
|
1777
|
+
import { createSweep, createLoft, createBend, createFlange, createTab, createSlot, unfoldSheetMetal, createSpring, createThread } from './js/advanced-ops.js?v=63';
|
|
1778
|
+
import Assembly from './js/assembly.js?v=63';
|
|
1779
|
+
import { exportSketchToDXF, exportProjectionToDXF, exportMultiViewDXF, export3DDXF, downloadDXF } from './js/dxf-export.js?v=63';
|
|
1780
|
+
import { initAgentAPI } from './js/agent-api.js?v=63';
|
|
1781
|
+
import { initTokenDashboard } from './js/token-dashboard.js?v=63';
|
|
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
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1823
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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: () =>
|
|
1918
|
+
fitAll: () => fitAllFeatures(),
|
|
1921
1919
|
save: () => saveProject(),
|
|
1922
1920
|
exportSTL: () => doExportSTL(),
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
// 9. Setup welcome splash
|
|
1926
|
-
setupWelcome();
|
|
1921
|
+
}));
|
|
1927
1922
|
|
|
1928
|
-
//
|
|
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
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
hardRefreshBtn.
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
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
|
-
|
|
1981
|
+
await tryStepAsync('agentAPI', async () => {
|
|
1982
|
+
const agentImports = await import('./js/agent-api.js?v=63');
|
|
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
|
-
|
|
2058
|
-
window.cycleCAD.materials.init();
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
window.cycleCAD.
|
|
2063
|
-
|
|
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
|
-
|
|
2083
|
-
|
|
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', () =>
|
|
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 (
|
|
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
|
|
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
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
mesh
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
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);
|
|
2783
|
+
|
|
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();
|
|
2678
2841
|
|
|
2679
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2774
|
-
|
|
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:
|
|
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
|
|
2983
|
+
function fitAllFeatures() {
|
|
2795
2984
|
if (APP.features.length === 0) {
|
|
2796
|
-
|
|
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:
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
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
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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,8 +3605,36 @@
|
|
|
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?.
|
|
3364
|
-
|
|
3608
|
+
if (mod?.init) {
|
|
3609
|
+
// Use init() which sets innerHTML AND attaches event listeners
|
|
3610
|
+
mod.init(body);
|
|
3611
|
+
} else if (mod?.getUI) {
|
|
3612
|
+
// Strip position:fixed and z-index from module HTML — our wrapper handles positioning
|
|
3613
|
+
let html = mod.getUI();
|
|
3614
|
+
html = html.replace(/position:\s*fixed;?/gi, 'position:relative;')
|
|
3615
|
+
.replace(/z-index:\s*\d+;?/gi, '')
|
|
3616
|
+
.replace(/right:\s*0;?/gi, '')
|
|
3617
|
+
.replace(/top:\s*44px;?/gi, '')
|
|
3618
|
+
.replace(/height:\s*calc\([^)]+\);?/gi, 'height:auto;')
|
|
3619
|
+
.replace(/width:\s*400px;?/gi, 'width:100%;');
|
|
3620
|
+
body.innerHTML = html;
|
|
3621
|
+
// Wire up tab switching for modules that use .tab-btn
|
|
3622
|
+
body.querySelectorAll('.tab-btn').forEach(btn => {
|
|
3623
|
+
btn.addEventListener('click', () => {
|
|
3624
|
+
const tab = btn.dataset.tab;
|
|
3625
|
+
body.querySelectorAll('.tab-btn').forEach(b => {
|
|
3626
|
+
b.style.color = '#888';
|
|
3627
|
+
b.style.borderBottom = 'none';
|
|
3628
|
+
b.classList.remove('active');
|
|
3629
|
+
});
|
|
3630
|
+
btn.style.color = '#58a6ff';
|
|
3631
|
+
btn.style.borderBottom = '2px solid #58a6ff';
|
|
3632
|
+
btn.classList.add('active');
|
|
3633
|
+
body.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
|
|
3634
|
+
const target = body.querySelector(`.tab-content.${tab}`);
|
|
3635
|
+
if (target) target.style.display = 'block';
|
|
3636
|
+
});
|
|
3637
|
+
});
|
|
3365
3638
|
} else if (mod?.getStatus) {
|
|
3366
3639
|
const status = mod.getStatus();
|
|
3367
3640
|
body.innerHTML = `<div style="color:var(--text-secondary);line-height:1.8;">${JSON.stringify(status, null, 2).replace(/\n/g, '<br>')}</div>`;
|
|
@@ -3393,6 +3666,286 @@
|
|
|
3393
3666
|
document.getElementById('btn-generative')?.addEventListener('click', () => toggleModulePanel('generativeDesign', 'generative-panel'));
|
|
3394
3667
|
|
|
3395
3668
|
// ========== Help & Tutorials Panel ==========
|
|
3669
|
+
// ========== Light/Dark Mode Toggle ==========
|
|
3670
|
+
document.getElementById('btn-theme-toggle')?.addEventListener('click', () => {
|
|
3671
|
+
const root = document.documentElement;
|
|
3672
|
+
const isLight = root.classList.toggle('light-mode');
|
|
3673
|
+
const icon = document.getElementById('theme-icon');
|
|
3674
|
+
if (icon) icon.textContent = isLight ? '🌙' : '☀';
|
|
3675
|
+
|
|
3676
|
+
// Update Three.js renderer background
|
|
3677
|
+
try {
|
|
3678
|
+
const scene = getScene();
|
|
3679
|
+
const renderer = window._renderer || document.querySelector('#viewport-container canvas')?.getContext('webgl');
|
|
3680
|
+
if (scene) {
|
|
3681
|
+
if (isLight) {
|
|
3682
|
+
scene.background = new THREE.Color(0xe8e8e8);
|
|
3683
|
+
scene.fog = null;
|
|
3684
|
+
} else {
|
|
3685
|
+
scene.background = new THREE.Color(0x1a1a2e);
|
|
3686
|
+
scene.fog = null;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
} catch(e) { console.warn('Theme: renderer update skipped', e); }
|
|
3690
|
+
|
|
3691
|
+
// Persist preference
|
|
3692
|
+
localStorage.setItem('cyclecad_theme', isLight ? 'light' : 'dark');
|
|
3693
|
+
updateStatus(isLight ? 'Light mode' : 'Dark mode');
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
// Auto-restore saved theme
|
|
3697
|
+
if (localStorage.getItem('cyclecad_theme') === 'light') {
|
|
3698
|
+
document.documentElement.classList.add('light-mode');
|
|
3699
|
+
const icon = document.getElementById('theme-icon');
|
|
3700
|
+
if (icon) icon.textContent = '🌙';
|
|
3701
|
+
try {
|
|
3702
|
+
const scene = getScene();
|
|
3703
|
+
if (scene) scene.background = new THREE.Color(0xe8e8e8);
|
|
3704
|
+
} catch(e) {}
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// ========== ViewCube Navigation ==========
|
|
3708
|
+
document.querySelectorAll('.vc-face').forEach(face => {
|
|
3709
|
+
face.addEventListener('click', () => {
|
|
3710
|
+
const view = face.dataset.vc;
|
|
3711
|
+
if (view) {
|
|
3712
|
+
try { setView(view); } catch(e) {}
|
|
3713
|
+
updateStatus(view.charAt(0).toUpperCase() + view.slice(1) + ' view');
|
|
3714
|
+
}
|
|
3715
|
+
});
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
// Continuously sync ViewCube rotation with camera using rAF
|
|
3719
|
+
(function syncViewCube() {
|
|
3720
|
+
const cube = document.getElementById('vc-cube');
|
|
3721
|
+
if (!cube) return;
|
|
3722
|
+
let lastRotX = null, lastRotY = null;
|
|
3723
|
+
function updateCubeFromCamera() {
|
|
3724
|
+
try {
|
|
3725
|
+
const cam = getCamera();
|
|
3726
|
+
if (cam) {
|
|
3727
|
+
const dir = new THREE.Vector3();
|
|
3728
|
+
cam.getWorldDirection(dir);
|
|
3729
|
+
const rotY = Math.atan2(dir.x, dir.z) * (180 / Math.PI);
|
|
3730
|
+
const rotX = Math.asin(-dir.y) * (180 / Math.PI);
|
|
3731
|
+
const rxi = Math.round(rotX * 10);
|
|
3732
|
+
const ryi = Math.round(rotY * 10);
|
|
3733
|
+
if (rxi !== lastRotX || ryi !== lastRotY) {
|
|
3734
|
+
lastRotX = rxi;
|
|
3735
|
+
lastRotY = ryi;
|
|
3736
|
+
cube.style.transform = `rotateX(${rotX}deg) rotateY(${rotY + 180}deg)`;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
} catch(e) {}
|
|
3740
|
+
requestAnimationFrame(updateCubeFromCamera);
|
|
3741
|
+
}
|
|
3742
|
+
requestAnimationFrame(updateCubeFromCamera);
|
|
3743
|
+
})();
|
|
3744
|
+
|
|
3745
|
+
// ViewCube drag-to-orbit: drag on the cube rotates the 3D camera
|
|
3746
|
+
(function initViewCubeDrag() {
|
|
3747
|
+
const vcEl = document.getElementById('viewcube');
|
|
3748
|
+
if (!vcEl) return;
|
|
3749
|
+
let dragging = false, startX = 0, startY = 0;
|
|
3750
|
+
let startAzimuth = 0, startPolar = 0;
|
|
3751
|
+
|
|
3752
|
+
vcEl.addEventListener('pointerdown', (e) => {
|
|
3753
|
+
// Only start drag if not clicking a face label directly
|
|
3754
|
+
dragging = true;
|
|
3755
|
+
startX = e.clientX;
|
|
3756
|
+
startY = e.clientY;
|
|
3757
|
+
try {
|
|
3758
|
+
const controls = getControls();
|
|
3759
|
+
if (controls) {
|
|
3760
|
+
startAzimuth = controls.getAzimuthalAngle();
|
|
3761
|
+
startPolar = controls.getPolarAngle();
|
|
3762
|
+
}
|
|
3763
|
+
} catch(err) {}
|
|
3764
|
+
vcEl.setPointerCapture(e.pointerId);
|
|
3765
|
+
e.preventDefault();
|
|
3766
|
+
});
|
|
3767
|
+
|
|
3768
|
+
vcEl.addEventListener('pointermove', (e) => {
|
|
3769
|
+
if (!dragging) return;
|
|
3770
|
+
const dx = e.clientX - startX;
|
|
3771
|
+
const dy = e.clientY - startY;
|
|
3772
|
+
// Only orbit if dragged more than 4px (otherwise it's a click)
|
|
3773
|
+
if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
|
|
3774
|
+
try {
|
|
3775
|
+
const controls = getControls();
|
|
3776
|
+
const cam = getCamera();
|
|
3777
|
+
if (controls && cam) {
|
|
3778
|
+
// Spherical orbit: dx → azimuthal, dy → polar
|
|
3779
|
+
const sensitivity = 0.02;
|
|
3780
|
+
const target = controls.target.clone();
|
|
3781
|
+
const offset = cam.position.clone().sub(target);
|
|
3782
|
+
const spherical = new THREE.Spherical().setFromVector3(offset);
|
|
3783
|
+
spherical.theta = startAzimuth - dx * sensitivity;
|
|
3784
|
+
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, startPolar + dy * sensitivity));
|
|
3785
|
+
offset.setFromSpherical(spherical);
|
|
3786
|
+
cam.position.copy(target).add(offset);
|
|
3787
|
+
cam.lookAt(target);
|
|
3788
|
+
controls.update();
|
|
3789
|
+
}
|
|
3790
|
+
} catch(err) {}
|
|
3791
|
+
});
|
|
3792
|
+
|
|
3793
|
+
vcEl.addEventListener('pointerup', (e) => {
|
|
3794
|
+
const dx = Math.abs(e.clientX - startX);
|
|
3795
|
+
const dy = Math.abs(e.clientY - startY);
|
|
3796
|
+
dragging = false;
|
|
3797
|
+
// If it was a small move, let the face click handler fire
|
|
3798
|
+
});
|
|
3799
|
+
})();
|
|
3800
|
+
|
|
3801
|
+
// ========== Measurement Tool ==========
|
|
3802
|
+
(function initMeasureTool() {
|
|
3803
|
+
let measureMode = false;
|
|
3804
|
+
let measurePoints = [];
|
|
3805
|
+
let measureLines = [];
|
|
3806
|
+
let measureLabels = [];
|
|
3807
|
+
|
|
3808
|
+
function clearMeasurements() {
|
|
3809
|
+
const scene = getScene();
|
|
3810
|
+
if (!scene) return;
|
|
3811
|
+
measureLines.forEach(l => scene.remove(l));
|
|
3812
|
+
measureLabels.forEach(l => l.remove());
|
|
3813
|
+
measureLines = [];
|
|
3814
|
+
measureLabels = [];
|
|
3815
|
+
measurePoints = [];
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
function addMeasureLabel(text, position) {
|
|
3819
|
+
const div = document.createElement('div');
|
|
3820
|
+
div.className = 'measure-label';
|
|
3821
|
+
div.textContent = text;
|
|
3822
|
+
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;';
|
|
3823
|
+
document.getElementById('viewport-container').appendChild(div);
|
|
3824
|
+
measureLabels.push(div);
|
|
3825
|
+
|
|
3826
|
+
function updatePos() {
|
|
3827
|
+
if (!div.parentElement) return;
|
|
3828
|
+
try {
|
|
3829
|
+
const cam = getCamera();
|
|
3830
|
+
const container = document.getElementById('viewport-container');
|
|
3831
|
+
if (!cam || !container) return;
|
|
3832
|
+
const vec = position.clone().project(cam);
|
|
3833
|
+
const rect = container.getBoundingClientRect();
|
|
3834
|
+
div.style.left = ((vec.x + 1) / 2 * rect.width) + 'px';
|
|
3835
|
+
div.style.top = ((-vec.y + 1) / 2 * rect.height) + 'px';
|
|
3836
|
+
} catch(e) {}
|
|
3837
|
+
requestAnimationFrame(updatePos);
|
|
3838
|
+
}
|
|
3839
|
+
requestAnimationFrame(updatePos);
|
|
3840
|
+
return div;
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
function onMeasureClick(e) {
|
|
3844
|
+
if (!measureMode) return;
|
|
3845
|
+
const cam = getCamera();
|
|
3846
|
+
const scene = getScene();
|
|
3847
|
+
if (!cam || !scene) return;
|
|
3848
|
+
|
|
3849
|
+
const container = document.getElementById('viewport-container');
|
|
3850
|
+
const rect = container.getBoundingClientRect();
|
|
3851
|
+
const mouse = new THREE.Vector2(
|
|
3852
|
+
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
|
3853
|
+
-((e.clientY - rect.top) / rect.height) * 2 + 1
|
|
3854
|
+
);
|
|
3855
|
+
|
|
3856
|
+
const raycaster = new THREE.Raycaster();
|
|
3857
|
+
raycaster.setFromCamera(mouse, cam);
|
|
3858
|
+
|
|
3859
|
+
const meshes = [];
|
|
3860
|
+
scene.traverse(obj => { if (obj.isMesh) meshes.push(obj); });
|
|
3861
|
+
const hits = raycaster.intersectObjects(meshes, true);
|
|
3862
|
+
if (hits.length === 0) return;
|
|
3863
|
+
|
|
3864
|
+
const pt = hits[0].point.clone();
|
|
3865
|
+
measurePoints.push(pt);
|
|
3866
|
+
|
|
3867
|
+
// Visual marker
|
|
3868
|
+
const marker = new THREE.Mesh(
|
|
3869
|
+
new THREE.SphereGeometry(0.3, 8, 8),
|
|
3870
|
+
new THREE.MeshBasicMaterial({ color: 0x58a6ff })
|
|
3871
|
+
);
|
|
3872
|
+
marker.position.copy(pt);
|
|
3873
|
+
scene.add(marker);
|
|
3874
|
+
measureLines.push(marker);
|
|
3875
|
+
|
|
3876
|
+
if (measurePoints.length === 2) {
|
|
3877
|
+
// Distance measurement
|
|
3878
|
+
const p1 = measurePoints[0], p2 = measurePoints[1];
|
|
3879
|
+
const dist = p1.distanceTo(p2);
|
|
3880
|
+
const dx = Math.abs(p2.x - p1.x);
|
|
3881
|
+
const dy = Math.abs(p2.y - p1.y);
|
|
3882
|
+
const dz = Math.abs(p2.z - p1.z);
|
|
3883
|
+
|
|
3884
|
+
// Draw line
|
|
3885
|
+
const lineGeo = new THREE.BufferGeometry().setFromPoints([p1, p2]);
|
|
3886
|
+
const lineMat = new THREE.LineBasicMaterial({ color: 0x58a6ff, linewidth: 2 });
|
|
3887
|
+
const line = new THREE.Line(lineGeo, lineMat);
|
|
3888
|
+
scene.add(line);
|
|
3889
|
+
measureLines.push(line);
|
|
3890
|
+
|
|
3891
|
+
// Label at midpoint
|
|
3892
|
+
const mid = p1.clone().add(p2).multiplyScalar(0.5);
|
|
3893
|
+
addMeasureLabel(`${dist.toFixed(1)}mm (dx:${dx.toFixed(1)} dy:${dy.toFixed(1)} dz:${dz.toFixed(1)})`, mid);
|
|
3894
|
+
updateStatus(`Distance: ${dist.toFixed(2)}mm`);
|
|
3895
|
+
|
|
3896
|
+
} else if (measurePoints.length === 3) {
|
|
3897
|
+
// Angle measurement between 3 points
|
|
3898
|
+
const p1 = measurePoints[0], p2 = measurePoints[1], p3 = measurePoints[2];
|
|
3899
|
+
const v1 = p1.clone().sub(p2).normalize();
|
|
3900
|
+
const v2 = p3.clone().sub(p2).normalize();
|
|
3901
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, v1.dot(v2)))) * (180 / Math.PI);
|
|
3902
|
+
|
|
3903
|
+
// Draw angle lines
|
|
3904
|
+
const lg1 = new THREE.BufferGeometry().setFromPoints([p1, p2]);
|
|
3905
|
+
const lg2 = new THREE.BufferGeometry().setFromPoints([p2, p3]);
|
|
3906
|
+
const lm = new THREE.LineBasicMaterial({ color: 0x3fb950, linewidth: 2 });
|
|
3907
|
+
const l1 = new THREE.Line(lg1, lm);
|
|
3908
|
+
const l2 = new THREE.Line(lg2, lm);
|
|
3909
|
+
scene.add(l1); scene.add(l2);
|
|
3910
|
+
measureLines.push(l1, l2);
|
|
3911
|
+
|
|
3912
|
+
addMeasureLabel(`${angle.toFixed(1)}°`, p2);
|
|
3913
|
+
updateStatus(`Angle: ${angle.toFixed(2)}° | Distance 1→2: ${p1.distanceTo(p2).toFixed(1)}mm | Distance 2→3: ${p2.distanceTo(p3).toFixed(1)}mm`);
|
|
3914
|
+
measurePoints = []; // Reset after angle
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
// Toggle measure mode with M key or toolbar button
|
|
3919
|
+
document.addEventListener('keydown', (e) => {
|
|
3920
|
+
if (e.key === 'm' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
|
|
3921
|
+
measureMode = !measureMode;
|
|
3922
|
+
if (measureMode) {
|
|
3923
|
+
measurePoints = [];
|
|
3924
|
+
updateStatus('Measure mode: Click 2 points for distance, 3 points for angle. Press M to exit.');
|
|
3925
|
+
document.getElementById('viewport-container').addEventListener('click', onMeasureClick);
|
|
3926
|
+
} else {
|
|
3927
|
+
clearMeasurements();
|
|
3928
|
+
document.getElementById('viewport-container').removeEventListener('click', onMeasureClick);
|
|
3929
|
+
updateStatus('Measure mode off');
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
});
|
|
3933
|
+
|
|
3934
|
+
// Also wire up measure button if it exists
|
|
3935
|
+
document.getElementById('tool-measure')?.addEventListener('click', () => {
|
|
3936
|
+
measureMode = !measureMode;
|
|
3937
|
+
if (measureMode) {
|
|
3938
|
+
measurePoints = [];
|
|
3939
|
+
updateStatus('Measure mode: Click 2 points for distance, 3 for angle. Press M to exit.');
|
|
3940
|
+
document.getElementById('viewport-container').addEventListener('click', onMeasureClick);
|
|
3941
|
+
} else {
|
|
3942
|
+
clearMeasurements();
|
|
3943
|
+
document.getElementById('viewport-container').removeEventListener('click', onMeasureClick);
|
|
3944
|
+
updateStatus('Measure mode off');
|
|
3945
|
+
}
|
|
3946
|
+
});
|
|
3947
|
+
})();
|
|
3948
|
+
|
|
3396
3949
|
document.getElementById('btn-help')?.addEventListener('click', () => {
|
|
3397
3950
|
let panel = document.getElementById('help-tutorials-panel');
|
|
3398
3951
|
if (panel) { panel.style.display = panel.style.display === 'none' ? 'flex' : 'none'; return; }
|
|
@@ -3846,6 +4399,6 @@
|
|
|
3846
4399
|
</div>
|
|
3847
4400
|
</div>
|
|
3848
4401
|
|
|
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.
|
|
4402
|
+
<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.6</span>
|
|
3850
4403
|
</body>
|
|
3851
4404
|
</html>
|