cyclecad 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Keyboard Shortcuts Module for cycleCAD
3
+ * Registers and manages all keyboard shortcuts
4
+ */
5
+
6
+ let shortcutsPanel = null;
7
+ let shortcutsVisible = false;
8
+
9
+ const SHORTCUT_MAP = {
10
+ // Sketch tools
11
+ 's': { action: 'newSketch', label: 'New Sketch', category: 'Sketch' },
12
+ 'l': { action: 'line', label: 'Line Tool', category: 'Sketch Tools' },
13
+ 'r': { action: 'rect', label: 'Rectangle Tool', category: 'Sketch Tools' },
14
+ 'c': { action: 'circle', label: 'Circle Tool', category: 'Sketch Tools' },
15
+ 'a': { action: 'arc', label: 'Arc Tool', category: 'Sketch Tools' },
16
+ 'd': { action: 'dimension', label: 'Dimension Tool', category: 'Sketch Tools' },
17
+
18
+ // 3D Operations
19
+ 'e': { action: 'extrude', label: 'Extrude', category: '3D Operations' },
20
+ 'v': { action: 'revolve', label: 'Revolve', category: '3D Operations' },
21
+ 'f': { action: 'fillet', label: 'Fillet', category: '3D Operations' },
22
+ 'shift+f': { action: 'chamfer', label: 'Chamfer', category: '3D Operations' },
23
+
24
+ // Boolean operations
25
+ 'ctrl+u': { action: 'union', label: 'Union', category: 'Boolean' },
26
+ 'ctrl+minus': { action: 'cut', label: 'Cut', category: 'Boolean' },
27
+
28
+ // Edit
29
+ 'ctrl+z': { action: 'undo', label: 'Undo', category: 'Edit' },
30
+ 'ctrl+y': { action: 'redo', label: 'Redo', category: 'Edit' },
31
+ 'ctrl+shift+z': { action: 'redo', label: 'Redo', category: 'Edit' },
32
+ 'delete': { action: 'delete', label: 'Delete Selected', category: 'Edit' },
33
+ 'backspace': { action: 'delete', label: 'Delete Selected', category: 'Edit' },
34
+ 'escape': { action: 'escape', label: 'Cancel / Deselect', category: 'Edit' },
35
+ 'enter': { action: 'enter', label: 'Confirm', category: 'Edit' },
36
+
37
+ // Views
38
+ '1': { action: 'viewFront', label: 'Front View', category: 'Views' },
39
+ '2': { action: 'viewBack', label: 'Back View', category: 'Views' },
40
+ '3': { action: 'viewRight', label: 'Right View', category: 'Views' },
41
+ '4': { action: 'viewLeft', label: 'Left View', category: 'Views' },
42
+ '5': { action: 'viewTop', label: 'Top View', category: 'Views' },
43
+ '6': { action: 'viewBottom', label: 'Bottom View', category: 'Views' },
44
+ '7': { action: 'viewIso', label: 'Isometric View', category: 'Views' },
45
+
46
+ // Display
47
+ 'g': { action: 'toggleGrid', label: 'Toggle Grid', category: 'Display' },
48
+ 'w': { action: 'toggleWireframe', label: 'Toggle Wireframe', category: 'Display' },
49
+ 'shift+f': { action: 'fitAll', label: 'Fit All', category: 'Display' },
50
+
51
+ // Export & Save
52
+ 'ctrl+s': { action: 'save', label: 'Save (Export JSON)', category: 'File' },
53
+ 'ctrl+shift+e': { action: 'exportSTL', label: 'Export STL', category: 'File' },
54
+
55
+ // Help
56
+ '?': { action: 'showHelp', label: 'Show Shortcuts', category: 'Help' },
57
+ };
58
+
59
+ /**
60
+ * Initialize keyboard shortcuts
61
+ * @param {Object} handlers - Map of action names to handler functions
62
+ */
63
+ export function initShortcuts(handlers) {
64
+ document.addEventListener('keydown', (e) => {
65
+ // Skip if focus is on input/textarea/select
66
+ if (isInputFocused()) return;
67
+
68
+ const key = getKeyCombo(e);
69
+ const shortcut = SHORTCUT_MAP[key.toLowerCase()];
70
+
71
+ if (shortcut && handlers[shortcut.action]) {
72
+ e.preventDefault();
73
+ handlers[shortcut.action]();
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Get keyboard combination string (e.g., "ctrl+z", "shift+a")
80
+ * @param {KeyboardEvent} e
81
+ * @returns {string}
82
+ */
83
+ function getKeyCombo(e) {
84
+ const parts = [];
85
+ if (e.ctrlKey) parts.push('ctrl');
86
+ if (e.shiftKey) parts.push('shift');
87
+ if (e.altKey) parts.push('alt');
88
+
89
+ // Get the actual key
90
+ let key = e.key.toLowerCase();
91
+ if (key === 'control' || key === 'shift' || key === 'alt' || key === 'meta') {
92
+ return parts.join('+'); // Modifier key only, not a valid shortcut
93
+ }
94
+
95
+ // Special key names
96
+ const specialKeys = {
97
+ 'arrowup': 'up',
98
+ 'arrowdown': 'down',
99
+ 'arrowleft': 'left',
100
+ 'arrowright': 'right',
101
+ ' ': 'space',
102
+ 'enter': 'enter',
103
+ 'escape': 'escape',
104
+ 'delete': 'delete',
105
+ 'backspace': 'backspace',
106
+ };
107
+
108
+ key = specialKeys[key] || key;
109
+ parts.push(key);
110
+
111
+ return parts.join('+');
112
+ }
113
+
114
+ /**
115
+ * Check if an input/textarea/select is currently focused
116
+ * @returns {boolean}
117
+ */
118
+ function isInputFocused() {
119
+ const activeEl = document.activeElement;
120
+ return (
121
+ activeEl &&
122
+ (activeEl.tagName === 'INPUT' ||
123
+ activeEl.tagName === 'TEXTAREA' ||
124
+ activeEl.tagName === 'SELECT' ||
125
+ activeEl.contentEditable === 'true')
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Show the shortcuts help panel
131
+ */
132
+ export function showShortcutsPanel() {
133
+ if (shortcutsVisible && shortcutsPanel) {
134
+ shortcutsPanel.style.display = 'none';
135
+ shortcutsVisible = false;
136
+ return;
137
+ }
138
+
139
+ // Create panel if it doesn't exist
140
+ if (!shortcutsPanel) {
141
+ shortcutsPanel = createShortcutsPanel();
142
+ document.body.appendChild(shortcutsPanel);
143
+ }
144
+
145
+ shortcutsPanel.style.display = 'flex';
146
+ shortcutsVisible = true;
147
+ }
148
+
149
+ /**
150
+ * Create the shortcuts panel DOM element
151
+ * @returns {HTMLElement}
152
+ */
153
+ function createShortcutsPanel() {
154
+ const panel = document.createElement('div');
155
+ panel.id = 'shortcuts-panel';
156
+ panel.style.cssText = `
157
+ position: fixed;
158
+ top: 50%;
159
+ left: 50%;
160
+ transform: translate(-50%, -50%);
161
+ width: 90%;
162
+ max-width: 800px;
163
+ max-height: 80vh;
164
+ background: #1a1a1a;
165
+ border: 2px solid #444;
166
+ border-radius: 8px;
167
+ padding: 24px;
168
+ z-index: 10000;
169
+ display: flex;
170
+ flex-direction: column;
171
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
172
+ overflow: hidden;
173
+ `;
174
+
175
+ // Header
176
+ const header = document.createElement('div');
177
+ header.style.cssText = `
178
+ display: flex;
179
+ justify-content: space-between;
180
+ align-items: center;
181
+ margin-bottom: 16px;
182
+ padding-bottom: 12px;
183
+ border-bottom: 1px solid #444;
184
+ `;
185
+
186
+ const title = document.createElement('h2');
187
+ title.textContent = 'Keyboard Shortcuts';
188
+ title.style.cssText = `
189
+ margin: 0;
190
+ color: #fff;
191
+ font-size: 18px;
192
+ `;
193
+
194
+ const closeBtn = document.createElement('button');
195
+ closeBtn.textContent = '✕';
196
+ closeBtn.style.cssText = `
197
+ background: none;
198
+ border: none;
199
+ color: #aaa;
200
+ font-size: 24px;
201
+ cursor: pointer;
202
+ padding: 0;
203
+ width: 24px;
204
+ height: 24px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ `;
209
+ closeBtn.onclick = () => showShortcutsPanel();
210
+ closeBtn.onmouseover = () => (closeBtn.style.color = '#fff');
211
+ closeBtn.onmouseout = () => (closeBtn.style.color = '#aaa');
212
+
213
+ header.appendChild(title);
214
+ header.appendChild(closeBtn);
215
+
216
+ // Content
217
+ const content = document.createElement('div');
218
+ content.style.cssText = `
219
+ overflow-y: auto;
220
+ flex: 1;
221
+ color: #ddd;
222
+ `;
223
+
224
+ // Group shortcuts by category
225
+ const categories = {};
226
+ Object.entries(SHORTCUT_MAP).forEach(([key, shortcut]) => {
227
+ if (!categories[shortcut.category]) {
228
+ categories[shortcut.category] = [];
229
+ }
230
+ categories[shortcut.category].push({ key, ...shortcut });
231
+ });
232
+
233
+ // Render categories
234
+ Object.entries(categories).forEach(([category, shortcuts]) => {
235
+ const section = document.createElement('div');
236
+ section.style.cssText = `
237
+ margin-bottom: 20px;
238
+ `;
239
+
240
+ const catTitle = document.createElement('h3');
241
+ catTitle.textContent = category;
242
+ catTitle.style.cssText = `
243
+ margin: 0 0 8px 0;
244
+ font-size: 12px;
245
+ color: #999;
246
+ text-transform: uppercase;
247
+ letter-spacing: 1px;
248
+ `;
249
+
250
+ const shortcuts_list = document.createElement('div');
251
+ shortcuts_list.style.cssText = `
252
+ display: grid;
253
+ grid-template-columns: 1fr 1fr;
254
+ gap: 8px;
255
+ `;
256
+
257
+ shortcuts.forEach(({ key, label }) => {
258
+ const row = document.createElement('div');
259
+ row.style.cssText = `
260
+ display: flex;
261
+ justify-content: space-between;
262
+ align-items: center;
263
+ padding: 6px 8px;
264
+ border-radius: 4px;
265
+ background: rgba(255,255,255,0.05);
266
+ font-size: 12px;
267
+ `;
268
+
269
+ const labelEl = document.createElement('span');
270
+ labelEl.textContent = label;
271
+ labelEl.style.color = '#ddd';
272
+
273
+ const keyEl = document.createElement('kbd');
274
+ keyEl.textContent = key.toUpperCase();
275
+ keyEl.style.cssText = `
276
+ background: rgba(255,255,255,0.1);
277
+ padding: 2px 6px;
278
+ border-radius: 3px;
279
+ font-family: monospace;
280
+ font-size: 10px;
281
+ color: #4a9eff;
282
+ border: 1px solid rgba(74,158,255,0.3);
283
+ `;
284
+
285
+ row.appendChild(labelEl);
286
+ row.appendChild(keyEl);
287
+ shortcuts_list.appendChild(row);
288
+ });
289
+
290
+ section.appendChild(catTitle);
291
+ section.appendChild(shortcuts_list);
292
+ content.appendChild(section);
293
+ });
294
+
295
+ // Footer
296
+ const footer = document.createElement('div');
297
+ footer.style.cssText = `
298
+ margin-top: 16px;
299
+ padding-top: 12px;
300
+ border-top: 1px solid #444;
301
+ font-size: 11px;
302
+ color: #666;
303
+ `;
304
+ footer.textContent = 'Press ? to toggle this panel';
305
+
306
+ panel.appendChild(header);
307
+ panel.appendChild(content);
308
+ panel.appendChild(footer);
309
+
310
+ // Close on Escape
311
+ const closeOnEscape = (e) => {
312
+ if (e.key === 'Escape' && shortcutsVisible) {
313
+ showShortcutsPanel();
314
+ document.removeEventListener('keydown', closeOnEscape);
315
+ }
316
+ };
317
+ document.addEventListener('keydown', closeOnEscape);
318
+
319
+ return panel;
320
+ }
321
+
322
+ /**
323
+ * Get list of all shortcuts for help/documentation
324
+ * @returns {Array}
325
+ */
326
+ export function getShortcutsList() {
327
+ return Object.entries(SHORTCUT_MAP).map(([key, shortcut]) => ({
328
+ key,
329
+ ...shortcut,
330
+ }));
331
+ }
332
+
333
+ /**
334
+ * Check if a key combination is valid
335
+ * @param {string} key - Key combination (e.g., "ctrl+z")
336
+ * @returns {boolean}
337
+ */
338
+ export function isValidShortcut(key) {
339
+ return key.toLowerCase() in SHORTCUT_MAP;
340
+ }
341
+
342
+ /**
343
+ * Get the handler function name for a key combination
344
+ * @param {string} key
345
+ * @returns {string|null}
346
+ */
347
+ export function getShortcutAction(key) {
348
+ const shortcut = SHORTCUT_MAP[key.toLowerCase()];
349
+ return shortcut ? shortcut.action : null;
350
+ }