@teachinglab/omd 0.3.7 → 0.3.9

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.
@@ -1,480 +1,480 @@
1
- import { CanvasConfig } from './canvasConfig.js';
2
- import { EventManager } from '../events/eventManager.js';
3
- import { ToolManager } from '../tools/toolManager.js';
4
- import { PencilTool } from '../tools/PencilTool.js';
5
- import { EraserTool } from '../tools/EraserTool.js';
6
- import { SelectTool } from '../tools/SelectTool.js';
7
- import { Cursor } from '../ui/cursor.js';
8
- import { Toolbar } from '../ui/toolbar.js';
9
- import { FocusFrameManager } from '../features/focusFrameManager.js';
10
- import {jsvgGroup} from '@teachinglab/jsvg'
11
- /**
12
- * Main OMD Canvas class
13
- * Provides the primary interface for creating and managing a drawing canvas
14
- */
15
- export class omdCanvas {
16
- /**
17
- * @param {HTMLElement|string} container - Container element or selector
18
- * @param {Object} options - Configuration options
19
- */
20
- constructor(container, options = {}) {
21
- // Resolve container element
22
- this.container = typeof container === 'string'
23
- ? document.querySelector(container)
24
- : container;
25
-
26
- if (!this.container) {
27
- throw new Error('Container element not found');
28
- }
29
-
30
- // Set container to relative positioning for absolute positioned children
31
- this.container.style.position = 'relative';
32
-
33
- // Initialize configuration
34
- this.config = new CanvasConfig(options);
35
-
36
- // Initialize state
37
- this.strokes = new Map();
38
- this.selectedStrokes = new Set();
39
- this.isDestroyed = false;
40
- this.strokeCounter = 0;
41
-
42
- // Event system
43
- this.listeners = new Map();
44
-
45
- // Initialize canvas
46
- this._createSVGContainer();
47
- this._createLayers();
48
- this._initializeManagers();
49
- this._setupEventListeners();
50
-
51
- console.log('OMDCanvas initialized successfully');
52
- }
53
-
54
- /**
55
- * Create the main SVG container
56
- * @private
57
- */
58
- _createSVGContainer() {
59
- this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
60
- this.svg.setAttribute('width', this.config.width);
61
- this.svg.setAttribute('height', this.config.height);
62
- this.svg.setAttribute('viewBox', `0 0 ${this.config.width} ${this.config.height}`);
63
- this.svg.style.cssText = `
64
- display: block;
65
- background: ${this.config.backgroundColor};
66
- cursor: none;
67
- touch-action: none;
68
- user-select: none;
69
- `;
70
-
71
- this.container.appendChild(this.svg);
72
- }
73
-
74
- /**
75
- * Create SVG layers for different canvas elements
76
- * @private
77
- */
78
- _createLayers() {
79
- // Background layer (grid, etc.)
80
- this.backgroundLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
81
- this.backgroundLayer.setAttribute('class', 'background-layer');
82
- this.svg.appendChild(this.backgroundLayer);
83
-
84
- // Drawing layer (strokes)
85
- this.drawingLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
86
- this.drawingLayer.setAttribute('class', 'drawing-layer');
87
- this.svg.appendChild(this.drawingLayer);
88
-
89
- // UI layer (selection boxes, etc.)
90
- this.uiLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
91
- this.uiLayer.setAttribute('class', 'ui-layer');
92
- this.svg.appendChild(this.uiLayer);
93
-
94
- // Focus frame layer
95
- this.focusFrameLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
96
- this.focusFrameLayer.setAttribute('class', 'focus-frame-layer');
97
- this.svg.appendChild(this.focusFrameLayer);
98
-
99
- // Add grid if enabled
100
- if (this.config.showGrid) {
101
- this._createGrid();
102
- }
103
- }
104
-
105
- /**
106
- * Initialize managers
107
- * @private
108
- */
109
- _initializeManagers() {
110
- // Tool manager
111
- this.toolManager = new ToolManager(this);
112
-
113
- // Register default tools
114
- if (this.config.enabledTools.includes('pencil')) {
115
- this.toolManager.registerTool('pencil', new PencilTool(this));
116
- }
117
- if (this.config.enabledTools.includes('eraser')) {
118
- this.toolManager.registerTool('eraser', new EraserTool(this));
119
- }
120
- if (this.config.enabledTools.includes('select')) {
121
- this.toolManager.registerTool('select', new SelectTool(this));
122
- }
123
-
124
- // Set default tool
125
- if (this.config.defaultTool && this.config.enabledTools.includes(this.config.defaultTool)) {
126
- this.toolManager.setActiveTool(this.config.defaultTool);
127
- }
128
-
129
- // Event manager
130
- this.eventManager = new EventManager(this);
131
- this.eventManager.initialize();
132
-
133
- // Cursor
134
- this.cursor = new Cursor(this);
135
-
136
- // Toolbar
137
- if (this.config.showToolbar) {
138
- this.toolbar = new Toolbar(this);
139
- // Toolbar is now added directly to container in its constructor
140
- }
141
-
142
- // Cursor will be shown/hidden based on mouse enter/leave events
143
-
144
- // Focus frame manager
145
- if (this.config.enableFocusFrames) {
146
- this.focusFrameManager = new FocusFrameManager(this);
147
- }
148
- }
149
-
150
- /**
151
- * Setup event listeners
152
- * @private
153
- */
154
- _setupEventListeners() {
155
- // Listen for window resize
156
- this._resizeHandler = () => this._handleResize();
157
- window.addEventListener('resize', this._resizeHandler);
158
-
159
- // Hide cursor initially - EventManager will handle show/hide
160
- if (this.cursor) {
161
- this.cursor.hide();
162
- }
163
- }
164
-
165
- /**
166
- * Create grid background
167
- * @private
168
- */
169
- _createGrid() {
170
- const gridSize = 20;
171
- const pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
172
- pattern.setAttribute('id', 'grid');
173
- pattern.setAttribute('width', gridSize);
174
- pattern.setAttribute('height', gridSize);
175
- pattern.setAttribute('patternUnits', 'userSpaceOnUse');
176
-
177
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
178
- path.setAttribute('d', `M ${gridSize} 0 L 0 0 0 ${gridSize}`);
179
- path.setAttribute('fill', 'none');
180
- path.setAttribute('stroke', '#ddd');
181
- path.setAttribute('stroke-width', '1');
182
-
183
- pattern.appendChild(path);
184
-
185
- const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
186
- defs.appendChild(pattern);
187
- this.svg.insertBefore(defs, this.backgroundLayer);
188
-
189
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
190
- rect.setAttribute('width', '100%');
191
- rect.setAttribute('height', '100%');
192
- rect.setAttribute('fill', 'url(#grid)');
193
- this.backgroundLayer.appendChild(rect);
194
- }
195
-
196
- /**
197
- * Add a stroke to the canvas
198
- * @param {Stroke} stroke - Stroke to add
199
- * @returns {string} Stroke ID
200
- */
201
- addStroke(stroke) {
202
- const id = `stroke_${++this.strokeCounter}`;
203
- stroke.id = id;
204
- this.strokes.set(id, stroke);
205
- this.drawingLayer.appendChild(stroke.element);
206
-
207
- this.emit('strokeAdded', { id, stroke });
208
- return id;
209
- }
210
-
211
- /**
212
- * Remove a stroke from the canvas
213
- * @param {string} strokeId - ID of stroke to remove
214
- * @returns {boolean} True if stroke was removed
215
- */
216
- removeStroke(strokeId) {
217
- const stroke = this.strokes.get(strokeId);
218
- if (!stroke) return false;
219
-
220
- console.log('[Canvas Debug] Removing stroke:', strokeId, 'Remaining strokes:', this.strokes.size - 1);
221
-
222
- if (stroke.element.parentNode) {
223
- stroke.element.parentNode.removeChild(stroke.element);
224
- }
225
-
226
- this.strokes.delete(strokeId);
227
- this.selectedStrokes.delete(strokeId);
228
-
229
- this.emit('strokeRemoved', { strokeId });
230
- return true;
231
- }
232
-
233
- /**
234
- * Clear all strokes
235
- */
236
- clear() {
237
- console.log('[Canvas Debug] Clearing all strokes. Current count:', this.strokes.size);
238
-
239
- this.strokes.forEach((stroke, id) => {
240
- if (stroke.element.parentNode) {
241
- stroke.element.parentNode.removeChild(stroke.element);
242
- }
243
- });
244
-
245
- this.strokes.clear();
246
- this.selectedStrokes.clear();
247
-
248
- console.log('[Canvas Debug] Canvas cleared. Stroke count now:', this.strokes.size);
249
- this.emit('cleared');
250
- }
251
-
252
- /**
253
- * Select strokes by IDs
254
- * @param {Array<string>} strokeIds - Array of stroke IDs
255
- */
256
- selectStrokes(strokeIds) {
257
- this.selectedStrokes.clear();
258
- strokeIds.forEach(id => {
259
- if (this.strokes.has(id)) {
260
- this.selectedStrokes.add(id);
261
- }
262
- });
263
-
264
- this._updateStrokeSelection();
265
- this.emit('selectionChanged', { selected: Array.from(this.selectedStrokes) });
266
- }
267
-
268
- /**
269
- * Update visual selection state of strokes
270
- * @private
271
- */
272
- _updateStrokeSelection() {
273
- this.strokes.forEach((stroke, id) => {
274
- const isSelected = this.selectedStrokes.has(id);
275
- if (stroke.setSelected) {
276
- stroke.setSelected(isSelected);
277
- }
278
- });
279
- }
280
-
281
- /**
282
- * Convert client coordinates to SVG coordinates
283
- * @param {number} clientX - Client X coordinate
284
- * @param {number} clientY - Client Y coordinate
285
- * @returns {Object} {x, y} SVG coordinates
286
- */
287
- clientToSVG(clientX, clientY) {
288
- const rect = this.svg.getBoundingClientRect();
289
- const scaleX = this.config.width / rect.width;
290
- const scaleY = this.config.height / rect.height;
291
-
292
- return {
293
- x: (clientX - rect.left) * scaleX,
294
- y: (clientY - rect.top) * scaleY
295
- };
296
- }
297
-
298
- /**
299
- * Export canvas as SVG string
300
- * @returns {string} SVG content
301
- */
302
- exportSVG() {
303
- const svgClone = this.svg.cloneNode(true);
304
-
305
- // Remove UI elements from export
306
- const uiLayer = svgClone.querySelector('.ui-layer');
307
- if (uiLayer) uiLayer.remove();
308
-
309
- const focusFrameLayer = svgClone.querySelector('.focus-frame-layer');
310
- if (focusFrameLayer) focusFrameLayer.remove();
311
-
312
- return new XMLSerializer().serializeToString(svgClone);
313
- }
314
-
315
- /**
316
- * Export canvas as image
317
- * @param {string} format - Image format (png, jpeg, webp)
318
- * @param {number} quality - Image quality (0-1)
319
- * @returns {Promise<Blob>} Image blob
320
- */
321
- async exportImage(format = 'png', quality = 1) {
322
- const svgData = this.exportSVG();
323
- const canvas = document.createElement('canvas');
324
- canvas.width = this.config.width;
325
- canvas.height = this.config.height;
326
-
327
- const ctx = canvas.getContext('2d');
328
- const img = new Image();
329
-
330
- const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
331
- const url = URL.createObjectURL(svgBlob);
332
-
333
- try {
334
- await new Promise((resolve, reject) => {
335
- img.onload = resolve;
336
- img.onerror = reject;
337
- img.src = url;
338
- });
339
-
340
- ctx.drawImage(img, 0, 0);
341
-
342
- return new Promise(resolve => {
343
- canvas.toBlob(resolve, `image/${format}`, quality);
344
- });
345
- } finally {
346
- URL.revokeObjectURL(url);
347
- }
348
- }
349
-
350
- /**
351
- * Resize canvas
352
- * @param {number} width - New width
353
- * @param {number} height - New height
354
- */
355
- resize(width, height) {
356
- this.config.width = width;
357
- this.config.height = height;
358
-
359
- this.svg.setAttribute('width', width);
360
- this.svg.setAttribute('height', height);
361
- this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
362
-
363
- this.emit('resized', { width, height });
364
- }
365
-
366
- /**
367
- * Handle window resize
368
- * @private
369
- */
370
- _handleResize() {
371
- // Optional: implement responsive behavior
372
- }
373
-
374
- /**
375
- * Toggle grid visibility
376
- */
377
- toggleGrid() {
378
- if (this.backgroundLayer.querySelector('rect[fill="url(#grid)"]')) {
379
- // Remove grid
380
- const gridRect = this.backgroundLayer.querySelector('rect[fill="url(#grid)"]');
381
- if (gridRect) gridRect.remove();
382
- const defs = this.svg.querySelector('defs');
383
- if (defs) defs.remove();
384
- } else {
385
- // Add grid
386
- this._createGrid();
387
- }
388
- }
389
-
390
- /**
391
- * Add event listener
392
- * @param {string} event - Event name
393
- * @param {Function} callback - Event callback
394
- */
395
- on(event, callback) {
396
- if (!this.listeners.has(event)) {
397
- this.listeners.set(event, []);
398
- }
399
- this.listeners.get(event).push(callback);
400
- }
401
-
402
- /**
403
- * Remove event listener
404
- * @param {string} event - Event name
405
- * @param {Function} callback - Event callback
406
- */
407
- off(event, callback) {
408
- const callbacks = this.listeners.get(event);
409
- if (callbacks) {
410
- const index = callbacks.indexOf(callback);
411
- if (index !== -1) {
412
- callbacks.splice(index, 1);
413
- }
414
- }
415
- }
416
-
417
- /**
418
- * Emit event
419
- * @param {string} event - Event name
420
- * @param {Object} data - Event data
421
- */
422
- emit(event, data = {}) {
423
- const callbacks = this.listeners.get(event);
424
- if (callbacks) {
425
- callbacks.forEach(callback => {
426
- try {
427
- callback({ type: event, detail: data });
428
- } catch (error) {
429
- console.error(`Error in event listener for ${event}:`, error);
430
- }
431
- });
432
- }
433
- }
434
-
435
- /**
436
- * Get canvas information
437
- * @returns {Object} Canvas information
438
- */
439
- getInfo() {
440
- return {
441
- width: this.config.width,
442
- height: this.config.height,
443
- strokeCount: this.strokes.size,
444
- selectedStrokeCount: this.selectedStrokes.size,
445
- activeTool: this.toolManager.getActiveTool()?.name,
446
- availableTools: this.toolManager.getToolNames(),
447
- isDestroyed: this.isDestroyed
448
- };
449
- }
450
-
451
- /**
452
- * Destroy the canvas and clean up resources
453
- */
454
- destroy() {
455
- if (this.isDestroyed) return;
456
-
457
- // Clean up event listeners
458
- window.removeEventListener('resize', this._resizeHandler);
459
-
460
- // Destroy managers
461
- if (this.eventManager) this.eventManager.destroy();
462
- if (this.toolManager) this.toolManager.destroy();
463
- if (this.focusFrameManager) this.focusFrameManager.destroy();
464
- if (this.toolbar) this.toolbar.destroy();
465
- if (this.cursor) this.cursor.destroy();
466
-
467
- // Remove DOM elements
468
- if (this.svg.parentNode) {
469
- this.svg.parentNode.removeChild(this.svg);
470
- }
471
-
472
- // Clear state
473
- this.strokes.clear();
474
- this.selectedStrokes.clear();
475
- this.listeners.clear();
476
-
477
- this.isDestroyed = true;
478
- this.emit('destroyed');
479
- }
1
+ import { CanvasConfig } from './canvasConfig.js';
2
+ import { EventManager } from '../events/eventManager.js';
3
+ import { ToolManager } from '../tools/toolManager.js';
4
+ import { PencilTool } from '../tools/PencilTool.js';
5
+ import { EraserTool } from '../tools/EraserTool.js';
6
+ import { SelectTool } from '../tools/SelectTool.js';
7
+ import { PointerTool } from '../tools/PointerTool.js';
8
+ import { Cursor } from '../ui/cursor.js';
9
+ import { Toolbar } from '../ui/toolbar.js';
10
+ import { FocusFrameManager } from '../features/focusFrameManager.js';
11
+ import {jsvgGroup} from '@teachinglab/jsvg'
12
+ /**
13
+ * Main OMD Canvas class
14
+ * Provides the primary interface for creating and managing a drawing canvas
15
+ */
16
+ export class omdCanvas {
17
+ /**
18
+ * @param {HTMLElement|string} container - Container element or selector
19
+ * @param {Object} options - Configuration options
20
+ */
21
+ constructor(container, options = {}) {
22
+ // Resolve container element
23
+ this.container = typeof container === 'string'
24
+ ? document.querySelector(container)
25
+ : container;
26
+
27
+ if (!this.container) {
28
+ throw new Error('Container element not found');
29
+ }
30
+
31
+ // Set container to relative positioning for absolute positioned children
32
+ this.container.style.position = 'relative';
33
+
34
+ // Initialize configuration
35
+ this.config = new CanvasConfig(options);
36
+
37
+ // Initialize state
38
+ this.strokes = new Map();
39
+ this.selectedStrokes = new Set();
40
+ this.isDestroyed = false;
41
+ this.strokeCounter = 0;
42
+
43
+ // Event system
44
+ this.listeners = new Map();
45
+
46
+ // Initialize canvas
47
+ this._createSVGContainer();
48
+ this._createLayers();
49
+ this._initializeManagers();
50
+ this._setupEventListeners();
51
+
52
+ }
53
+
54
+ /**
55
+ * Create the main SVG container
56
+ * @private
57
+ */
58
+ _createSVGContainer() {
59
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
60
+ this.svg.setAttribute('width', this.config.width);
61
+ this.svg.setAttribute('height', this.config.height);
62
+ this.svg.setAttribute('viewBox', `0 0 ${this.config.width} ${this.config.height}`);
63
+ this.svg.style.cssText = `
64
+ display: block;
65
+ background: ${this.config.backgroundColor};
66
+ cursor: none;
67
+ touch-action: none;
68
+ user-select: none;
69
+ `;
70
+
71
+ this.container.appendChild(this.svg);
72
+ }
73
+
74
+ /**
75
+ * Create SVG layers for different canvas elements
76
+ * @private
77
+ */
78
+ _createLayers() {
79
+ // Background layer (grid, etc.)
80
+ this.backgroundLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
81
+ this.backgroundLayer.setAttribute('class', 'background-layer');
82
+ this.svg.appendChild(this.backgroundLayer);
83
+
84
+ // Drawing layer (strokes)
85
+ this.drawingLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
86
+ this.drawingLayer.setAttribute('class', 'drawing-layer');
87
+ this.svg.appendChild(this.drawingLayer);
88
+
89
+ // UI layer (selection boxes, etc.)
90
+ this.uiLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
91
+ this.uiLayer.setAttribute('class', 'ui-layer');
92
+ this.svg.appendChild(this.uiLayer);
93
+
94
+ // Focus frame layer
95
+ this.focusFrameLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
96
+ this.focusFrameLayer.setAttribute('class', 'focus-frame-layer');
97
+ this.svg.appendChild(this.focusFrameLayer);
98
+
99
+ // Add grid if enabled
100
+ if (this.config.showGrid) {
101
+ this._createGrid();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Initialize managers
107
+ * @private
108
+ */
109
+ _initializeManagers() {
110
+ // Tool manager
111
+ this.toolManager = new ToolManager(this);
112
+
113
+ // Register default tools
114
+ if (this.config.enabledTools.includes('pointer')) {
115
+ this.toolManager.registerTool('pointer', new PointerTool(this));
116
+ }
117
+ if (this.config.enabledTools.includes('pencil')) {
118
+ this.toolManager.registerTool('pencil', new PencilTool(this));
119
+ }
120
+ if (this.config.enabledTools.includes('eraser')) {
121
+ this.toolManager.registerTool('eraser', new EraserTool(this));
122
+ }
123
+ if (this.config.enabledTools.includes('select')) {
124
+ this.toolManager.registerTool('select', new SelectTool(this));
125
+ }
126
+
127
+ // Set default tool
128
+ if (this.config.defaultTool && this.config.enabledTools.includes(this.config.defaultTool)) {
129
+ this.toolManager.setActiveTool(this.config.defaultTool);
130
+ }
131
+
132
+ // Event manager
133
+ this.eventManager = new EventManager(this);
134
+ this.eventManager.initialize();
135
+
136
+ // Cursor
137
+ this.cursor = new Cursor(this);
138
+
139
+ // Toolbar
140
+ if (this.config.showToolbar) {
141
+ this.toolbar = new Toolbar(this);
142
+ // Toolbar is now added directly to container in its constructor
143
+ }
144
+
145
+ // Cursor will be shown/hidden based on mouse enter/leave events
146
+
147
+ // Focus frame manager
148
+ if (this.config.enableFocusFrames) {
149
+ this.focusFrameManager = new FocusFrameManager(this);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Setup event listeners
155
+ * @private
156
+ */
157
+ _setupEventListeners() {
158
+ // Listen for window resize
159
+ this._resizeHandler = () => this._handleResize();
160
+ window.addEventListener('resize', this._resizeHandler);
161
+
162
+ // Hide cursor initially - EventManager will handle show/hide
163
+ if (this.cursor) {
164
+ this.cursor.hide();
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Create grid background
170
+ * @private
171
+ */
172
+ _createGrid() {
173
+ const gridSize = 20;
174
+ const pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
175
+ pattern.setAttribute('id', 'grid');
176
+ pattern.setAttribute('width', gridSize);
177
+ pattern.setAttribute('height', gridSize);
178
+ pattern.setAttribute('patternUnits', 'userSpaceOnUse');
179
+
180
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
181
+ path.setAttribute('d', `M ${gridSize} 0 L 0 0 0 ${gridSize}`);
182
+ path.setAttribute('fill', 'none');
183
+ path.setAttribute('stroke', '#ddd');
184
+ path.setAttribute('stroke-width', '1');
185
+
186
+ pattern.appendChild(path);
187
+
188
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
189
+ defs.appendChild(pattern);
190
+ this.svg.insertBefore(defs, this.backgroundLayer);
191
+
192
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
193
+ rect.setAttribute('width', '100%');
194
+ rect.setAttribute('height', '100%');
195
+ rect.setAttribute('fill', 'url(#grid)');
196
+ this.backgroundLayer.appendChild(rect);
197
+ }
198
+
199
+ /**
200
+ * Add a stroke to the canvas
201
+ * @param {Stroke} stroke - Stroke to add
202
+ * @returns {string} Stroke ID
203
+ */
204
+ addStroke(stroke) {
205
+ const id = `stroke_${++this.strokeCounter}`;
206
+ stroke.id = id;
207
+ this.strokes.set(id, stroke);
208
+ this.drawingLayer.appendChild(stroke.element);
209
+
210
+ this.emit('strokeAdded', { id, stroke });
211
+ return id;
212
+ }
213
+
214
+ /**
215
+ * Remove a stroke from the canvas
216
+ * @param {string} strokeId - ID of stroke to remove
217
+ * @returns {boolean} True if stroke was removed
218
+ */
219
+ removeStroke(strokeId) {
220
+ const stroke = this.strokes.get(strokeId);
221
+ if (!stroke) return false;
222
+
223
+
224
+ if (stroke.element.parentNode) {
225
+ stroke.element.parentNode.removeChild(stroke.element);
226
+ }
227
+
228
+ this.strokes.delete(strokeId);
229
+ this.selectedStrokes.delete(strokeId);
230
+
231
+ this.emit('strokeRemoved', { strokeId });
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * Clear all strokes
237
+ */
238
+ clear() {
239
+
240
+ this.strokes.forEach((stroke, id) => {
241
+ if (stroke.element.parentNode) {
242
+ stroke.element.parentNode.removeChild(stroke.element);
243
+ }
244
+ });
245
+
246
+ this.strokes.clear();
247
+ this.selectedStrokes.clear();
248
+
249
+ this.emit('cleared');
250
+ }
251
+
252
+ /**
253
+ * Select strokes by IDs
254
+ * @param {Array<string>} strokeIds - Array of stroke IDs
255
+ */
256
+ selectStrokes(strokeIds) {
257
+ this.selectedStrokes.clear();
258
+ strokeIds.forEach(id => {
259
+ if (this.strokes.has(id)) {
260
+ this.selectedStrokes.add(id);
261
+ }
262
+ });
263
+
264
+ this._updateStrokeSelection();
265
+ this.emit('selectionChanged', { selected: Array.from(this.selectedStrokes) });
266
+ }
267
+
268
+ /**
269
+ * Update visual selection state of strokes
270
+ * @private
271
+ */
272
+ _updateStrokeSelection() {
273
+ this.strokes.forEach((stroke, id) => {
274
+ const isSelected = this.selectedStrokes.has(id);
275
+ if (stroke.setSelected) {
276
+ stroke.setSelected(isSelected);
277
+ }
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Convert client coordinates to SVG coordinates
283
+ * @param {number} clientX - Client X coordinate
284
+ * @param {number} clientY - Client Y coordinate
285
+ * @returns {Object} {x, y} SVG coordinates
286
+ */
287
+ clientToSVG(clientX, clientY) {
288
+ const rect = this.svg.getBoundingClientRect();
289
+ const scaleX = this.config.width / rect.width;
290
+ const scaleY = this.config.height / rect.height;
291
+
292
+ return {
293
+ x: (clientX - rect.left) * scaleX,
294
+ y: (clientY - rect.top) * scaleY
295
+ };
296
+ }
297
+
298
+ /**
299
+ * Export canvas as SVG string
300
+ * @returns {string} SVG content
301
+ */
302
+ exportSVG() {
303
+ const svgClone = this.svg.cloneNode(true);
304
+
305
+ // Remove UI elements from export
306
+ const uiLayer = svgClone.querySelector('.ui-layer');
307
+ if (uiLayer) uiLayer.remove();
308
+
309
+ const focusFrameLayer = svgClone.querySelector('.focus-frame-layer');
310
+ if (focusFrameLayer) focusFrameLayer.remove();
311
+
312
+ return new XMLSerializer().serializeToString(svgClone);
313
+ }
314
+
315
+ /**
316
+ * Export canvas as image
317
+ * @param {string} format - Image format (png, jpeg, webp)
318
+ * @param {number} quality - Image quality (0-1)
319
+ * @returns {Promise<Blob>} Image blob
320
+ */
321
+ async exportImage(format = 'png', quality = 1) {
322
+ const svgData = this.exportSVG();
323
+ const canvas = document.createElement('canvas');
324
+ canvas.width = this.config.width;
325
+ canvas.height = this.config.height;
326
+
327
+ const ctx = canvas.getContext('2d');
328
+ const img = new Image();
329
+
330
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
331
+ const url = URL.createObjectURL(svgBlob);
332
+
333
+ try {
334
+ await new Promise((resolve, reject) => {
335
+ img.onload = resolve;
336
+ img.onerror = reject;
337
+ img.src = url;
338
+ });
339
+
340
+ ctx.drawImage(img, 0, 0);
341
+
342
+ return new Promise(resolve => {
343
+ canvas.toBlob(resolve, `image/${format}`, quality);
344
+ });
345
+ } finally {
346
+ URL.revokeObjectURL(url);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Resize canvas
352
+ * @param {number} width - New width
353
+ * @param {number} height - New height
354
+ */
355
+ resize(width, height) {
356
+ this.config.width = width;
357
+ this.config.height = height;
358
+
359
+ this.svg.setAttribute('width', width);
360
+ this.svg.setAttribute('height', height);
361
+ this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
362
+
363
+ this.emit('resized', { width, height });
364
+ }
365
+
366
+ /**
367
+ * Handle window resize
368
+ * @private
369
+ */
370
+ _handleResize() {
371
+ // Optional: implement responsive behavior
372
+ }
373
+
374
+ /**
375
+ * Toggle grid visibility
376
+ */
377
+ toggleGrid() {
378
+ if (this.backgroundLayer.querySelector('rect[fill="url(#grid)"]')) {
379
+ // Remove grid
380
+ const gridRect = this.backgroundLayer.querySelector('rect[fill="url(#grid)"]');
381
+ if (gridRect) gridRect.remove();
382
+ const defs = this.svg.querySelector('defs');
383
+ if (defs) defs.remove();
384
+ } else {
385
+ // Add grid
386
+ this._createGrid();
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Add event listener
392
+ * @param {string} event - Event name
393
+ * @param {Function} callback - Event callback
394
+ */
395
+ on(event, callback) {
396
+ if (!this.listeners.has(event)) {
397
+ this.listeners.set(event, []);
398
+ }
399
+ this.listeners.get(event).push(callback);
400
+ }
401
+
402
+ /**
403
+ * Remove event listener
404
+ * @param {string} event - Event name
405
+ * @param {Function} callback - Event callback
406
+ */
407
+ off(event, callback) {
408
+ const callbacks = this.listeners.get(event);
409
+ if (callbacks) {
410
+ const index = callbacks.indexOf(callback);
411
+ if (index !== -1) {
412
+ callbacks.splice(index, 1);
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Emit event
419
+ * @param {string} event - Event name
420
+ * @param {Object} data - Event data
421
+ */
422
+ emit(event, data = {}) {
423
+ const callbacks = this.listeners.get(event);
424
+ if (callbacks) {
425
+ callbacks.forEach(callback => {
426
+ try {
427
+ callback({ type: event, detail: data });
428
+ } catch (error) {
429
+ console.error(`Error in event listener for ${event}:`, error);
430
+ }
431
+ });
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Get canvas information
437
+ * @returns {Object} Canvas information
438
+ */
439
+ getInfo() {
440
+ return {
441
+ width: this.config.width,
442
+ height: this.config.height,
443
+ strokeCount: this.strokes.size,
444
+ selectedStrokeCount: this.selectedStrokes.size,
445
+ activeTool: this.toolManager.getActiveTool()?.name,
446
+ availableTools: this.toolManager.getToolNames(),
447
+ isDestroyed: this.isDestroyed
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Destroy the canvas and clean up resources
453
+ */
454
+ destroy() {
455
+ if (this.isDestroyed) return;
456
+
457
+ // Clean up event listeners
458
+ window.removeEventListener('resize', this._resizeHandler);
459
+
460
+ // Destroy managers
461
+ if (this.eventManager) this.eventManager.destroy();
462
+ if (this.toolManager) this.toolManager.destroy();
463
+ if (this.focusFrameManager) this.focusFrameManager.destroy();
464
+ if (this.toolbar) this.toolbar.destroy();
465
+ if (this.cursor) this.cursor.destroy();
466
+
467
+ // Remove DOM elements
468
+ if (this.svg.parentNode) {
469
+ this.svg.parentNode.removeChild(this.svg);
470
+ }
471
+
472
+ // Clear state
473
+ this.strokes.clear();
474
+ this.selectedStrokes.clear();
475
+ this.listeners.clear();
476
+
477
+ this.isDestroyed = true;
478
+ this.emit('destroyed');
479
+ }
480
480
  }