cyclecad 2.1.0 → 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/app/js/app.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /**
2
- * cycleCAD - Main Application Entry Point
3
- * Wires all modules together and manages application state
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
- // Application state
17
- const APP = {
18
- mode: 'idle', // idle, sketch, extrude, operation
19
- currentSketch: null,
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
- features: [],
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
- * Initialize the application
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 async function initApp() {
33
- console.log('Initializing cycleCAD...');
109
+ export function on(eventName, callback) {
110
+ if (!eventListeners[eventName]) {
111
+ eventListeners[eventName] = [];
112
+ }
113
+ eventListeners[eventName].push(callback);
114
+ }
34
115
 
35
- // Initialize 3D viewport
36
- const canvas = document.getElementById('three-canvas');
37
- if (!canvas) {
38
- console.error('Canvas #three-canvas not found');
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
- initViewport(canvas);
43
- APP.scene = getScene();
44
- APP.camera = getCamera();
189
+ const workspace = WORKSPACES[workspaceName];
190
+ APP.currentWorkspace = workspaceName;
45
191
 
46
- // Initialize UI panels
47
- initTree();
48
- initParams();
49
- initChat();
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
- // Setup toolbar buttons
52
- setupToolbar();
202
+ // Update status bar
203
+ updateStatusBar(`Switched to ${workspace.name} workspace`);
204
+ emit('workspace:changed', workspaceName);
53
205
 
54
- // Setup feature tree selection flow
55
- onSelect((featureId) => {
56
- APP.selectedFeature = APP.features.find((f) => f.id === featureId);
57
- if (APP.selectedFeature) {
58
- showParams(APP.selectedFeature);
59
- }
60
- });
206
+ // Fit view after workspace change
207
+ setTimeout(() => fitAll(), 100);
208
+ }
61
209
 
62
- // Setup param change flow
63
- onParamChange((paramName, value) => {
64
- if (APP.selectedFeature) {
65
- APP.selectedFeature.params[paramName] = value;
66
- rebuildFeature(APP.selectedFeature);
67
- updateScene();
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
- // Initialize keyboard shortcuts
72
- initShortcuts(getShortcutHandlers());
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
- // Initialize AI chat integration
75
- setupAIChat();
247
+ updateWindowTitle();
248
+ updateStatusBar('New document created');
249
+ emit('document:created', APP.document);
250
+ }
76
251
 
77
- // Show welcome screen
78
- showWelcomeScreen();
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
- console.log('cycleCAD initialized');
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
- * Get keyboard shortcut handlers
85
- * @returns {Object}
282
+ * Load document data
283
+ * @param {Object} data - Document data
284
+ * @param {string} [filename] - Original filename
86
285
  */
