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