cyclecad 0.1.0
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/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
package/app/js/tree.js
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree.js - Feature tree panel for cycleCAD
|
|
3
|
+
* Manages feature list, selection, and tree operations (rename, delete, suppress, edit)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const FEATURE_ICONS = {
|
|
7
|
+
Origin: '⊕',
|
|
8
|
+
Sketch: '✏️',
|
|
9
|
+
Extrude: '📦',
|
|
10
|
+
Revolve: '🔄',
|
|
11
|
+
Fillet: '⭕',
|
|
12
|
+
Chamfer: '📐',
|
|
13
|
+
Cut: '✂️',
|
|
14
|
+
Union: '∪',
|
|
15
|
+
Box: '□',
|
|
16
|
+
Cylinder: '🔵',
|
|
17
|
+
Sphere: '🔴',
|
|
18
|
+
Pad: '📦',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let treeState = {
|
|
22
|
+
containerEl: null,
|
|
23
|
+
features: [],
|
|
24
|
+
selectedIndex: -1,
|
|
25
|
+
onSelectCallback: null,
|
|
26
|
+
contextMenuTarget: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the feature tree in the left panel
|
|
31
|
+
* @param {HTMLElement} containerEl - Container for the tree
|
|
32
|
+
*/
|
|
33
|
+
export function initTree(containerEl) {
|
|
34
|
+
treeState.containerEl = containerEl;
|
|
35
|
+
treeState.features = [];
|
|
36
|
+
treeState.selectedIndex = -1;
|
|
37
|
+
|
|
38
|
+
// Create tree structure
|
|
39
|
+
containerEl.innerHTML = `
|
|
40
|
+
<div class="tree-panel">
|
|
41
|
+
<div class="tree-header">
|
|
42
|
+
<h3>Features</h3>
|
|
43
|
+
<div class="tree-counter"><span id="feature-count">0</span> features</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="tree-list" id="tree-list"></div>
|
|
46
|
+
<div class="tree-empty" id="tree-empty">
|
|
47
|
+
<p>Start with a sketch</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
// Add styles if not already present
|
|
53
|
+
if (!document.getElementById('tree-styles')) {
|
|
54
|
+
const style = document.createElement('style');
|
|
55
|
+
style.id = 'tree-styles';
|
|
56
|
+
style.textContent = `
|
|
57
|
+
.tree-panel {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
height: 100%;
|
|
61
|
+
background: var(--surface, #1e1e1e);
|
|
62
|
+
color: var(--text, #e0e0e0);
|
|
63
|
+
border-right: 1px solid var(--border, #333);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.tree-header {
|
|
67
|
+
padding: 16px;
|
|
68
|
+
border-bottom: 1px solid var(--border, #333);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.tree-header h3 {
|
|
72
|
+
margin: 0 0 8px 0;
|
|
73
|
+
font-size: 14px;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: var(--text, #e0e0e0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.tree-counter {
|
|
79
|
+
font-size: 12px;
|
|
80
|
+
color: var(--text2, #a0a0a0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.tree-list {
|
|
84
|
+
flex: 1;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
min-height: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tree-list::-webkit-scrollbar {
|
|
90
|
+
width: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tree-list::-webkit-scrollbar-track {
|
|
94
|
+
background: transparent;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.tree-list::-webkit-scrollbar-thumb {
|
|
98
|
+
background: var(--border, #333);
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.tree-list::-webkit-scrollbar-thumb:hover {
|
|
103
|
+
background: var(--text2, #a0a0a0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tree-item {
|
|
107
|
+
padding: 12px 16px;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
border-left: 3px solid transparent;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 10px;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
user-select: none;
|
|
115
|
+
transition: background 0.15s, border-color 0.15s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.tree-item:hover {
|
|
119
|
+
background: rgba(255, 255, 255, 0.05);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.tree-item.selected {
|
|
123
|
+
background: var(--accent, rgba(100, 150, 255, 0.15));
|
|
124
|
+
border-left-color: var(--accent, #6496ff);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.tree-item-icon {
|
|
128
|
+
min-width: 20px;
|
|
129
|
+
text-align: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.tree-item-name {
|
|
133
|
+
flex: 1;
|
|
134
|
+
white-space: nowrap;
|
|
135
|
+
overflow: hidden;
|
|
136
|
+
text-overflow: ellipsis;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.tree-item-menu {
|
|
140
|
+
opacity: 0;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
padding: 4px;
|
|
144
|
+
transition: opacity 0.15s;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.tree-item:hover .tree-item-menu {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.tree-empty {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
height: 100%;
|
|
156
|
+
color: var(--text2, #a0a0a0);
|
|
157
|
+
font-size: 13px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.tree-context-menu {
|
|
161
|
+
position: fixed;
|
|
162
|
+
background: var(--surface, #1e1e1e);
|
|
163
|
+
border: 1px solid var(--border, #333);
|
|
164
|
+
border-radius: 4px;
|
|
165
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
166
|
+
z-index: 10000;
|
|
167
|
+
min-width: 160px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.tree-context-item {
|
|
171
|
+
padding: 8px 16px;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
color: var(--text, #e0e0e0);
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 8px;
|
|
178
|
+
transition: background 0.15s;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tree-context-item:hover {
|
|
182
|
+
background: var(--accent, rgba(100, 150, 255, 0.2));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.tree-context-item.danger {
|
|
186
|
+
color: #ff6b6b;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.tree-context-item.danger:hover {
|
|
190
|
+
background: rgba(255, 107, 107, 0.15);
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
document.head.appendChild(style);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Attach event listeners
|
|
197
|
+
attachTreeListeners();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add a feature to the tree
|
|
202
|
+
* @param {Object} feature - Feature object { type, name, icon, params, mesh }
|
|
203
|
+
*/
|
|
204
|
+
export function addFeature(feature) {
|
|
205
|
+
if (!feature.type || !feature.name) {
|
|
206
|
+
console.warn('Feature missing type or name', feature);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
feature.icon = feature.icon || FEATURE_ICONS[feature.type] || '?';
|
|
211
|
+
feature.suppressed = false;
|
|
212
|
+
feature.mesh = feature.mesh || null;
|
|
213
|
+
feature.params = feature.params || {};
|
|
214
|
+
|
|
215
|
+
treeState.features.push(feature);
|
|
216
|
+
renderTree();
|
|
217
|
+
updateFeatureCount();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Remove a feature from the tree
|
|
222
|
+
* @param {number} index - Feature index
|
|
223
|
+
*/
|
|
224
|
+
export function removeFeature(index) {
|
|
225
|
+
if (index < 0 || index >= treeState.features.length) {
|
|
226
|
+
console.warn('Invalid feature index', index);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
treeState.features.splice(index, 1);
|
|
231
|
+
|
|
232
|
+
// Adjust selected index if needed
|
|
233
|
+
if (treeState.selectedIndex >= treeState.features.length) {
|
|
234
|
+
treeState.selectedIndex = treeState.features.length - 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (treeState.selectedIndex < 0) {
|
|
238
|
+
treeState.selectedIndex = -1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
renderTree();
|
|
242
|
+
updateFeatureCount();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Select a feature in the tree
|
|
247
|
+
* @param {number} index - Feature index
|
|
248
|
+
* @returns {Object} Selected feature or null
|
|
249
|
+
*/
|
|
250
|
+
export function selectFeature(index) {
|
|
251
|
+
if (index < 0 || index >= treeState.features.length) {
|
|
252
|
+
treeState.selectedIndex = -1;
|
|
253
|
+
renderTree();
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
treeState.selectedIndex = index;
|
|
258
|
+
renderTree();
|
|
259
|
+
|
|
260
|
+
if (treeState.onSelectCallback) {
|
|
261
|
+
treeState.onSelectCallback(treeState.features[index], index);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return treeState.features[index];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get all features
|
|
269
|
+
* @returns {Array} Array of feature objects
|
|
270
|
+
*/
|
|
271
|
+
export function getFeatures() {
|
|
272
|
+
return [...treeState.features];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get currently selected feature
|
|
277
|
+
* @returns {Object|null} Selected feature or null
|
|
278
|
+
*/
|
|
279
|
+
export function getSelectedFeature() {
|
|
280
|
+
if (treeState.selectedIndex < 0) return null;
|
|
281
|
+
return treeState.features[treeState.selectedIndex];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Update the feature counter in the header
|
|
286
|
+
*/
|
|
287
|
+
export function updateFeatureCount() {
|
|
288
|
+
const countEl = document.getElementById('feature-count');
|
|
289
|
+
if (countEl) {
|
|
290
|
+
countEl.textContent = treeState.features.length;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Register a callback when a feature is selected
|
|
296
|
+
* @param {Function} callback - Called with (feature, index)
|
|
297
|
+
*/
|
|
298
|
+
export function onSelect(callback) {
|
|
299
|
+
treeState.onSelectCallback = callback;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Suppress/unsuppress a feature
|
|
304
|
+
* @param {number} index - Feature index
|
|
305
|
+
*/
|
|
306
|
+
export function suppressFeature(index) {
|
|
307
|
+
if (index >= 0 && index < treeState.features.length) {
|
|
308
|
+
treeState.features[index].suppressed = !treeState.features[index].suppressed;
|
|
309
|
+
renderTree();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Rename a feature
|
|
315
|
+
* @param {number} index - Feature index
|
|
316
|
+
* @param {string} newName - New name
|
|
317
|
+
*/
|
|
318
|
+
export function renameFeature(index, newName) {
|
|
319
|
+
if (index >= 0 && index < treeState.features.length && newName.trim()) {
|
|
320
|
+
treeState.features[index].name = newName.trim();
|
|
321
|
+
renderTree();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Internal: Render the tree list
|
|
327
|
+
*/
|
|
328
|
+
function renderTree() {
|
|
329
|
+
const treeList = document.getElementById('tree-list');
|
|
330
|
+
const treeEmpty = document.getElementById('tree-empty');
|
|
331
|
+
|
|
332
|
+
if (!treeList) return;
|
|
333
|
+
|
|
334
|
+
if (treeState.features.length === 0) {
|
|
335
|
+
treeList.innerHTML = '';
|
|
336
|
+
if (treeEmpty) treeEmpty.style.display = 'flex';
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (treeEmpty) treeEmpty.style.display = 'none';
|
|
341
|
+
|
|
342
|
+
treeList.innerHTML = treeState.features
|
|
343
|
+
.map(
|
|
344
|
+
(feature, idx) => `
|
|
345
|
+
<div class="tree-item ${idx === treeState.selectedIndex ? 'selected' : ''}"
|
|
346
|
+
data-index="${idx}">
|
|
347
|
+
<div class="tree-item-icon">${feature.icon}</div>
|
|
348
|
+
<div class="tree-item-name" title="${feature.name}">
|
|
349
|
+
${feature.suppressed ? '🚫 ' : ''}${feature.name}
|
|
350
|
+
</div>
|
|
351
|
+
<div class="tree-item-menu" data-menu="${idx}">⋮</div>
|
|
352
|
+
</div>
|
|
353
|
+
`
|
|
354
|
+
)
|
|
355
|
+
.join('');
|
|
356
|
+
|
|
357
|
+
attachTreeListeners();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Internal: Attach event listeners to tree items
|
|
362
|
+
*/
|
|
363
|
+
function attachTreeListeners() {
|
|
364
|
+
const treeItems = document.querySelectorAll('.tree-item');
|
|
365
|
+
|
|
366
|
+
treeItems.forEach((item) => {
|
|
367
|
+
// Select on click
|
|
368
|
+
item.addEventListener('click', (e) => {
|
|
369
|
+
if (e.target.closest('.tree-item-menu')) return;
|
|
370
|
+
const idx = parseInt(item.dataset.index);
|
|
371
|
+
selectFeature(idx);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Context menu on right-click or menu button click
|
|
375
|
+
item.addEventListener('contextmenu', (e) => {
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
const idx = parseInt(item.dataset.index);
|
|
378
|
+
showContextMenu(idx, e.clientX, e.clientY);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const menuBtn = item.querySelector('.tree-item-menu');
|
|
382
|
+
if (menuBtn) {
|
|
383
|
+
menuBtn.addEventListener('click', (e) => {
|
|
384
|
+
e.stopPropagation();
|
|
385
|
+
const idx = parseInt(item.dataset.index);
|
|
386
|
+
const rect = menuBtn.getBoundingClientRect();
|
|
387
|
+
showContextMenu(idx, rect.right - 10, rect.bottom + 5);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Internal: Show context menu
|
|
395
|
+
*/
|
|
396
|
+
function showContextMenu(index, x, y) {
|
|
397
|
+
if (index < 0 || index >= treeState.features.length) return;
|
|
398
|
+
|
|
399
|
+
// Remove existing context menu
|
|
400
|
+
const existing = document.querySelector('.tree-context-menu');
|
|
401
|
+
if (existing) existing.remove();
|
|
402
|
+
|
|
403
|
+
const feature = treeState.features[index];
|
|
404
|
+
const menu = document.createElement('div');
|
|
405
|
+
menu.className = 'tree-context-menu';
|
|
406
|
+
menu.style.left = x + 'px';
|
|
407
|
+
menu.style.top = y + 'px';
|
|
408
|
+
|
|
409
|
+
menu.innerHTML = `
|
|
410
|
+
<div class="tree-context-item" data-action="rename">
|
|
411
|
+
✏️ Rename
|
|
412
|
+
</div>
|
|
413
|
+
<div class="tree-context-item" data-action="edit-sketch" ${feature.type !== 'Sketch' ? 'style="opacity:0.5; cursor:not-allowed;"' : ''}>
|
|
414
|
+
📐 Edit Sketch
|
|
415
|
+
</div>
|
|
416
|
+
<div class="tree-context-item" data-action="suppress">
|
|
417
|
+
${feature.suppressed ? '✓' : '⊘'} ${feature.suppressed ? 'Unsuppress' : 'Suppress'}
|
|
418
|
+
</div>
|
|
419
|
+
<div class="tree-context-item danger" data-action="delete">
|
|
420
|
+
🗑️ Delete
|
|
421
|
+
</div>
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
document.body.appendChild(menu);
|
|
425
|
+
|
|
426
|
+
// Close menu on click outside
|
|
427
|
+
const closeMenu = () => {
|
|
428
|
+
menu.remove();
|
|
429
|
+
document.removeEventListener('click', closeMenu);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
setTimeout(() => {
|
|
433
|
+
document.addEventListener('click', closeMenu);
|
|
434
|
+
}, 0);
|
|
435
|
+
|
|
436
|
+
// Handle menu actions
|
|
437
|
+
menu.querySelectorAll('.tree-context-item').forEach((item) => {
|
|
438
|
+
item.addEventListener('click', () => {
|
|
439
|
+
const action = item.dataset.action;
|
|
440
|
+
|
|
441
|
+
if (action === 'rename') {
|
|
442
|
+
const newName = prompt('Enter new name:', feature.name);
|
|
443
|
+
if (newName !== null) {
|
|
444
|
+
renameFeature(index, newName);
|
|
445
|
+
}
|
|
446
|
+
} else if (action === 'edit-sketch') {
|
|
447
|
+
if (feature.type === 'Sketch') {
|
|
448
|
+
// Dispatch custom event for sketch editor
|
|
449
|
+
window.dispatchEvent(
|
|
450
|
+
new CustomEvent('cyclecad:edit-sketch', {
|
|
451
|
+
detail: { index, feature },
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
} else if (action === 'suppress') {
|
|
456
|
+
suppressFeature(index);
|
|
457
|
+
} else if (action === 'delete') {
|
|
458
|
+
if (confirm(`Delete "${feature.name}"?`)) {
|
|
459
|
+
removeFeature(index);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
menu.remove();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export default {
|
|
469
|
+
initTree,
|
|
470
|
+
addFeature,
|
|
471
|
+
removeFeature,
|
|
472
|
+
selectFeature,
|
|
473
|
+
getFeatures,
|
|
474
|
+
getSelectedFeature,
|
|
475
|
+
updateFeatureCount,
|
|
476
|
+
onSelect,
|
|
477
|
+
suppressFeature,
|
|
478
|
+
renameFeature,
|
|
479
|
+
};
|