87
- function getShortcutHandlers() {
88
- return {
89
- // Sketch operations
90
- newSketch: () => {
91
- if (APP.mode === 'idle') {
92
- startNewSketch();
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
- // Boolean operations
112
- union: () => console.log('Union not yet implemented'),
113
- cut: () => console.log('Cut not yet implemented'),
293
+ APP.features = [];
294
+ APP.history = [];
295
+ APP.historyIndex = -1;
114
296
 
115
- // Edit
116
- undo: () => undo(),
117
- redo: () => redo(),
118
- delete: () => deleteSelected(),
119
- escape: () => cancelOperation(),
120
- enter: () => confirmOperation(),
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
- // Views
123
- viewFront: () => setView('front'),
124
- viewBack: () => setView('back'),
125
- viewRight: () => setView('right'),
126
- viewLeft: () => setView('left'),
127
- viewTop: () => setView('top'),
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
- // Display
132
- toggleGrid: () => toggleGrid(),
133
- toggleWireframe: () => toggleWireframe(),
134
- fitAll: () => fitAll(),
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
- // File
137
- save: () => saveProject(),
138
- exportSTL: () => showExportDialog('stl'),
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
- // Help
141
- showHelp: () => showShortcutsPanel(),
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
- * Setup toolbar buttons
379
+ * Add filename to recent files list
380
+ * @param {string} filename
147
381
  */
148
- function setupToolbar() {
149
- const toolbar = document.getElementById('toolbar');
150
- if (!toolbar) return;
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
- const openBtn = document.getElementById('btn-open');
166
- if (openBtn) {
167
- openBtn.onclick = () => {
168
- const input = document.createElement('input');
169
- input.type = 'file';
170
- input.accept = '.json';
171
- input.onchange = (e) => {
172
- const file = e.target.files[0];
173
- if (file) {
174
- const reader = new FileReader();
175
- reader.onload = (event) => {
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
- const saveBtn = document.getElementById('btn-save');
191
- if (saveBtn) {
192
- saveBtn.onclick = () => saveProject();
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
- // Export buttons
196
- const exportSTLBtn = document.getElementById('btn-export-stl');
197
- if (exportSTLBtn) {
198
- exportSTLBtn.onclick = () => showExportDialog('stl');
422
+ if (!multiSelect) {
423
+ APP.selectedFeature = feature;
199
424
  }
200
425
 
201
- const exportOBJBtn = document.getElementById('btn-export-obj');
202
- if (exportOBJBtn) {
203
- exportOBJBtn.onclick = () => showExportDialog('obj');
426
+ if (!silent) {
427
+ showParams(feature);
428
+ updateStatusBar(`Selected: ${feature.name}`);
429
+ emit('selection:changed', { feature, multiSelect });
204
430
  }
431
+ }
205
432
 
206
- // Sketch tools
207
- const sketchBtn = document.getElementById('btn-sketch');
208
- if (sketchBtn) {
209
- sketchBtn.onclick = () => startNewSketch();
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
- const lineBtn = document.getElementById('btn-line');
213
- if (lineBtn) {
214
- lineBtn.onclick = () => setTool('line');
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
- const rectBtn = document.getElementById('btn-rect');
218
- if (rectBtn) {
219
- rectBtn.onclick = () => setTool('rect');
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
- const circleBtn = document.getElementById('btn-circle');
223
- if (circleBtn) {
224
- circleBtn.onclick = () => setTool('circle');
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
- // 3D operations
228
- const extrudeBtn = document.getElementById('btn-extrude');
229
- if (extrudeBtn) {
230
- extrudeBtn.onclick = () => {
231
- if (APP.mode === 'sketch') {
232
- startExtrude();
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
- // View buttons
238
- const viewFrontBtn = document.getElementById('btn-view-front');
239
- if (viewFrontBtn) {
240
- viewFrontBtn.onclick = () => setView('front');
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
- const viewTopBtn = document.getElementById('btn-view-top');
244
- if (viewTopBtn) {
245
- viewTopBtn.onclick = () => setView('top');
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
- const viewIsoBtn = document.getElementById('btn-view-iso');
249
- if (viewIsoBtn) {
250
- viewIsoBtn.onclick = () => setView('iso');
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
- // Edit buttons
254
- const undoBtn = document.getElementById('btn-undo');
255
- if (undoBtn) {
256
- undoBtn.onclick = () => undo();
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
- const redoBtn = document.getElementById('btn-redo');
260
- if (redoBtn) {
261
- redoBtn.onclick = () => redo();
658
+ // Deactivate previous tool
659
+ if (APP.currentTool) {
660
+ deactivateTool();
262
661
  }
263
662
 
264
- // Display toggles
265
- const gridBtn = document.getElementById('btn-grid');
266
- if (gridBtn) {
267
- gridBtn.onclick = () => toggleGrid();
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
- const wireframeBtn = document.getElementById('btn-wireframe');
271
- if (wireframeBtn) {
272
- wireframeBtn.onclick = () => toggleWireframe();
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
- const fitBtn = document.getElementById('btn-fit-all');
276
- if (fitBtn) {
277
- fitBtn.onclick = () => fitAll();
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
- * Start a new sketch
707
+ * Cancel current tool operation
283
708
  */
284
- function startNewSketch() {
285
- if (APP.mode === 'sketch') {
286
- alert('Finish current sketch first');
287
- return;
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
- APP.mode = 'sketch';
291
- APP.currentSketch = startSketch();
292
- updateStatusBar(`Sketch mode. Use L/R/C/A for tools. Press Escape to cancel.`);
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
- * Start extrude operation
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 startExtrude() {
299
- const entities = getEntities();
300
- if (!entities || entities.length === 0) {
301
- alert('No sketch geometry to extrude');
302
- return;
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
- APP.mode = 'extrude';
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
- // Prompt for height
308
- const height = prompt('Enter extrusion height:', '10');
309
- if (height === null) {
310
- APP.mode = 'sketch';
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
- const h = parseFloat(height);
315
- if (isNaN(h)) {
316
- alert('Invalid height');
317
- return;
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
- try {
321
- const mesh3d = extrudeProfile(entities, h);
322
- endSketch();
323
- APP.currentSketch = null;
324
- APP.mode = 'idle';
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
- // Add to scene
327
- addToScene(mesh3d);
1255
+ // Edit buttons
1256
+ const undoBtn = document.getElementById('btn-undo');
1257
+ if (undoBtn) undoBtn.onclick = () => undo();
328
1258
 
329
- // Create feature and add to tree
330
- const feature = {
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
- APP.features.push(feature);
339
- addFeature(feature);
340
- pushHistory();
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
- * Cancel current operation
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
- * Confirm current operation
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
- * Delete selected feature
1274
+ * Get keyboard shortcut handlers
379
1275
  */
380
- function deleteSelected() {
381
- if (!APP.selectedFeature) return;
382
-
383
- if (confirm(`Delete "${APP.selectedFeature.name}"?`)) {
384
- removeFromScene(APP.selectedFeature.mesh);
385
- APP.features = APP.features.filter((f) => f.id !== APP.selectedFeature.id);
386
- APP.selectedFeature = null;
387
- updateScene();
388
- pushHistory();
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
- startNewSketch();
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
- const input = document.createElement('input');
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
- * Update the 3D scene
1372
+ * Start a new sketch
510
1373
  */
511
- function updateScene() {
512
- // Scene will auto-render via viewport's animation loop
1374
+ function startNewSketch() {
1375
+ activateTool('sketch');
1376
+ updateStatusBar('Sketch mode active. Press Escape to cancel.');
513
1377
  }
514
1378
 
515
1379
  /**
516
- * Save project as JSON
1380
+ * Start extrude operation
517
1381
  */
518
- function saveProject() {
519
- const data = {
520
- version: '1.0',
521
- timestamp: new Date().toISOString(),
522
- features: APP.features.map((f) => ({
523
- id: f.id,
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
- const json = JSON.stringify(data, null, 2);
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
- updateStatusBar('Project saved');
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
- * Load project from JSON
544
- */
545
- function loadProject(data) {
546
- try {
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
- // Rebuild features
554
- if (data.features && Array.isArray(data.features)) {
555
- data.features.forEach((featureData) => {
556
- try {
557
- const primitive = createPrimitive(featureData.type, featureData.params);
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
- const feature = {
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
- APP.features.push(feature);
569
- addFeature(feature);
570
- } catch (err) {
571
- console.warn(`Failed to load feature ${featureData.name}:`, err);
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
- updateStatusBar(`Loaded project with ${APP.features.length} features`);
1422
+ showNotification('Extrusion created', 'success');
1423
+ deactivateTool();
578
1424
  } catch (err) {
579
- console.error('Load project failed:', err);
580
- alert('Failed to load project: ' + err.message);
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
- * Show export dialog
1432
+ * Execute a parsed CAD prompt from AI
586
1433
  */
587
- function showExportDialog(format) {
588
- if (APP.features.length === 0) {
589
- alert('No features to export');
590
- return;
591
- }
592
-
1434
+ function executeParsedPrompt(prompt) {
593
1435
  try {
594
- let blob;
595
- let filename;
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
- if (blob) {
606
- const url = URL.createObjectURL(blob);
607
- const a = document.createElement('a');
608
- a.href = url;
609
- a.download = filename;
610
- a.click();
611
- URL.revokeObjectURL(url);
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
- updateStatusBar(`Exported to ${filename}`);
614
- }
1447
+ APP.features.push(feature);
1448
+ addFeature(feature);
1449
+ pushHistory();
1450
+ showNotification(`Created ${feature.name}`, 'success');
615
1451
  } catch (err) {
616
- console.error('Export failed:', err);
617
- alert('Export failed: ' + err.message);
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
- * Redo last undone operation
1458
+ * Delete selected feature
634
1459
  */
635
- function redo() {
636
- if (APP.historyIndex < APP.history.length - 1) {
637
- APP.historyIndex++;
638
- restoreFromHistory();
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
- APP.features.push(feature);
671
- addFeature(feature);
672
- } catch (err) {
673
- console.warn(`Failed to restore feature ${featureData.name}:`, err);
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 isCurrentlyVisible = btn ? !btn.classList.contains('active') : true;
1486
+ const isVisible = btn ? btn.classList.contains('active') : false;
717
1487
 
718
- // Call viewport function
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(isCurrentlyVisible ? 'Grid hidden' : 'Grid visible');
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 isCurrentlyWireframe = btn ? btn.classList.contains('active') : false;
1502
+ const isWireframe = btn ? btn.classList.contains('active') : false;
735
1503
 
736
- // Call viewport function to toggle wireframe on all meshes
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(isCurrentlyWireframe ? 'Solid shading' : 'Wireframe mode');
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('Nothing to fit');
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 to fit');
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 status bar
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
- * Entry point - run when DOM is ready
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 for testing
794
- export { APP, startNewSketch, startExtrude, cancelOperation };
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
+ };