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