cyclecad 2.0.1 → 3.0.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/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- package/.github/workflows/cad-diff.yml +0 -117
package/app/js/app.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cycleCAD
|
|
3
|
-
*
|
|
2
|
+
* cycleCAD — Main Application Controller
|
|
3
|
+
* Comprehensive application state manager, workspace switching, document handling,
|
|
4
|
+
* undo/redo, preferences, notifications, and global event management.
|
|
5
|
+
*
|
|
6
|
+
* Version: 1.0.0 (Enterprise-grade CAD platform)
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { initViewport, setView, addToScene, removeFromScene, getScene, getCamera, toggleGrid as vpToggleGrid, toggleWireframe as vpToggleWireframe, fitToObject } from './viewport.js';
|
|
@@ -8,392 +11,1298 @@ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.m
|
|
|
8
11
|
import { startSketch, endSketch, setTool, getEntities } from './sketch.js';
|
|
9
12
|
import { extrudeProfile, createPrimitive, rebuildFeature } from './operations.js';
|
|
10
13
|
import { initChat, parseCADPrompt } from './ai-chat.js';
|
|
11
|
-
import { initTree, addFeature, selectFeature, onSelect } from './tree.js';
|
|
14
|
+
import { initTree, addFeature, selectFeature, onSelect, removeFeature } from './tree.js';
|
|
12
15
|
import { initParams, showParams, onParamChange } from './params.js';
|
|
13
16
|
import { exportSTL, exportOBJ, exportJSON } from './export.js';
|
|
14
17
|
import { initShortcuts, showShortcutsPanel } from './shortcuts.js';
|
|
15
18
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// APPLICATION STATE — Core data model
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Main application state object
|
|
25
|
+
* @type {Object}
|
|
26
|
+
*/
|
|
27
|
+
export const APP = {
|
|
28
|
+
// Core state
|
|
29
|
+
mode: 'idle', // idle | sketch | extrude | operation | render | animation | simulation | manufacture | drawing
|
|
30
|
+
currentWorkspace: 'design', // design | render | animation | simulation | manufacture | drawing
|
|
31
|
+
currentTool: null,
|
|
32
|
+
|
|
33
|
+
// Feature data
|
|
34
|
+
features: [],
|
|
20
35
|
selectedFeature: null,
|
|
21
36
|
selectedEntity: null,
|
|
37
|
+
currentSketch: null,
|
|
38
|
+
clipboard: [],
|
|
39
|
+
|
|
40
|
+
// History and undo/redo
|
|
22
41
|
history: [],
|
|
23
42
|
historyIndex: -1,
|
|
24
|
-
|
|
43
|
+
maxHistorySize: 100,
|
|
44
|
+
|
|
45
|
+
// Document metadata
|
|
46
|
+
document: {
|
|
47
|
+
name: 'Untitled',
|
|
48
|
+
units: 'mm',
|
|
49
|
+
author: '',
|
|
50
|
+
created: new Date(),
|
|
51
|
+
modified: new Date(),
|
|
52
|
+
version: 1,
|
|
53
|
+
filepath: null,
|
|
54
|
+
unsavedChanges: false,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// User preferences
|
|
58
|
+
preferences: {
|
|
59
|
+
theme: 'dark', // dark | light
|
|
60
|
+
gridSnap: true,
|
|
61
|
+
gridSize: 10,
|
|
62
|
+
selectionMode: 'single', // single | multi | box
|
|
63
|
+
language: 'en',
|
|
64
|
+
autoSave: true,
|
|
65
|
+
autoSaveInterval: 300000, // 5 minutes
|
|
66
|
+
performanceMode: 'balanced', // balanced | quality | performance
|
|
67
|
+
showHelpOnStartup: true,
|
|
68
|
+
keyboardShortcuts: {},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// UI state
|
|
72
|
+
uiState: {
|
|
73
|
+
panelLayout: 'default', // default | minimal | fullscreen
|
|
74
|
+
leftPanelWidth: 280,
|
|
75
|
+
rightPanelWidth: 320,
|
|
76
|
+
bottomPanelHeight: 200,
|
|
77
|
+
showStatusBar: true,
|
|
78
|
+
showToolbar: true,
|
|
79
|
+
showCommandPalette: false,
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Scene references
|
|
25
83
|
scene: null,
|
|
26
84
|
camera: null,
|
|
85
|
+
renderer: null,
|
|
86
|
+
|
|
87
|
+
// Performance metrics
|
|
88
|
+
metrics: {
|
|
89
|
+
fps: 60,
|
|
90
|
+
frameTime: 0,
|
|
91
|
+
meshCount: 0,
|
|
92
|
+
vertexCount: 0,
|
|
93
|
+
memoryUsage: 0,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Recent files
|
|
97
|
+
recentFiles: [],
|
|
98
|
+
maxRecentFiles: 10,
|
|
27
99
|
};
|
|
28
100
|
|
|
101
|
+
// Event system for pub/sub
|
|
102
|
+
const eventListeners = {};
|
|
103
|
+
|
|
29
104
|
/**
|
|
30
|
-
*
|
|
105
|
+
* Subscribe to application events
|
|
106
|
+
* @param {string} eventName - Event name (e.g., 'selection:changed', 'history:pushed', 'document:saved')
|
|
107
|
+
* @param {Function} callback - Callback function
|
|
31
108
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
109
|
+
export function on(eventName, callback) {
|
|
110
|
+
if (!eventListeners[eventName]) {
|
|
111
|
+
eventListeners[eventName] = [];
|
|
112
|
+
}
|
|
113
|
+
eventListeners[eventName].push(callback);
|
|
114
|
+
}
|
|
34
115
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Unsubscribe from application events
|
|
118
|
+
* @param {string} eventName
|
|
119
|
+
* @param {Function} callback
|
|
120
|
+
*/
|
|
121
|
+
export function off(eventName, callback) {
|
|
122
|
+
if (!eventListeners[eventName]) return;
|
|
123
|
+
eventListeners[eventName] = eventListeners[eventName].filter(cb => cb !== callback);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Emit application event
|
|
128
|
+
* @param {string} eventName
|
|
129
|
+
* @param {*} data
|
|
130
|
+
*/
|
|
131
|
+
function emit(eventName, data) {
|
|
132
|
+
if (!eventListeners[eventName]) return;
|
|
133
|
+
eventListeners[eventName].forEach(callback => {
|
|
134
|
+
try {
|
|
135
|
+
callback(data);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(`Event handler error for ${eventName}:`, err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// WORKSPACE MANAGER — Switch between workspaces
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
const WORKSPACES = {
|
|
147
|
+
design: {
|
|
148
|
+
name: 'Design',
|
|
149
|
+
toolbars: ['file', 'sketch', 'operations', 'assembly', 'view', 'help'],
|
|
150
|
+
activeModules: ['viewport', 'sketch', 'operations', 'tree', 'params'],
|
|
151
|
+
},
|
|
152
|
+
render: {
|
|
153
|
+
name: 'Render',
|
|
154
|
+
toolbars: ['file', 'materials', 'lighting', 'render', 'view', 'help'],
|
|
155
|
+
activeModules: ['viewport', 'materials', 'lighting'],
|
|
156
|
+
},
|
|
157
|
+
animation: {
|
|
158
|
+
name: 'Animation',
|
|
159
|
+
toolbars: ['file', 'timeline', 'animation', 'keyframe', 'view', 'help'],
|
|
160
|
+
activeModules: ['viewport', 'timeline', 'animation'],
|
|
161
|
+
},
|
|
162
|
+
simulation: {
|
|
163
|
+
name: 'Simulation',
|
|
164
|
+
toolbars: ['file', 'simulation', 'solver', 'results', 'view', 'help'],
|
|
165
|
+
activeModules: ['viewport', 'simulation', 'solver'],
|
|
166
|
+
},
|
|
167
|
+
manufacture: {
|
|
168
|
+
name: 'Manufacture',
|
|
169
|
+
toolbars: ['file', 'cam', 'toolpaths', 'tooling', 'view', 'help'],
|
|
170
|
+
activeModules: ['viewport', 'cam', 'toolpaths'],
|
|
171
|
+
},
|
|
172
|
+
drawing: {
|
|
173
|
+
name: 'Drawing',
|
|
174
|
+
toolbars: ['file', 'views', 'dimensions', 'annotations', 'view', 'help'],
|
|
175
|
+
activeModules: ['viewport', 'drawing', 'dimensions'],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Switch to a different workspace
|
|
181
|
+
* @param {string} workspaceName - Name of workspace (design, render, animation, simulation, manufacture, drawing)
|
|
182
|
+
*/
|
|
183
|
+
export function switchWorkspace(workspaceName) {
|
|
184
|
+
if (!WORKSPACES[workspaceName]) {
|
|
185
|
+
console.warn(`Unknown workspace: ${workspaceName}`);
|
|
39
186
|
return;
|
|
40
187
|
}
|
|
41
188
|
|
|
42
|
-
|
|
43
|
-
APP.
|
|
44
|
-
APP.camera = getCamera();
|
|
189
|
+
const workspace = WORKSPACES[workspaceName];
|
|
190
|
+
APP.currentWorkspace = workspaceName;
|
|
45
191
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
192
|
+
// Update toolbar visibility
|
|
193
|
+
const toolbar = document.getElementById('toolbar');
|
|
194
|
+
if (toolbar) {
|
|
195
|
+
const toolbarGroups = toolbar.querySelectorAll('[data-toolbar-group]');
|
|
196
|
+
toolbarGroups.forEach(group => {
|
|
197
|
+
const groupName = group.getAttribute('data-toolbar-group');
|
|
198
|
+
group.style.display = workspace.toolbars.includes(groupName) ? 'flex' : 'none';
|
|
199
|
+
});
|
|
200
|
+
}
|
|
50
201
|
|
|
51
|
-
//
|
|
52
|
-
|
|
202
|
+
// Update status bar
|
|
203
|
+
updateStatusBar(`Switched to ${workspace.name} workspace`);
|
|
204
|
+
emit('workspace:changed', workspaceName);
|
|
53
205
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (APP.selectedFeature) {
|
|
58
|
-
showParams(APP.selectedFeature);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
206
|
+
// Fit view after workspace change
|
|
207
|
+
setTimeout(() => fitAll(), 100);
|
|
208
|
+
}
|
|
61
209
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// DOCUMENT MANAGER — File operations
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a new document with optional template
|
|
216
|
+
* @param {string} [templateName] - Optional template name
|
|
217
|
+
*/
|
|
218
|
+
export function newDocument(templateName) {
|
|
219
|
+
if (APP.document.unsavedChanges) {
|
|
220
|
+
if (!confirm('Discard unsaved changes?')) return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Clear current document
|
|
224
|
+
APP.features.forEach(f => {
|
|
225
|
+
if (f.mesh) removeFromScene(f.mesh);
|
|
69
226
|
});
|
|
70
227
|
|
|
71
|
-
|
|
72
|
-
|
|
228
|
+
APP.features = [];
|
|
229
|
+
APP.history = [];
|
|
230
|
+
APP.historyIndex = -1;
|
|
231
|
+
APP.selectedFeature = null;
|
|
232
|
+
APP.selectedEntity = null;
|
|
233
|
+
APP.currentSketch = null;
|
|
234
|
+
|
|
235
|
+
// Initialize new document
|
|
236
|
+
APP.document = {
|
|
237
|
+
name: templateName ? `${templateName} Document` : 'Untitled',
|
|
238
|
+
units: 'mm',
|
|
239
|
+
author: APP.preferences.author || '',
|
|
240
|
+
created: new Date(),
|
|
241
|
+
modified: new Date(),
|
|
242
|
+
version: 1,
|
|
243
|
+
filepath: null,
|
|
244
|
+
unsavedChanges: false,
|
|
245
|
+
};
|
|
73
246
|
|
|
74
|
-
|
|
75
|
-
|
|
247
|
+
updateWindowTitle();
|
|
248
|
+
updateStatusBar('New document created');
|
|
249
|
+
emit('document:created', APP.document);
|
|
250
|
+
}
|
|
76
251
|
|
|
77
|
-
|
|
78
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Open a document from file
|
|
254
|
+
*/
|
|
255
|
+
export function openDocument() {
|
|
256
|
+
if (APP.document.unsavedChanges) {
|
|
257
|
+
if (!confirm('Discard unsaved changes?')) return;
|
|
258
|
+
}
|
|
79
259
|
|
|
80
|
-
|
|
260
|
+
const input = document.createElement('input');
|
|
261
|
+
input.type = 'file';
|
|
262
|
+
input.accept = '.json,.ccad';
|
|
263
|
+
input.onchange = (e) => {
|
|
264
|
+
const file = e.target.files[0];
|
|
265
|
+
if (file) {
|
|
266
|
+
const reader = new FileReader();
|
|
267
|
+
reader.onload = (event) => {
|
|
268
|
+
try {
|
|
269
|
+
const data = JSON.parse(event.target.result);
|
|
270
|
+
loadDocumentData(data, file.name);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
showNotification(`Failed to load: ${err.message}`, 'error');
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
reader.readAsText(file);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
input.click();
|
|
81
279
|
}
|
|
82
280
|
|
|
83
281
|
/**
|
|
84
|
-
*
|
|
85
|
-
* @
|
|
282
|
+
* Load document data
|
|
283
|
+
* @param {Object} data - Document data
|
|
284
|
+
* @param {string} [filename] - Original filename
|
|
86
285
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
line: () => setTool('line'),
|
|
96
|
-
rect: () => setTool('rect'),
|
|
97
|
-
circle: () => setTool('circle'),
|
|
98
|
-
arc: () => setTool('arc'),
|
|
99
|
-
dimension: () => setTool('dimension'),
|
|
100
|
-
|
|
101
|
-
// 3D operations
|
|
102
|
-
extrude: () => {
|
|
103
|
-
if (APP.currentSketch && APP.mode === 'sketch') {
|
|
104
|
-
startExtrude();
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
revolve: () => console.log('Revolve not yet implemented'),
|
|
108
|
-
fillet: () => console.log('Fillet not yet implemented'),
|
|
109
|
-
chamfer: () => console.log('Chamfer not yet implemented'),
|
|
286
|
+
function loadDocumentData(data, filename) {
|
|
287
|
+
try {
|
|
288
|
+
// Clear current
|
|
289
|
+
APP.features.forEach(f => {
|
|
290
|
+
if (f.mesh) removeFromScene(f.mesh);
|
|
291
|
+
});
|
|
110
292
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
293
|
+
APP.features = [];
|
|
294
|
+
APP.history = [];
|
|
295
|
+
APP.historyIndex = -1;
|
|
114
296
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
297
|
+
// Restore document metadata
|
|
298
|
+
APP.document = {
|
|
299
|
+
name: data.name || filename || 'Loaded',
|
|
300
|
+
units: data.units || 'mm',
|
|
301
|
+
author: data.author || '',
|
|
302
|
+
created: data.created ? new Date(data.created) : new Date(),
|
|
303
|
+
modified: data.modified ? new Date(data.modified) : new Date(),
|
|
304
|
+
version: data.version || 1,
|
|
305
|
+
filepath: null,
|
|
306
|
+
unsavedChanges: false,
|
|
307
|
+
};
|
|
121
308
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
viewBottom: () => setView('bottom'),
|
|
129
|
-
viewIso: () => setView('iso'),
|
|
309
|
+
// Rebuild features
|
|
310
|
+
if (data.features && Array.isArray(data.features)) {
|
|
311
|
+
data.features.forEach((featureData) => {
|
|
312
|
+
try {
|
|
313
|
+
const primitive = createPrimitive(featureData.type, featureData.params);
|
|
314
|
+
addToScene(primitive.mesh);
|
|
130
315
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
316
|
+
const feature = {
|
|
317
|
+
id: featureData.id || `feature_${Date.now()}`,
|
|
318
|
+
name: featureData.name,
|
|
319
|
+
type: featureData.type,
|
|
320
|
+
mesh: primitive.mesh,
|
|
321
|
+
params: featureData.params,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
APP.features.push(feature);
|
|
325
|
+
addFeature(feature);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.warn(`Failed to load feature ${featureData.name}:`, err);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
135
331
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
332
|
+
pushHistory();
|
|
333
|
+
addToRecentFiles(APP.document.name);
|
|
334
|
+
updateWindowTitle();
|
|
335
|
+
updateStatusBar(`Loaded ${APP.features.length} features`);
|
|
336
|
+
emit('document:loaded', APP.document);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error('Load failed:', err);
|
|
339
|
+
showNotification(`Load failed: ${err.message}`, 'error');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
139
342
|
|
|
140
|
-
|
|
141
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Save current document
|
|
345
|
+
*/
|
|
346
|
+
export function saveDocument() {
|
|
347
|
+
const data = {
|
|
348
|
+
version: '1.0',
|
|
349
|
+
name: APP.document.name,
|
|
350
|
+
units: APP.document.units,
|
|
351
|
+
author: APP.document.author,
|
|
352
|
+
created: APP.document.created.toISOString(),
|
|
353
|
+
modified: new Date().toISOString(),
|
|
354
|
+
features: APP.features.map((f) => ({
|
|
355
|
+
id: f.id,
|
|
356
|
+
name: f.name,
|
|
357
|
+
type: f.type,
|
|
358
|
+
params: f.params,
|
|
359
|
+
})),
|
|
142
360
|
};
|
|
361
|
+
|
|
362
|
+
const json = JSON.stringify(data, null, 2);
|
|
363
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
364
|
+
const url = URL.createObjectURL(blob);
|
|
365
|
+
const a = document.createElement('a');
|
|
366
|
+
a.href = url;
|
|
367
|
+
a.download = `${APP.document.name}_${new Date().toISOString().split('T')[0]}.ccad`;
|
|
368
|
+
a.click();
|
|
369
|
+
URL.revokeObjectURL(url);
|
|
370
|
+
|
|
371
|
+
APP.document.unsavedChanges = false;
|
|
372
|
+
APP.document.modified = new Date();
|
|
373
|
+
updateWindowTitle();
|
|
374
|
+
updateStatusBar('Document saved');
|
|
375
|
+
emit('document:saved', APP.document);
|
|
143
376
|
}
|
|
144
377
|
|
|
145
378
|
/**
|
|
146
|
-
*
|
|
379
|
+
* Add filename to recent files list
|
|
380
|
+
* @param {string} filename
|
|
147
381
|
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
// File buttons
|
|
153
|
-
const newBtn = document.getElementById('btn-new');
|
|
154
|
-
if (newBtn) {
|
|
155
|
-
newBtn.onclick = () => {
|
|
156
|
-
if (confirm('Start a new project? Unsaved changes will be lost.')) {
|
|
157
|
-
APP.features = [];
|
|
158
|
-
APP.history = [];
|
|
159
|
-
APP.historyIndex = -1;
|
|
160
|
-
updateScene();
|
|
161
|
-
}
|
|
162
|
-
};
|
|
382
|
+
function addToRecentFiles(filename) {
|
|
383
|
+
APP.recentFiles = [filename, ...APP.recentFiles.filter(f => f !== filename)];
|
|
384
|
+
if (APP.recentFiles.length > APP.maxRecentFiles) {
|
|
385
|
+
APP.recentFiles = APP.recentFiles.slice(0, APP.maxRecentFiles);
|
|
163
386
|
}
|
|
387
|
+
localStorage.setItem('cyclecad_recent_files', JSON.stringify(APP.recentFiles));
|
|
388
|
+
}
|
|
164
389
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const data = JSON.parse(event.target.result);
|
|
178
|
-
loadProject(data);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
alert('Failed to load project: ' + err.message);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
reader.readAsText(file);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
input.click();
|
|
187
|
-
};
|
|
390
|
+
/**
|
|
391
|
+
* Load recent files from storage
|
|
392
|
+
*/
|
|
393
|
+
function loadRecentFiles() {
|
|
394
|
+
const stored = localStorage.getItem('cyclecad_recent_files');
|
|
395
|
+
if (stored) {
|
|
396
|
+
try {
|
|
397
|
+
APP.recentFiles = JSON.parse(stored);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
APP.recentFiles = [];
|
|
400
|
+
}
|
|
188
401
|
}
|
|
402
|
+
}
|
|
189
403
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// SELECTION MANAGER — Part selection and filtering
|
|
406
|
+
// ============================================================================
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Select a feature
|
|
410
|
+
* @param {string} featureId - Feature ID
|
|
411
|
+
* @param {Object} [options] - Selection options
|
|
412
|
+
*/
|
|
413
|
+
export function selectFeatureById(featureId, options = {}) {
|
|
414
|
+
const { multiSelect = false, silent = false } = options;
|
|
415
|
+
|
|
416
|
+
const feature = APP.features.find(f => f.id === featureId);
|
|
417
|
+
if (!feature) {
|
|
418
|
+
console.warn(`Feature not found: ${featureId}`);
|
|
419
|
+
return;
|
|
193
420
|
}
|
|
194
421
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (exportSTLBtn) {
|
|
198
|
-
exportSTLBtn.onclick = () => showExportDialog('stl');
|
|
422
|
+
if (!multiSelect) {
|
|
423
|
+
APP.selectedFeature = feature;
|
|
199
424
|
}
|
|
200
425
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
426
|
+
if (!silent) {
|
|
427
|
+
showParams(feature);
|
|
428
|
+
updateStatusBar(`Selected: ${feature.name}`);
|
|
429
|
+
emit('selection:changed', { feature, multiSelect });
|
|
204
430
|
}
|
|
431
|
+
}
|
|
205
432
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Clear selection
|
|
435
|
+
*/
|
|
436
|
+
export function clearSelection() {
|
|
437
|
+
APP.selectedFeature = null;
|
|
438
|
+
APP.selectedEntity = null;
|
|
439
|
+
updateStatusBar('Selection cleared');
|
|
440
|
+
emit('selection:cleared', null);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Select multiple features by box (rect selection)
|
|
445
|
+
* @param {THREE.Vector3} start - Start point
|
|
446
|
+
* @param {THREE.Vector3} end - End point
|
|
447
|
+
*/
|
|
448
|
+
export function boxSelectFeatures(start, end) {
|
|
449
|
+
const box = new THREE.Box3(start, end);
|
|
450
|
+
|
|
451
|
+
APP.features.forEach(feature => {
|
|
452
|
+
if (feature.mesh) {
|
|
453
|
+
const featureBox = new THREE.Box3().setFromObject(feature.mesh);
|
|
454
|
+
if (box.intersectsBox(featureBox)) {
|
|
455
|
+
selectFeatureById(feature.id, { multiSelect: true, silent: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
emit('selection:changed', { features: [APP.selectedFeature], boxSelect: true });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// UNDO/REDO SYSTEM — History management
|
|
465
|
+
// ============================================================================
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Push current state to undo/redo stack
|
|
469
|
+
*/
|
|
470
|
+
export function pushHistory() {
|
|
471
|
+
// Trim redo stack if not at end
|
|
472
|
+
if (APP.historyIndex < APP.history.length - 1) {
|
|
473
|
+
APP.history = APP.history.slice(0, APP.historyIndex + 1);
|
|
210
474
|
}
|
|
211
475
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
476
|
+
// Save state snapshot
|
|
477
|
+
const state = {
|
|
478
|
+
features: APP.features.map((f) => ({
|
|
479
|
+
id: f.id,
|
|
480
|
+
name: f.name,
|
|
481
|
+
type: f.type,
|
|
482
|
+
params: JSON.parse(JSON.stringify(f.params)),
|
|
483
|
+
})),
|
|
484
|
+
timestamp: Date.now(),
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
APP.history.push(state);
|
|
488
|
+
APP.historyIndex = APP.history.length - 1;
|
|
489
|
+
|
|
490
|
+
// Limit history size
|
|
491
|
+
if (APP.history.length > APP.maxHistorySize) {
|
|
492
|
+
APP.history.shift();
|
|
493
|
+
APP.historyIndex--;
|
|
215
494
|
}
|
|
216
495
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
496
|
+
APP.document.unsavedChanges = true;
|
|
497
|
+
updateWindowTitle();
|
|
498
|
+
emit('history:pushed', state);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Undo last operation
|
|
503
|
+
*/
|
|
504
|
+
export function undo() {
|
|
505
|
+
if (APP.historyIndex <= 0) {
|
|
506
|
+
showNotification('Nothing to undo', 'info');
|
|
507
|
+
return;
|
|
220
508
|
}
|
|
221
509
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
510
|
+
APP.historyIndex--;
|
|
511
|
+
restoreFromHistory();
|
|
512
|
+
updateStatusBar('Undo');
|
|
513
|
+
emit('history:undo', APP.history[APP.historyIndex]);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Redo last undone operation
|
|
518
|
+
*/
|
|
519
|
+
export function redo() {
|
|
520
|
+
if (APP.historyIndex >= APP.history.length - 1) {
|
|
521
|
+
showNotification('Nothing to redo', 'info');
|
|
522
|
+
return;
|
|
225
523
|
}
|
|
226
524
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
525
|
+
APP.historyIndex++;
|
|
526
|
+
restoreFromHistory();
|
|
527
|
+
updateStatusBar('Redo');
|
|
528
|
+
emit('history:redo', APP.history[APP.historyIndex]);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Restore state from history at current index
|
|
533
|
+
*/
|
|
534
|
+
function restoreFromHistory() {
|
|
535
|
+
const state = APP.history[APP.historyIndex];
|
|
536
|
+
if (!state) return;
|
|
537
|
+
|
|
538
|
+
// Clear scene
|
|
539
|
+
APP.features.forEach(f => {
|
|
540
|
+
if (f.mesh) removeFromScene(f.mesh);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Restore features
|
|
544
|
+
APP.features = [];
|
|
545
|
+
APP.selectedFeature = null;
|
|
546
|
+
|
|
547
|
+
if (state.features && Array.isArray(state.features)) {
|
|
548
|
+
state.features.forEach((featureData) => {
|
|
549
|
+
try {
|
|
550
|
+
const primitive = createPrimitive(featureData.type, featureData.params);
|
|
551
|
+
addToScene(primitive.mesh);
|
|
552
|
+
|
|
553
|
+
const feature = {
|
|
554
|
+
id: featureData.id,
|
|
555
|
+
name: featureData.name,
|
|
556
|
+
type: featureData.type,
|
|
557
|
+
mesh: primitive.mesh,
|
|
558
|
+
params: featureData.params,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
APP.features.push(feature);
|
|
562
|
+
addFeature(feature);
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.warn(`Failed to restore feature ${featureData.name}:`, err);
|
|
233
565
|
}
|
|
234
|
-
};
|
|
566
|
+
});
|
|
235
567
|
}
|
|
236
568
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
569
|
+
APP.document.unsavedChanges = true;
|
|
570
|
+
updateWindowTitle();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// PREFERENCES MANAGER — User settings persistence
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Load preferences from localStorage
|
|
579
|
+
*/
|
|
580
|
+
function loadPreferences() {
|
|
581
|
+
const stored = localStorage.getItem('cyclecad_preferences');
|
|
582
|
+
if (stored) {
|
|
583
|
+
try {
|
|
584
|
+
const prefs = JSON.parse(stored);
|
|
585
|
+
APP.preferences = { ...APP.preferences, ...prefs };
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.warn('Failed to load preferences:', err);
|
|
588
|
+
}
|
|
241
589
|
}
|
|
590
|
+
}
|
|
242
591
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
592
|
+
/**
|
|
593
|
+
* Save preferences to localStorage
|
|
594
|
+
*/
|
|
595
|
+
export function savePreferences() {
|
|
596
|
+
localStorage.setItem('cyclecad_preferences', JSON.stringify(APP.preferences));
|
|
597
|
+
emit('preferences:changed', APP.preferences);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Update a preference
|
|
602
|
+
* @param {string} key - Preference key
|
|
603
|
+
* @param {*} value - New value
|
|
604
|
+
*/
|
|
605
|
+
export function setPreference(key, value) {
|
|
606
|
+
const keys = key.split('.');
|
|
607
|
+
let obj = APP.preferences;
|
|
608
|
+
|
|
609
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
610
|
+
obj = obj[keys[i]];
|
|
246
611
|
}
|
|
247
612
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
613
|
+
obj[keys[keys.length - 1]] = value;
|
|
614
|
+
savePreferences();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get a preference value
|
|
619
|
+
* @param {string} key - Preference key
|
|
620
|
+
* @returns {*}
|
|
621
|
+
*/
|
|
622
|
+
export function getPreference(key) {
|
|
623
|
+
const keys = key.split('.');
|
|
624
|
+
let obj = APP.preferences;
|
|
625
|
+
|
|
626
|
+
for (const k of keys) {
|
|
627
|
+
obj = obj[k];
|
|
628
|
+
if (obj === undefined) return undefined;
|
|
251
629
|
}
|
|
252
630
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
631
|
+
return obj;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Toggle theme between light and dark
|
|
636
|
+
*/
|
|
637
|
+
export function toggleTheme() {
|
|
638
|
+
const newTheme = APP.preferences.theme === 'dark' ? 'light' : 'dark';
|
|
639
|
+
setPreference('theme', newTheme);
|
|
640
|
+
|
|
641
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
642
|
+
updateStatusBar(`Theme: ${newTheme}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ============================================================================
|
|
646
|
+
// TOOL MANAGER — Tool activation and state
|
|
647
|
+
// ============================================================================
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Activate a tool
|
|
651
|
+
* @param {string} toolName - Tool name (line, circle, rect, extrude, etc.)
|
|
652
|
+
*/
|
|
653
|
+
export function activateTool(toolName) {
|
|
654
|
+
if (APP.currentTool === toolName) {
|
|
655
|
+
return; // Already active
|
|
257
656
|
}
|
|
258
657
|
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
658
|
+
// Deactivate previous tool
|
|
659
|
+
if (APP.currentTool) {
|
|
660
|
+
deactivateTool();
|
|
262
661
|
}
|
|
263
662
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
663
|
+
APP.currentTool = toolName;
|
|
664
|
+
|
|
665
|
+
// Highlight tool button
|
|
666
|
+
const btn = document.querySelector(`[data-tool="${toolName}"]`);
|
|
667
|
+
if (btn) {
|
|
668
|
+
btn.classList.add('active');
|
|
268
669
|
}
|
|
269
670
|
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
671
|
+
// Set mode and update UI
|
|
672
|
+
if (toolName === 'sketch') {
|
|
673
|
+
APP.mode = 'sketch';
|
|
674
|
+
APP.currentSketch = startSketch();
|
|
675
|
+
} else if (['extrude', 'revolve', 'fillet', 'chamfer'].includes(toolName)) {
|
|
676
|
+
APP.mode = 'operation';
|
|
273
677
|
}
|
|
274
678
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
679
|
+
updateStatusBar(`Tool: ${toolName}`);
|
|
680
|
+
emit('tool:activated', toolName);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Deactivate current tool
|
|
685
|
+
*/
|
|
686
|
+
export function deactivateTool() {
|
|
687
|
+
if (!APP.currentTool) return;
|
|
688
|
+
|
|
689
|
+
const btn = document.querySelector(`[data-tool="${APP.currentTool}"]`);
|
|
690
|
+
if (btn) {
|
|
691
|
+
btn.classList.remove('active');
|
|
278
692
|
}
|
|
693
|
+
|
|
694
|
+
// Cancel sketching
|
|
695
|
+
if (APP.mode === 'sketch' && APP.currentSketch) {
|
|
696
|
+
endSketch();
|
|
697
|
+
APP.currentSketch = null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
APP.currentTool = null;
|
|
701
|
+
APP.mode = 'idle';
|
|
702
|
+
updateStatusBar('Tool deactivated');
|
|
703
|
+
emit('tool:deactivated', null);
|
|
279
704
|
}
|
|
280
705
|
|
|
281
706
|
/**
|
|
282
|
-
*
|
|
707
|
+
* Cancel current tool operation
|
|
283
708
|
*/
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
709
|
+
export function cancelTool() {
|
|
710
|
+
deactivateTool();
|
|
711
|
+
updateStatusBar('Operation cancelled');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ============================================================================
|
|
715
|
+
// NOTIFICATION SYSTEM — Toast and modal dialogs
|
|
716
|
+
// ============================================================================
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Show a notification toast
|
|
720
|
+
* @param {string} message - Message text
|
|
721
|
+
* @param {string} [type='info'] - Type: info, success, warning, error
|
|
722
|
+
* @param {number} [duration=3000] - Duration in milliseconds
|
|
723
|
+
*/
|
|
724
|
+
export function showNotification(message, type = 'info', duration = 3000) {
|
|
725
|
+
const container = document.getElementById('notifications') || createNotificationsContainer();
|
|
726
|
+
|
|
727
|
+
const toast = document.createElement('div');
|
|
728
|
+
toast.className = `notification notification-${type}`;
|
|
729
|
+
toast.textContent = message;
|
|
730
|
+
toast.style.cssText = `
|
|
731
|
+
padding: 12px 16px;
|
|
732
|
+
margin: 8px;
|
|
733
|
+
background: ${getNotificationColor(type)};
|
|
734
|
+
color: white;
|
|
735
|
+
border-radius: 4px;
|
|
736
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
737
|
+
animation: slideIn 0.3s ease-out;
|
|
738
|
+
max-width: 300px;
|
|
739
|
+
word-wrap: break-word;
|
|
740
|
+
`;
|
|
741
|
+
|
|
742
|
+
container.appendChild(toast);
|
|
743
|
+
|
|
744
|
+
if (duration > 0) {
|
|
745
|
+
setTimeout(() => {
|
|
746
|
+
toast.style.animation = 'slideOut 0.3s ease-out';
|
|
747
|
+
setTimeout(() => toast.remove(), 300);
|
|
748
|
+
}, duration);
|
|
288
749
|
}
|
|
289
750
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
751
|
+
return toast;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getNotificationColor(type) {
|
|
755
|
+
const colors = {
|
|
756
|
+
info: '#3b82f6',
|
|
757
|
+
success: '#10b981',
|
|
758
|
+
warning: '#f59e0b',
|
|
759
|
+
error: '#ef4444',
|
|
760
|
+
};
|
|
761
|
+
return colors[type] || colors.info;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function createNotificationsContainer() {
|
|
765
|
+
const container = document.createElement('div');
|
|
766
|
+
container.id = 'notifications';
|
|
767
|
+
container.style.cssText = `
|
|
768
|
+
position: fixed;
|
|
769
|
+
top: 20px;
|
|
770
|
+
right: 20px;
|
|
771
|
+
z-index: 10000;
|
|
772
|
+
max-width: 400px;
|
|
773
|
+
`;
|
|
774
|
+
document.body.appendChild(container);
|
|
775
|
+
return container;
|
|
293
776
|
}
|
|
294
777
|
|
|
295
778
|
/**
|
|
296
|
-
*
|
|
779
|
+
* Show a confirmation dialog
|
|
780
|
+
* @param {string} message - Dialog message
|
|
781
|
+
* @param {string} [title='Confirm'] - Dialog title
|
|
782
|
+
* @returns {Promise<boolean>}
|
|
297
783
|
*/
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
784
|
+
export function showConfirmDialog(message, title = 'Confirm') {
|
|
785
|
+
return new Promise((resolve) => {
|
|
786
|
+
const dialog = document.createElement('div');
|
|
787
|
+
dialog.className = 'modal-dialog';
|
|
788
|
+
dialog.innerHTML = `
|
|
789
|
+
<div style="background: var(--bg-secondary); padding: 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); max-width: 400px;">
|
|
790
|
+
<h3 style="margin: 0 0 12px 0;">${escapeHtml(title)}</h3>
|
|
791
|
+
<p style="margin: 0 0 20px 0; color: var(--text-secondary);">${escapeHtml(message)}</p>
|
|
792
|
+
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
793
|
+
<button onclick="this.parentElement.parentElement.parentElement.remove();" style="padding: 8px 16px; background: var(--bg-tertiary); border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
|
|
794
|
+
<button onclick="window._resolveConfirm(true); this.parentElement.parentElement.parentElement.remove();" style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">OK</button>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
`;
|
|
798
|
+
dialog.style.cssText = `
|
|
799
|
+
position: fixed;
|
|
800
|
+
top: 0;
|
|
801
|
+
left: 0;
|
|
802
|
+
width: 100%;
|
|
803
|
+
height: 100%;
|
|
804
|
+
background: rgba(0,0,0,0.5);
|
|
805
|
+
display: flex;
|
|
806
|
+
align-items: center;
|
|
807
|
+
justify-content: center;
|
|
808
|
+
z-index: 10001;
|
|
809
|
+
`;
|
|
810
|
+
|
|
811
|
+
window._resolveConfirm = (result) => {
|
|
812
|
+
resolve(result);
|
|
813
|
+
delete window._resolveConfirm;
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
document.body.appendChild(dialog);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function escapeHtml(text) {
|
|
821
|
+
const div = document.createElement('div');
|
|
822
|
+
div.textContent = text;
|
|
823
|
+
return div.innerHTML;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Show progress bar for long-running operations
|
|
828
|
+
* @param {string} [message='Processing...'] - Progress message
|
|
829
|
+
* @returns {Object} Progress controller
|
|
830
|
+
*/
|
|
831
|
+
export function showProgress(message = 'Processing...') {
|
|
832
|
+
const container = document.createElement('div');
|
|
833
|
+
container.style.cssText = `
|
|
834
|
+
position: fixed;
|
|
835
|
+
bottom: 20px;
|
|
836
|
+
left: 20px;
|
|
837
|
+
background: var(--bg-secondary);
|
|
838
|
+
padding: 12px 16px;
|
|
839
|
+
border-radius: 4px;
|
|
840
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
841
|
+
z-index: 10000;
|
|
842
|
+
min-width: 300px;
|
|
843
|
+
`;
|
|
844
|
+
|
|
845
|
+
container.innerHTML = `
|
|
846
|
+
<div style="margin-bottom: 8px; font-size: 12px; color: var(--text-secondary);">${message}</div>
|
|
847
|
+
<div style="width: 100%; height: 4px; background: var(--bg-tertiary); border-radius: 2px; overflow: hidden;">
|
|
848
|
+
<div style="height: 100%; background: #3b82f6; width: 0%; transition: width 0.3s ease-out;"></div>
|
|
849
|
+
</div>
|
|
850
|
+
`;
|
|
851
|
+
|
|
852
|
+
document.body.appendChild(container);
|
|
853
|
+
|
|
854
|
+
const progressBar = container.querySelector('div:last-child > div');
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
update: (percent) => {
|
|
858
|
+
progressBar.style.width = Math.min(100, Math.max(0, percent)) + '%';
|
|
859
|
+
},
|
|
860
|
+
close: () => {
|
|
861
|
+
container.remove();
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ============================================================================
|
|
867
|
+
// COMMAND PALETTE — Global command search (Ctrl+Shift+P)
|
|
868
|
+
// ============================================================================
|
|
869
|
+
|
|
870
|
+
const commands = [
|
|
871
|
+
{ name: 'New Document', category: 'File', action: newDocument },
|
|
872
|
+
{ name: 'Open Document', category: 'File', action: openDocument },
|
|
873
|
+
{ name: 'Save', category: 'File', action: saveDocument },
|
|
874
|
+
{ name: 'Undo', category: 'Edit', action: undo },
|
|
875
|
+
{ name: 'Redo', category: 'Edit', action: redo },
|
|
876
|
+
{ name: 'Delete', category: 'Edit', action: deleteSelected },
|
|
877
|
+
{ name: 'Toggle Grid', category: 'View', action: toggleGrid },
|
|
878
|
+
{ name: 'Toggle Wireframe', category: 'View', action: toggleWireframe },
|
|
879
|
+
{ name: 'Fit All', category: 'View', action: fitAll },
|
|
880
|
+
{ name: 'Toggle Theme', category: 'Settings', action: toggleTheme },
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Show command palette (Ctrl+Shift+P)
|
|
885
|
+
*/
|
|
886
|
+
export function showCommandPalette() {
|
|
887
|
+
if (APP.uiState.showCommandPalette) return;
|
|
888
|
+
|
|
889
|
+
APP.uiState.showCommandPalette = true;
|
|
890
|
+
|
|
891
|
+
const dialog = document.createElement('div');
|
|
892
|
+
dialog.className = 'command-palette';
|
|
893
|
+
dialog.innerHTML = `
|
|
894
|
+
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999;" onclick="if (event.target === this) this.parentElement.remove();"></div>
|
|
895
|
+
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--bg-secondary); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; width: 90%; max-width: 500px;">
|
|
896
|
+
<input type="text" id="command-input" placeholder="Search commands..." style="width: 100%; padding: 12px 16px; border: none; border-bottom: 1px solid var(--bg-tertiary); background: transparent; color: var(--text-primary); font-size: 14px; box-sizing: border-box;">
|
|
897
|
+
<div id="command-list" style="max-height: 400px; overflow-y: auto;"></div>
|
|
898
|
+
</div>
|
|
899
|
+
`;
|
|
900
|
+
|
|
901
|
+
document.body.appendChild(dialog);
|
|
902
|
+
|
|
903
|
+
const input = document.getElementById('command-input');
|
|
904
|
+
const list = document.getElementById('command-list');
|
|
905
|
+
|
|
906
|
+
input.focus();
|
|
907
|
+
renderCommandList(commands, list);
|
|
908
|
+
|
|
909
|
+
input.addEventListener('input', (e) => {
|
|
910
|
+
const query = e.target.value.toLowerCase();
|
|
911
|
+
const filtered = commands.filter(cmd =>
|
|
912
|
+
cmd.name.toLowerCase().includes(query) ||
|
|
913
|
+
cmd.category.toLowerCase().includes(query)
|
|
914
|
+
);
|
|
915
|
+
renderCommandList(filtered, list);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
input.addEventListener('keydown', (e) => {
|
|
919
|
+
if (e.key === 'Escape') {
|
|
920
|
+
dialog.remove();
|
|
921
|
+
APP.uiState.showCommandPalette = false;
|
|
922
|
+
} else if (e.key === 'Enter') {
|
|
923
|
+
const selected = list.querySelector('.selected');
|
|
924
|
+
if (selected) {
|
|
925
|
+
const cmd = commands.find(c => c.name === selected.textContent.split('\n')[0]);
|
|
926
|
+
if (cmd && cmd.action) {
|
|
927
|
+
cmd.action();
|
|
928
|
+
dialog.remove();
|
|
929
|
+
APP.uiState.showCommandPalette = false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function renderCommandList(filteredCommands, container) {
|
|
937
|
+
container.innerHTML = '';
|
|
938
|
+
|
|
939
|
+
const grouped = {};
|
|
940
|
+
filteredCommands.forEach(cmd => {
|
|
941
|
+
if (!grouped[cmd.category]) grouped[cmd.category] = [];
|
|
942
|
+
grouped[cmd.category].push(cmd);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
Object.keys(grouped).forEach(category => {
|
|
946
|
+
const group = document.createElement('div');
|
|
947
|
+
group.innerHTML = `<div style="padding: 8px 16px; color: var(--text-secondary); font-size: 11px; font-weight: bold; text-transform: uppercase;">${category}</div>`;
|
|
948
|
+
|
|
949
|
+
grouped[category].forEach(cmd => {
|
|
950
|
+
const item = document.createElement('div');
|
|
951
|
+
item.innerHTML = `<div style="padding: 8px 16px; cursor: pointer; color: var(--text-primary);">${cmd.name}</div>`;
|
|
952
|
+
item.style.cursor = 'pointer';
|
|
953
|
+
item.onmouseenter = () => item.classList.add('selected');
|
|
954
|
+
item.onmouseleave = () => item.classList.remove('selected');
|
|
955
|
+
item.onclick = () => {
|
|
956
|
+
if (cmd.action) cmd.action();
|
|
957
|
+
document.querySelector('.command-palette').parentElement.remove();
|
|
958
|
+
APP.uiState.showCommandPalette = false;
|
|
959
|
+
};
|
|
960
|
+
group.appendChild(item);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
container.appendChild(group);
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ============================================================================
|
|
968
|
+
// PERFORMANCE MONITOR — FPS and metrics
|
|
969
|
+
// ============================================================================
|
|
970
|
+
|
|
971
|
+
let frameCount = 0;
|
|
972
|
+
let lastTime = performance.now();
|
|
973
|
+
|
|
974
|
+
function updateMetrics() {
|
|
975
|
+
const now = performance.now();
|
|
976
|
+
const deltaTime = now - lastTime;
|
|
977
|
+
|
|
978
|
+
frameCount++;
|
|
979
|
+
|
|
980
|
+
if (deltaTime >= 1000) {
|
|
981
|
+
APP.metrics.fps = Math.round(frameCount * 1000 / deltaTime);
|
|
982
|
+
APP.metrics.frameTime = deltaTime / frameCount;
|
|
983
|
+
|
|
984
|
+
// Update metrics display
|
|
985
|
+
const metricsPanel = document.getElementById('metrics-panel');
|
|
986
|
+
if (metricsPanel) {
|
|
987
|
+
metricsPanel.innerHTML = `
|
|
988
|
+
<div style="font-size: 11px; font-family: monospace;">
|
|
989
|
+
FPS: ${APP.metrics.fps}<br>
|
|
990
|
+
Frame: ${APP.metrics.frameTime.toFixed(2)}ms<br>
|
|
991
|
+
Parts: ${APP.metrics.meshCount}<br>
|
|
992
|
+
Verts: ${APP.metrics.vertexCount}
|
|
993
|
+
</div>
|
|
994
|
+
`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
frameCount = 0;
|
|
998
|
+
lastTime = now;
|
|
303
999
|
}
|
|
304
1000
|
|
|
305
|
-
|
|
1001
|
+
// Count meshes and vertices
|
|
1002
|
+
APP.metrics.meshCount = APP.features.length;
|
|
1003
|
+
APP.metrics.vertexCount = APP.features.reduce((sum, f) => {
|
|
1004
|
+
if (f.mesh && f.mesh.geometry) {
|
|
1005
|
+
return sum + (f.mesh.geometry.attributes.position?.count || 0);
|
|
1006
|
+
}
|
|
1007
|
+
return sum;
|
|
1008
|
+
}, 0);
|
|
1009
|
+
}
|
|
306
1010
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1011
|
+
// ============================================================================
|
|
1012
|
+
// INITIALIZATION & STARTUP
|
|
1013
|
+
// ============================================================================
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Main application initialization
|
|
1017
|
+
*/
|
|
1018
|
+
export async function initApp() {
|
|
1019
|
+
console.log('Initializing cycleCAD v1.0.0...');
|
|
1020
|
+
|
|
1021
|
+
// 1. Load preferences and recent files
|
|
1022
|
+
loadPreferences();
|
|
1023
|
+
loadRecentFiles();
|
|
1024
|
+
|
|
1025
|
+
// 2. Apply theme
|
|
1026
|
+
document.documentElement.setAttribute('data-theme', APP.preferences.theme);
|
|
1027
|
+
|
|
1028
|
+
// 3. Initialize 3D viewport
|
|
1029
|
+
const canvas = document.getElementById('three-canvas');
|
|
1030
|
+
if (!canvas) {
|
|
1031
|
+
console.error('Canvas #three-canvas not found');
|
|
311
1032
|
return;
|
|
312
1033
|
}
|
|
313
1034
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
1035
|
+
initViewport(canvas);
|
|
1036
|
+
APP.scene = getScene();
|
|
1037
|
+
APP.camera = getCamera();
|
|
1038
|
+
|
|
1039
|
+
// 4. Initialize UI panels
|
|
1040
|
+
initTree();
|
|
1041
|
+
initParams();
|
|
1042
|
+
initChat();
|
|
1043
|
+
|
|
1044
|
+
// 5. Setup toolbar and event listeners
|
|
1045
|
+
setupToolbar();
|
|
1046
|
+
setupKeyboardShortcuts();
|
|
1047
|
+
setupAIChat();
|
|
1048
|
+
setupGlobalEventHandlers();
|
|
1049
|
+
|
|
1050
|
+
// 6. Feature tree selection flow
|
|
1051
|
+
onSelect((featureId) => {
|
|
1052
|
+
selectFeatureById(featureId);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// 7. Parameter change flow
|
|
1056
|
+
onParamChange((paramName, value) => {
|
|
1057
|
+
if (APP.selectedFeature) {
|
|
1058
|
+
APP.selectedFeature.params[paramName] = value;
|
|
1059
|
+
rebuildFeature(APP.selectedFeature);
|
|
1060
|
+
pushHistory();
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// 8. Initialize keyboard shortcuts
|
|
1065
|
+
initShortcuts(getShortcutHandlers());
|
|
1066
|
+
|
|
1067
|
+
// 9. Setup auto-save
|
|
1068
|
+
if (APP.preferences.autoSave) {
|
|
1069
|
+
setInterval(() => {
|
|
1070
|
+
if (APP.document.unsavedChanges) {
|
|
1071
|
+
saveDocument();
|
|
1072
|
+
}
|
|
1073
|
+
}, APP.preferences.autoSaveInterval);
|
|
318
1074
|
}
|
|
319
1075
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
1076
|
+
// 10. Start animation loop
|
|
1077
|
+
animate();
|
|
1078
|
+
|
|
1079
|
+
// 11. Show welcome screen
|
|
1080
|
+
showWelcomeScreen();
|
|
1081
|
+
|
|
1082
|
+
updateWindowTitle();
|
|
1083
|
+
updateStatusBar('cycleCAD ready');
|
|
1084
|
+
emit('app:initialized', APP);
|
|
1085
|
+
|
|
1086
|
+
console.log('cycleCAD initialized successfully');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Setup global keyboard shortcuts
|
|
1091
|
+
*/
|
|
1092
|
+
function setupKeyboardShortcuts() {
|
|
1093
|
+
document.addEventListener('keydown', (e) => {
|
|
1094
|
+
// Ctrl+S / Cmd+S - Save
|
|
1095
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
1096
|
+
e.preventDefault();
|
|
1097
|
+
saveDocument();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Ctrl+Z / Cmd+Z - Undo
|
|
1102
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
undo();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Ctrl+Y / Cmd+Y - Redo
|
|
1109
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
|
|
1110
|
+
e.preventDefault();
|
|
1111
|
+
redo();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Ctrl+N / Cmd+N - New
|
|
1116
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
|
1117
|
+
e.preventDefault();
|
|
1118
|
+
newDocument();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Ctrl+O / Cmd+O - Open
|
|
1123
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'o') {
|
|
1124
|
+
e.preventDefault();
|
|
1125
|
+
openDocument();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Ctrl+Shift+P - Command Palette
|
|
1130
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'p') {
|
|
1131
|
+
e.preventDefault();
|
|
1132
|
+
showCommandPalette();
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Escape - Cancel current tool
|
|
1137
|
+
if (e.key === 'Escape') {
|
|
1138
|
+
e.preventDefault();
|
|
1139
|
+
cancelTool();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Delete - Delete selected
|
|
1144
|
+
if (e.key === 'Delete') {
|
|
1145
|
+
e.preventDefault();
|
|
1146
|
+
deleteSelected();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Setup global event handlers
|
|
1154
|
+
*/
|
|
1155
|
+
function setupGlobalEventHandlers() {
|
|
1156
|
+
// Window resize
|
|
1157
|
+
window.addEventListener('resize', () => {
|
|
1158
|
+
const canvas = document.getElementById('three-canvas');
|
|
1159
|
+
if (canvas) {
|
|
1160
|
+
canvas.width = canvas.clientWidth;
|
|
1161
|
+
canvas.height = canvas.clientHeight;
|
|
1162
|
+
if (APP.camera) {
|
|
1163
|
+
APP.camera.aspect = canvas.width / canvas.height;
|
|
1164
|
+
APP.camera.updateProjectionMatrix();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// Before unload - prompt if unsaved
|
|
1170
|
+
window.addEventListener('beforeunload', (e) => {
|
|
1171
|
+
if (APP.document.unsavedChanges) {
|
|
1172
|
+
e.preventDefault();
|
|
1173
|
+
e.returnValue = '';
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Visibility change - pause/resume
|
|
1178
|
+
document.addEventListener('visibilitychange', () => {
|
|
1179
|
+
if (document.hidden) {
|
|
1180
|
+
// Auto-save before hiding
|
|
1181
|
+
if (APP.document.unsavedChanges) {
|
|
1182
|
+
saveDocument();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// Drag and drop for file import
|
|
1188
|
+
document.addEventListener('dragover', (e) => {
|
|
1189
|
+
e.preventDefault();
|
|
1190
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
document.addEventListener('drop', (e) => {
|
|
1194
|
+
e.preventDefault();
|
|
1195
|
+
|
|
1196
|
+
const files = e.dataTransfer.files;
|
|
1197
|
+
for (const file of files) {
|
|
1198
|
+
if (file.type === 'application/json' || file.name.endsWith('.ccad')) {
|
|
1199
|
+
const reader = new FileReader();
|
|
1200
|
+
reader.onload = (event) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const data = JSON.parse(event.target.result);
|
|
1203
|
+
loadDocumentData(data, file.name);
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
showNotification(`Failed to load: ${err.message}`, 'error');
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
reader.readAsText(file);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Setup toolbar buttons
|
|
1216
|
+
*/
|
|
1217
|
+
function setupToolbar() {
|
|
1218
|
+
// File buttons
|
|
1219
|
+
const newBtn = document.getElementById('btn-new');
|
|
1220
|
+
if (newBtn) newBtn.onclick = () => newDocument();
|
|
1221
|
+
|
|
1222
|
+
const openBtn = document.getElementById('btn-open');
|
|
1223
|
+
if (openBtn) openBtn.onclick = () => openDocument();
|
|
1224
|
+
|
|
1225
|
+
const saveBtn = document.getElementById('btn-save');
|
|
1226
|
+
if (saveBtn) saveBtn.onclick = () => saveDocument();
|
|
1227
|
+
|
|
1228
|
+
// Sketch tools
|
|
1229
|
+
const sketchBtn = document.getElementById('btn-sketch');
|
|
1230
|
+
if (sketchBtn) sketchBtn.onclick = () => activateTool('sketch');
|
|
1231
|
+
|
|
1232
|
+
const lineBtn = document.getElementById('btn-line');
|
|
1233
|
+
if (lineBtn) lineBtn.onclick = () => activateTool('line');
|
|
1234
|
+
|
|
1235
|
+
const rectBtn = document.getElementById('btn-rect');
|
|
1236
|
+
if (rectBtn) rectBtn.onclick = () => activateTool('rect');
|
|
1237
|
+
|
|
1238
|
+
const circleBtn = document.getElementById('btn-circle');
|
|
1239
|
+
if (circleBtn) circleBtn.onclick = () => activateTool('circle');
|
|
1240
|
+
|
|
1241
|
+
// 3D operations
|
|
1242
|
+
const extrudeBtn = document.getElementById('btn-extrude');
|
|
1243
|
+
if (extrudeBtn) extrudeBtn.onclick = () => startExtrude();
|
|
1244
|
+
|
|
1245
|
+
// View buttons
|
|
1246
|
+
const viewFrontBtn = document.getElementById('btn-view-front');
|
|
1247
|
+
if (viewFrontBtn) viewFrontBtn.onclick = () => setView('front');
|
|
1248
|
+
|
|
1249
|
+
const viewTopBtn = document.getElementById('btn-view-top');
|
|
1250
|
+
if (viewTopBtn) viewTopBtn.onclick = () => setView('top');
|
|
1251
|
+
|
|
1252
|
+
const viewIsoBtn = document.getElementById('btn-view-iso');
|
|
1253
|
+
if (viewIsoBtn) viewIsoBtn.onclick = () => setView('iso');
|
|
325
1254
|
|
|
326
|
-
|
|
327
|
-
|
|
1255
|
+
// Edit buttons
|
|
1256
|
+
const undoBtn = document.getElementById('btn-undo');
|
|
1257
|
+
if (undoBtn) undoBtn.onclick = () => undo();
|
|
328
1258
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
id: 'feature_' + Date.now(),
|
|
332
|
-
name: 'Extrusion',
|
|
333
|
-
type: 'extrude',
|
|
334
|
-
mesh: mesh3d,
|
|
335
|
-
params: { height: h },
|
|
336
|
-
};
|
|
1259
|
+
const redoBtn = document.getElementById('btn-redo');
|
|
1260
|
+
if (redoBtn) redoBtn.onclick = () => redo();
|
|
337
1261
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
updateStatusBar('Extrusion created');
|
|
342
|
-
} catch (err) {
|
|
343
|
-
console.error('Extrude failed:', err);
|
|
344
|
-
alert('Extrude failed: ' + err.message);
|
|
345
|
-
APP.mode = 'sketch';
|
|
346
|
-
}
|
|
347
|
-
}
|
|
1262
|
+
// Display toggles
|
|
1263
|
+
const gridBtn = document.getElementById('btn-grid');
|
|
1264
|
+
if (gridBtn) gridBtn.onclick = () => toggleGrid();
|
|
348
1265
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
*/
|
|
352
|
-
function cancelOperation() {
|
|
353
|
-
if (APP.mode === 'sketch') {
|
|
354
|
-
endSketch();
|
|
355
|
-
APP.currentSketch = null;
|
|
356
|
-
APP.mode = 'idle';
|
|
357
|
-
updateStatusBar('Sketch cancelled');
|
|
358
|
-
} else if (APP.mode === 'extrude') {
|
|
359
|
-
APP.mode = 'sketch';
|
|
360
|
-
updateStatusBar('Extrude cancelled');
|
|
361
|
-
}
|
|
362
|
-
}
|
|
1266
|
+
const wireframeBtn = document.getElementById('btn-wireframe');
|
|
1267
|
+
if (wireframeBtn) wireframeBtn.onclick = () => toggleWireframe();
|
|
363
1268
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
*/
|
|
367
|
-
function confirmOperation() {
|
|
368
|
-
if (APP.mode === 'sketch') {
|
|
369
|
-
// Auto-extrude or prompt
|
|
370
|
-
const entities = getEntities();
|
|
371
|
-
if (entities && entities.length > 0) {
|
|
372
|
-
startExtrude();
|
|
373
|
-
}
|
|
374
|
-
}
|
|
1269
|
+
const fitBtn = document.getElementById('btn-fit-all');
|
|
1270
|
+
if (fitBtn) fitBtn.onclick = () => fitAll();
|
|
375
1271
|
}
|
|
376
1272
|
|
|
377
1273
|
/**
|
|
378
|
-
*
|
|
1274
|
+
* Get keyboard shortcut handlers
|
|
379
1275
|
*/
|
|
380
|
-
function
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1276
|
+
function getShortcutHandlers() {
|
|
1277
|
+
return {
|
|
1278
|
+
newSketch: () => activateTool('sketch'),
|
|
1279
|
+
line: () => activateTool('line'),
|
|
1280
|
+
rect: () => activateTool('rect'),
|
|
1281
|
+
circle: () => activateTool('circle'),
|
|
1282
|
+
extrude: () => startExtrude(),
|
|
1283
|
+
undo: () => undo(),
|
|
1284
|
+
redo: () => redo(),
|
|
1285
|
+
delete: () => deleteSelected(),
|
|
1286
|
+
escape: () => cancelTool(),
|
|
1287
|
+
viewFront: () => setView('front'),
|
|
1288
|
+
viewBack: () => setView('back'),
|
|
1289
|
+
viewRight: () => setView('right'),
|
|
1290
|
+
viewLeft: () => setView('left'),
|
|
1291
|
+
viewTop: () => setView('top'),
|
|
1292
|
+
viewBottom: () => setView('bottom'),
|
|
1293
|
+
viewIso: () => setView('iso'),
|
|
1294
|
+
toggleGrid: () => toggleGrid(),
|
|
1295
|
+
toggleWireframe: () => toggleWireframe(),
|
|
1296
|
+
fitAll: () => fitAll(),
|
|
1297
|
+
save: () => saveDocument(),
|
|
1298
|
+
showHelp: () => showShortcutsPanel(),
|
|
1299
|
+
};
|
|
390
1300
|
}
|
|
391
1301
|
|
|
392
1302
|
/**
|
|
393
1303
|
* Setup AI chat integration
|
|
394
1304
|
*/
|
|
395
1305
|
function setupAIChat() {
|
|
396
|
-
// Listen for parsed CAD prompts from AI chat
|
|
397
1306
|
const chatInput = document.getElementById('chat-input');
|
|
398
1307
|
const chatSend = document.getElementById('chat-send');
|
|
399
1308
|
|
|
@@ -402,7 +1311,6 @@ function setupAIChat() {
|
|
|
402
1311
|
const text = chatInput?.value || '';
|
|
403
1312
|
if (!text.trim()) return;
|
|
404
1313
|
|
|
405
|
-
// Parse prompt with AI
|
|
406
1314
|
parseCADPrompt(text).then((parsedPrompt) => {
|
|
407
1315
|
if (parsedPrompt && parsedPrompt.action) {
|
|
408
1316
|
executeParsedPrompt(parsedPrompt);
|
|
@@ -413,7 +1321,6 @@ function setupAIChat() {
|
|
|
413
1321
|
};
|
|
414
1322
|
}
|
|
415
1323
|
|
|
416
|
-
// Allow Enter to send
|
|
417
1324
|
if (chatInput) {
|
|
418
1325
|
chatInput.addEventListener('keypress', (e) => {
|
|
419
1326
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -424,38 +1331,12 @@ function setupAIChat() {
|
|
|
424
1331
|
}
|
|
425
1332
|
}
|
|
426
1333
|
|
|
427
|
-
/**
|
|
428
|
-
* Execute a parsed CAD prompt from AI
|
|
429
|
-
* @param {Object} prompt
|
|
430
|
-
*/
|
|
431
|
-
function executeParsedPrompt(prompt) {
|
|
432
|
-
try {
|
|
433
|
-
// Create primitive based on parsed prompt
|
|
434
|
-
const primitive = createPrimitive(prompt.type, prompt.params);
|
|
435
|
-
addToScene(primitive.mesh);
|
|
436
|
-
|
|
437
|
-
const feature = {
|
|
438
|
-
id: 'feature_' + Date.now(),
|
|
439
|
-
name: prompt.name || prompt.type,
|
|
440
|
-
type: prompt.type,
|
|
441
|
-
mesh: primitive.mesh,
|
|
442
|
-
params: prompt.params,
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
APP.features.push(feature);
|
|
446
|
-
addFeature(feature);
|
|
447
|
-
pushHistory();
|
|
448
|
-
updateStatusBar(`Created ${feature.name}`);
|
|
449
|
-
} catch (err) {
|
|
450
|
-
console.error('Failed to execute prompt:', err);
|
|
451
|
-
alert('Failed to create feature: ' + err.message);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
1334
|
/**
|
|
456
1335
|
* Show welcome screen
|
|
457
1336
|
*/
|
|
458
1337
|
function showWelcomeScreen() {
|
|
1338
|
+
if (!APP.preferences.showHelpOnStartup) return;
|
|
1339
|
+
|
|
459
1340
|
const welcomePanel = document.getElementById('welcome-panel');
|
|
460
1341
|
if (!welcomePanel) return;
|
|
461
1342
|
|
|
@@ -465,7 +1346,7 @@ function showWelcomeScreen() {
|
|
|
465
1346
|
if (newSketchBtn) {
|
|
466
1347
|
newSketchBtn.onclick = () => {
|
|
467
1348
|
welcomePanel.style.display = 'none';
|
|
468
|
-
|
|
1349
|
+
activateTool('sketch');
|
|
469
1350
|
};
|
|
470
1351
|
}
|
|
471
1352
|
|
|
@@ -482,230 +1363,119 @@ function showWelcomeScreen() {
|
|
|
482
1363
|
if (importBtn) {
|
|
483
1364
|
importBtn.onclick = () => {
|
|
484
1365
|
welcomePanel.style.display = 'none';
|
|
485
|
-
|
|
486
|
-
input.type = 'file';
|
|
487
|
-
input.accept = '.json';
|
|
488
|
-
input.onchange = (e) => {
|
|
489
|
-
const file = e.target.files[0];
|
|
490
|
-
if (file) {
|
|
491
|
-
const reader = new FileReader();
|
|
492
|
-
reader.onload = (event) => {
|
|
493
|
-
try {
|
|
494
|
-
const data = JSON.parse(event.target.result);
|
|
495
|
-
loadProject(data);
|
|
496
|
-
} catch (err) {
|
|
497
|
-
alert('Failed to load project: ' + err.message);
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
reader.readAsText(file);
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
input.click();
|
|
1366
|
+
openDocument();
|
|
504
1367
|
};
|
|
505
1368
|
}
|
|
506
1369
|
}
|
|
507
1370
|
|
|
508
1371
|
/**
|
|
509
|
-
*
|
|
1372
|
+
* Start a new sketch
|
|
510
1373
|
*/
|
|
511
|
-
function
|
|
512
|
-
|
|
1374
|
+
function startNewSketch() {
|
|
1375
|
+
activateTool('sketch');
|
|
1376
|
+
updateStatusBar('Sketch mode active. Press Escape to cancel.');
|
|
513
1377
|
}
|
|
514
1378
|
|
|
515
1379
|
/**
|
|
516
|
-
*
|
|
1380
|
+
* Start extrude operation
|
|
517
1381
|
*/
|
|
518
|
-
function
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
name: f.name,
|
|
525
|
-
type: f.type,
|
|
526
|
-
params: f.params,
|
|
527
|
-
})),
|
|
528
|
-
};
|
|
1382
|
+
function startExtrude() {
|
|
1383
|
+
const entities = getEntities();
|
|
1384
|
+
if (!entities || entities.length === 0) {
|
|
1385
|
+
showNotification('No sketch geometry to extrude', 'warning');
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
529
1388
|
|
|
530
|
-
|
|
531
|
-
const blob = new Blob([json], { type: 'application/json' });
|
|
532
|
-
const url = URL.createObjectURL(blob);
|
|
533
|
-
const a = document.createElement('a');
|
|
534
|
-
a.href = url;
|
|
535
|
-
a.download = `cyclecad_project_${Date.now()}.json`;
|
|
536
|
-
a.click();
|
|
537
|
-
URL.revokeObjectURL(url);
|
|
1389
|
+
APP.mode = 'extrude';
|
|
538
1390
|
|
|
539
|
-
|
|
540
|
-
|
|
1391
|
+
const height = prompt('Enter extrusion height (mm):', '10');
|
|
1392
|
+
if (height === null) {
|
|
1393
|
+
APP.mode = 'idle';
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
541
1396
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// Clear current
|
|
548
|
-
APP.features = [];
|
|
549
|
-
APP.history = [];
|
|
550
|
-
APP.historyIndex = -1;
|
|
551
|
-
updateScene();
|
|
1397
|
+
const h = parseFloat(height);
|
|
1398
|
+
if (isNaN(h)) {
|
|
1399
|
+
showNotification('Invalid height value', 'error');
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
552
1402
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
addToScene(primitive.mesh);
|
|
1403
|
+
try {
|
|
1404
|
+
const mesh3d = extrudeProfile(entities, h);
|
|
1405
|
+
endSketch();
|
|
1406
|
+
APP.currentSketch = null;
|
|
1407
|
+
APP.mode = 'idle';
|
|
559
1408
|
|
|
560
|
-
|
|
561
|
-
id: featureData.id,
|
|
562
|
-
name: featureData.name,
|
|
563
|
-
type: featureData.type,
|
|
564
|
-
mesh: primitive.mesh,
|
|
565
|
-
params: featureData.params,
|
|
566
|
-
};
|
|
1409
|
+
addToScene(mesh3d);
|
|
567
1410
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
}
|
|
1411
|
+
const feature = {
|
|
1412
|
+
id: `feature_${Date.now()}`,
|
|
1413
|
+
name: 'Extrusion',
|
|
1414
|
+
type: 'extrude',
|
|
1415
|
+
mesh: mesh3d,
|
|
1416
|
+
params: { height: h },
|
|
1417
|
+
};
|
|
575
1418
|
|
|
1419
|
+
APP.features.push(feature);
|
|
1420
|
+
addFeature(feature);
|
|
576
1421
|
pushHistory();
|
|
577
|
-
|
|
1422
|
+
showNotification('Extrusion created', 'success');
|
|
1423
|
+
deactivateTool();
|
|
578
1424
|
} catch (err) {
|
|
579
|
-
console.error('
|
|
580
|
-
|
|
1425
|
+
console.error('Extrude failed:', err);
|
|
1426
|
+
showNotification(`Extrude failed: ${err.message}`, 'error');
|
|
1427
|
+
APP.mode = 'idle';
|
|
581
1428
|
}
|
|
582
1429
|
}
|
|
583
1430
|
|
|
584
1431
|
/**
|
|
585
|
-
*
|
|
1432
|
+
* Execute a parsed CAD prompt from AI
|
|
586
1433
|
*/
|
|
587
|
-
function
|
|
588
|
-
if (APP.features.length === 0) {
|
|
589
|
-
alert('No features to export');
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
1434
|
+
function executeParsedPrompt(prompt) {
|
|
593
1435
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (format === 'stl') {
|
|
598
|
-
blob = exportSTL(APP.features);
|
|
599
|
-
filename = `cyclecad_model_${Date.now()}.stl`;
|
|
600
|
-
} else if (format === 'obj') {
|
|
601
|
-
blob = exportOBJ(APP.features);
|
|
602
|
-
filename = `cyclecad_model_${Date.now()}.obj`;
|
|
603
|
-
}
|
|
1436
|
+
const primitive = createPrimitive(prompt.type, prompt.params);
|
|
1437
|
+
addToScene(primitive.mesh);
|
|
604
1438
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1439
|
+
const feature = {
|
|
1440
|
+
id: `feature_${Date.now()}`,
|
|
1441
|
+
name: prompt.name || prompt.type,
|
|
1442
|
+
type: prompt.type,
|
|
1443
|
+
mesh: primitive.mesh,
|
|
1444
|
+
params: prompt.params,
|
|
1445
|
+
};
|
|
612
1446
|
|
|
613
|
-
|
|
614
|
-
|
|
1447
|
+
APP.features.push(feature);
|
|
1448
|
+
addFeature(feature);
|
|
1449
|
+
pushHistory();
|
|
1450
|
+
showNotification(`Created ${feature.name}`, 'success');
|
|
615
1451
|
} catch (err) {
|
|
616
|
-
console.error('
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Undo last operation
|
|
623
|
-
*/
|
|
624
|
-
function undo() {
|
|
625
|
-
if (APP.historyIndex > 0) {
|
|
626
|
-
APP.historyIndex--;
|
|
627
|
-
restoreFromHistory();
|
|
628
|
-
updateStatusBar('Undo');
|
|
1452
|
+
console.error('Failed to execute prompt:', err);
|
|
1453
|
+
showNotification(`Failed to create feature: ${err.message}`, 'error');
|
|
629
1454
|
}
|
|
630
1455
|
}
|
|
631
1456
|
|
|
632
1457
|
/**
|
|
633
|
-
*
|
|
1458
|
+
* Delete selected feature
|
|
634
1459
|
*/
|
|
635
|
-
function
|
|
636
|
-
if (APP.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
updateStatusBar('Redo');
|
|
1460
|
+
function deleteSelected() {
|
|
1461
|
+
if (!APP.selectedFeature) {
|
|
1462
|
+
showNotification('Nothing selected', 'info');
|
|
1463
|
+
return;
|
|
640
1464
|
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Restore state from history
|
|
645
|
-
*/
|
|
646
|
-
function restoreFromHistory() {
|
|
647
|
-
const state = APP.history[APP.historyIndex];
|
|
648
|
-
if (state) {
|
|
649
|
-
// Clear current scene
|
|
650
|
-
APP.features.forEach((f) => {
|
|
651
|
-
if (f.mesh) removeFromScene(f.mesh);
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
// Restore features from history state
|
|
655
|
-
APP.features = [];
|
|
656
|
-
if (state.features && Array.isArray(state.features)) {
|
|
657
|
-
state.features.forEach((featureData) => {
|
|
658
|
-
try {
|
|
659
|
-
const primitive = createPrimitive(featureData.type, featureData.params);
|
|
660
|
-
addToScene(primitive.mesh);
|
|
661
|
-
|
|
662
|
-
const feature = {
|
|
663
|
-
id: featureData.id,
|
|
664
|
-
name: featureData.name,
|
|
665
|
-
type: featureData.type,
|
|
666
|
-
mesh: primitive.mesh,
|
|
667
|
-
params: featureData.params,
|
|
668
|
-
};
|
|
669
1465
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1466
|
+
showConfirmDialog(`Delete "${APP.selectedFeature.name}"?`, 'Delete Feature').then(result => {
|
|
1467
|
+
if (result) {
|
|
1468
|
+
removeFromScene(APP.selectedFeature.mesh);
|
|
1469
|
+
const idx = APP.features.findIndex(f => f.id === APP.selectedFeature.id);
|
|
1470
|
+
if (idx >= 0) {
|
|
1471
|
+
removeFeature(APP.selectedFeature.id);
|
|
1472
|
+
APP.features.splice(idx, 1);
|
|
1473
|
+
}
|
|
1474
|
+
APP.selectedFeature = null;
|
|
1475
|
+
pushHistory();
|
|
1476
|
+
showNotification('Feature deleted', 'success');
|
|
676
1477
|
}
|
|
677
|
-
|
|
678
|
-
updateStatusBar(`Restored state from history (${APP.history.length - APP.historyIndex} steps remaining)`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Push current state to history
|
|
684
|
-
*/
|
|
685
|
-
function pushHistory() {
|
|
686
|
-
// Trim redo stack if not at end
|
|
687
|
-
if (APP.historyIndex < APP.history.length - 1) {
|
|
688
|
-
APP.history = APP.history.slice(0, APP.historyIndex + 1);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Save state snapshot
|
|
692
|
-
APP.history.push({
|
|
693
|
-
features: JSON.parse(JSON.stringify(APP.features.map((f) => ({
|
|
694
|
-
id: f.id,
|
|
695
|
-
name: f.name,
|
|
696
|
-
type: f.type,
|
|
697
|
-
params: f.params,
|
|
698
|
-
})))),
|
|
699
|
-
timestamp: Date.now(),
|
|
700
1478
|
});
|
|
701
|
-
|
|
702
|
-
APP.historyIndex = APP.history.length - 1;
|
|
703
|
-
|
|
704
|
-
// Keep history limited
|
|
705
|
-
if (APP.history.length > 50) {
|
|
706
|
-
APP.history.shift();
|
|
707
|
-
APP.historyIndex--;
|
|
708
|
-
}
|
|
709
1479
|
}
|
|
710
1480
|
|
|
711
1481
|
/**
|
|
@@ -713,17 +1483,15 @@ function pushHistory() {
|
|
|
713
1483
|
*/
|
|
714
1484
|
function toggleGrid() {
|
|
715
1485
|
const btn = document.getElementById('btn-grid');
|
|
716
|
-
const
|
|
1486
|
+
const isVisible = btn ? btn.classList.contains('active') : false;
|
|
717
1487
|
|
|
718
|
-
|
|
719
|
-
vpToggleGrid(!isCurrentlyVisible);
|
|
1488
|
+
vpToggleGrid(!isVisible);
|
|
720
1489
|
|
|
721
|
-
// Update button state
|
|
722
1490
|
if (btn) {
|
|
723
1491
|
btn.classList.toggle('active');
|
|
724
1492
|
}
|
|
725
1493
|
|
|
726
|
-
updateStatusBar(
|
|
1494
|
+
updateStatusBar(isVisible ? 'Grid hidden' : 'Grid visible');
|
|
727
1495
|
}
|
|
728
1496
|
|
|
729
1497
|
/**
|
|
@@ -731,17 +1499,15 @@ function toggleGrid() {
|
|
|
731
1499
|
*/
|
|
732
1500
|
function toggleWireframe() {
|
|
733
1501
|
const btn = document.getElementById('btn-wireframe');
|
|
734
|
-
const
|
|
1502
|
+
const isWireframe = btn ? btn.classList.contains('active') : false;
|
|
735
1503
|
|
|
736
|
-
|
|
737
|
-
vpToggleWireframe(!isCurrentlyWireframe);
|
|
1504
|
+
vpToggleWireframe(!isWireframe);
|
|
738
1505
|
|
|
739
|
-
// Update button state
|
|
740
1506
|
if (btn) {
|
|
741
1507
|
btn.classList.toggle('active');
|
|
742
1508
|
}
|
|
743
1509
|
|
|
744
|
-
updateStatusBar(
|
|
1510
|
+
updateStatusBar(isWireframe ? 'Solid shading' : 'Wireframe mode');
|
|
745
1511
|
}
|
|
746
1512
|
|
|
747
1513
|
/**
|
|
@@ -749,11 +1515,10 @@ function toggleWireframe() {
|
|
|
749
1515
|
*/
|
|
750
1516
|
function fitAll() {
|
|
751
1517
|
if (APP.features.length === 0) {
|
|
752
|
-
updateStatusBar('
|
|
1518
|
+
updateStatusBar('No features to fit');
|
|
753
1519
|
return;
|
|
754
1520
|
}
|
|
755
1521
|
|
|
756
|
-
// Create a temporary group of all features to fit camera
|
|
757
1522
|
const group = new THREE.Group();
|
|
758
1523
|
APP.features.forEach((f) => {
|
|
759
1524
|
if (f.mesh) {
|
|
@@ -761,20 +1526,26 @@ function fitAll() {
|
|
|
761
1526
|
}
|
|
762
1527
|
});
|
|
763
1528
|
|
|
764
|
-
// Create a bounding box to check if there's anything to show
|
|
765
1529
|
const box = new THREE.Box3().setFromObject(group);
|
|
766
1530
|
if (box.isEmpty()) {
|
|
767
|
-
updateStatusBar('No visible features
|
|
1531
|
+
updateStatusBar('No visible features');
|
|
768
1532
|
return;
|
|
769
1533
|
}
|
|
770
1534
|
|
|
771
|
-
// Fit camera to all features with padding
|
|
772
1535
|
fitToObject(group, 1.3);
|
|
773
1536
|
updateStatusBar('Fit all features');
|
|
774
1537
|
}
|
|
775
1538
|
|
|
776
1539
|
/**
|
|
777
|
-
* Update
|
|
1540
|
+
* Update window title with document name
|
|
1541
|
+
*/
|
|
1542
|
+
function updateWindowTitle() {
|
|
1543
|
+
const unsaved = APP.document.unsavedChanges ? ' ●' : '';
|
|
1544
|
+
document.title = `${APP.document.name}${unsaved} — cycleCAD`;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Update status bar message
|
|
778
1549
|
*/
|
|
779
1550
|
function updateStatusBar(message) {
|
|
780
1551
|
const statusBar = document.getElementById('status-bar');
|
|
@@ -784,11 +1555,38 @@ function updateStatusBar(message) {
|
|
|
784
1555
|
}
|
|
785
1556
|
|
|
786
1557
|
/**
|
|
787
|
-
*
|
|
1558
|
+
* Animation loop
|
|
788
1559
|
*/
|
|
1560
|
+
function animate() {
|
|
1561
|
+
requestAnimationFrame(animate);
|
|
1562
|
+
updateMetrics();
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ============================================================================
|
|
1566
|
+
// STARTUP
|
|
1567
|
+
// ============================================================================
|
|
1568
|
+
|
|
789
1569
|
document.addEventListener('DOMContentLoaded', () => {
|
|
790
1570
|
initApp().catch(console.error);
|
|
791
1571
|
});
|
|
792
1572
|
|
|
793
|
-
// Export
|
|
794
|
-
export {
|
|
1573
|
+
// Export public API
|
|
1574
|
+
export {
|
|
1575
|
+
switchWorkspace,
|
|
1576
|
+
getPreference,
|
|
1577
|
+
setPreference,
|
|
1578
|
+
activateTool,
|
|
1579
|
+
deactivateTool,
|
|
1580
|
+
selectFeatureById,
|
|
1581
|
+
clearSelection,
|
|
1582
|
+
newDocument,
|
|
1583
|
+
openDocument,
|
|
1584
|
+
saveDocument,
|
|
1585
|
+
showNotification,
|
|
1586
|
+
showConfirmDialog,
|
|
1587
|
+
showProgress,
|
|
1588
|
+
showCommandPalette,
|
|
1589
|
+
on,
|
|
1590
|
+
off,
|
|
1591
|
+
emit,
|
|
1592
|
+
};
|