@teachinglab/omd 0.3.8 → 0.4.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.
@@ -86,14 +86,18 @@ export class EventManager {
86
86
  this.pointerEventHandler.handleMultiTouchStart(this.activePointers);
87
87
  return;
88
88
  }
89
- // Set drawing state
90
- this.isDrawing = true;
91
89
  // Delegate to pointer event handler
92
90
  this.pointerEventHandler.handlePointerDown(event, normalizedEvent);
93
91
  // Delegate to active tool
94
92
  const activeTool = this.canvas.toolManager.getActiveTool();
95
93
  if (activeTool) {
94
+ // Let the tool decide if it's drawing (e.g., PointerTool won't set isDrawing)
96
95
  activeTool.onPointerDown(normalizedEvent);
96
+ // If tool hasn't explicitly set isDrawing, set it for backwards compatibility
97
+ // (tools like PencilTool and EraserTool expect isDrawing to be true)
98
+ if (this.isDrawing === false && activeTool.constructor.name !== 'PointerTool') {
99
+ this.isDrawing = true;
100
+ }
97
101
  }
98
102
  // Emit canvas event
99
103
  this.canvas.emit('pointerDown', normalizedEvent);
@@ -241,8 +245,14 @@ export class EventManager {
241
245
  this.canvas.cursor.hide();
242
246
  }
243
247
  // Do NOT cancel drawing on pointerleave; drawing continues outside canvas
244
- // Clear active pointers
245
- this.activePointers.clear();
248
+ // But if no buttons are pressed, we can safely clear the drawing state
249
+ if (event.buttons === 0) {
250
+ this.isDrawing = false;
251
+ }
252
+ // Clear active pointers only if no buttons pressed
253
+ if (event.buttons === 0) {
254
+ this.activePointers.clear();
255
+ }
246
256
  // Emit canvas event
247
257
  this.canvas.emit('pointerLeave', { event });
248
258
  }
@@ -1,287 +1,285 @@
1
- export class FocusFrameManager {
2
- /**
3
- * @param {OMDCanvas} canvas - Canvas instance
4
- */
5
- constructor(canvas) {
6
- this.canvas = canvas;
7
- this.frames = new Map();
8
- this.activeFrameId = null;
9
- this.frameCounter = 0;
10
- }
11
-
12
- /**
13
- * Create a new focus frame
14
- * @param {Object} options - Frame options
15
- * @param {number} options.x - X position
16
- * @param {number} options.y - Y position
17
- * @param {number} options.width - Frame width
18
- * @param {number} options.height - Frame height
19
- * @param {boolean} [options.showOutline=true] - Show frame outline
20
- * @param {string} [options.outlineColor='#007bff'] - Outline color
21
- * @param {number} [options.outlineWidth=2] - Outline width
22
- * @param {boolean} [options.outlineDashed=false] - Dashed outline
23
- * @returns {Object} {id, frame} Created frame
24
- */
25
- createFrame(options = {}) {
26
- const id = `frame_${++this.frameCounter}`;
27
- const frame = new FocusFrame(this.canvas, id, options);
28
-
29
- this.frames.set(id, frame);
30
-
31
- // Add to focus frame layer if available
32
- if (this.canvas.focusFrameLayer) {
33
- this.canvas.focusFrameLayer.appendChild(frame.element);
34
- console.log('Focus frame added to layer:', this.canvas.focusFrameLayer);
35
- console.log('Focus frame element:', frame.element);
36
- } else {
37
- console.warn('Focus frame layer not found!');
38
- }
39
-
40
- this.canvas.emit('focusFrameCreated', { id, frame });
41
-
42
- return { id, frame };
43
- }
44
-
45
- /**
46
- * Remove a focus frame
47
- * @param {string} frameId - Frame ID
48
- * @returns {boolean} True if frame was removed
49
- */
50
- removeFrame(frameId) {
51
- const frame = this.frames.get(frameId);
52
- if (!frame) return false;
53
-
54
- if (this.activeFrameId === frameId) {
55
- this.activeFrameId = null;
56
- }
57
-
58
- frame.destroy();
59
- this.frames.delete(frameId);
60
-
61
- this.canvas.emit('focusFrameRemoved', { frameId });
62
-
63
- return true;
64
- }
65
-
66
- /**
67
- * Get frame by ID
68
- * @param {string} frameId - Frame ID
69
- * @returns {FocusFrame|undefined} Frame instance
70
- */
71
- getFrame(frameId) {
72
- return this.frames.get(frameId);
73
- }
74
-
75
- /**
76
- * Set active frame
77
- * @param {string} frameId - Frame ID
78
- * @returns {boolean} True if frame was set as active
79
- */
80
- setActiveFrame(frameId) {
81
- const frame = this.frames.get(frameId);
82
- if (!frame) return false;
83
-
84
- // Deactivate previous frame
85
- if (this.activeFrameId) {
86
- const prevFrame = this.frames.get(this.activeFrameId);
87
- if (prevFrame) {
88
- prevFrame.setActive(false);
89
- }
90
- }
91
-
92
- // Activate new frame
93
- this.activeFrameId = frameId;
94
- frame.setActive(true);
95
-
96
- this.canvas.emit('focusFrameActivated', { frameId, frame });
97
-
98
- return true;
99
- }
100
-
101
- /**
102
- * Get active frame
103
- * @returns {FocusFrame|null} Active frame or null
104
- */
105
- getActiveFrame() {
106
- return this.activeFrameId ? this.frames.get(this.activeFrameId) : null;
107
- }
108
-
109
- /**
110
- * Capture content from active frame
111
- * @returns {string|null} SVG content or null
112
- */
113
- captureActiveFrame() {
114
- const activeFrame = this.getActiveFrame();
115
- return activeFrame ? activeFrame.capture() : null;
116
- }
117
-
118
- /**
119
- * Capture all frames
120
- * @returns {Map<string, string>} Map of frame IDs to SVG content
121
- */
122
- captureAllFrames() {
123
- const captures = new Map();
124
-
125
- for (const [id, frame] of this.frames) {
126
- captures.set(id, frame.capture());
127
- }
128
-
129
- return captures;
130
- }
131
-
132
- /**
133
- * Clear all frames
134
- */
135
- clearAllFrames() {
136
- for (const [id, frame] of this.frames) {
137
- frame.destroy();
138
- }
139
-
140
- this.frames.clear();
141
- this.activeFrameId = null;
142
-
143
- this.canvas.emit('focusFramesCleared');
144
- }
145
-
146
- /**
147
- * Get all frame IDs
148
- * @returns {Array<string>} Array of frame IDs
149
- */
150
- getFrameIds() {
151
- return Array.from(this.frames.keys());
152
- }
153
-
154
- /**
155
- * Destroy focus frame manager
156
- */
157
- destroy() {
158
- this.clearAllFrames();
159
- }
160
- }
161
-
162
- /**
163
- * Individual focus frame for capturing canvas regions
164
- */
165
- class FocusFrame {
166
- /**
167
- * @param {OMDCanvas} canvas - Canvas instance
168
- * @param {string} id - Frame ID
169
- * @param {Object} options - Frame options
170
- */
171
- constructor(canvas, id, options = {}) {
172
- this.canvas = canvas;
173
- this.id = id;
174
- this.x = options.x || 0;
175
- this.y = options.y || 0;
176
- this.width = options.width || 200;
177
- this.height = options.height || 150;
178
- this.showOutline = options.showOutline !== false;
179
- this.outlineColor = options.outlineColor || '#007bff';
180
- this.outlineWidth = options.outlineWidth || 2;
181
- this.outlineDashed = options.outlineDashed || false;
182
- this.isActive = false;
183
- this._createElement();
184
- }
185
- /**
186
- * Create the visual frame element
187
- * @private
188
- */
189
- _createElement() {
190
- this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
191
- this.element.setAttribute('class', 'focus-frame');
192
- this.element.setAttribute('data-frame-id', this.id);
193
- this.element.style.pointerEvents = 'none'; // No pointer events
194
- this.element.style.zIndex = '1000';
195
- if (this.showOutline) {
196
- this.outline = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
197
- this.outline.setAttribute('x', this.x);
198
- this.outline.setAttribute('y', this.y);
199
- this.outline.setAttribute('width', this.width);
200
- this.outline.setAttribute('height', this.height);
201
- this.outline.setAttribute('fill', 'none');
202
- this.outline.setAttribute('stroke', this.outlineColor);
203
- this.outline.setAttribute('stroke-width', this.outlineWidth);
204
- if (this.outlineDashed) {
205
- this.outline.setAttribute('stroke-dasharray', '5,5');
206
- }
207
- this.outline.style.pointerEvents = 'none';
208
- this.element.appendChild(this.outline);
209
- }
210
- }
211
- setActive(active) {
212
- this.isActive = active;
213
- if (this.outline) {
214
- this.outline.setAttribute('stroke-width', this.outlineWidth);
215
- }
216
- }
217
- capture() {
218
- const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
219
- tempSvg.setAttribute('width', this.width);
220
- tempSvg.setAttribute('height', this.height);
221
- tempSvg.setAttribute('viewBox', `${this.x} ${this.y} ${this.width} ${this.height}`);
222
- const drawingLayer = this.canvas.drawingLayer.cloneNode(true);
223
- tempSvg.appendChild(drawingLayer);
224
- return new XMLSerializer().serializeToString(tempSvg);
225
- }
226
- async toBitmap(format = 'png', quality = 1) {
227
- const svgData = this.capture();
228
- const canvas = document.createElement('canvas');
229
- canvas.width = this.width;
230
- canvas.height = this.height;
231
- const ctx = canvas.getContext('2d');
232
- const img = new Image();
233
- const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
234
- const url = URL.createObjectURL(svgBlob);
235
- try {
236
- await new Promise((resolve, reject) => {
237
- img.onload = resolve;
238
- img.onerror = reject;
239
- img.src = url;
240
- });
241
- ctx.drawImage(img, 0, 0);
242
- return new Promise(resolve => {
243
- canvas.toBlob(resolve, `image/${format}`, quality);
244
- });
245
- } finally {
246
- URL.revokeObjectURL(url);
247
- }
248
- }
249
- async downloadAsBitmap(filename = `focus-frame-${this.id}.png`, format = 'png') {
250
- try {
251
- const blob = await this.toBitmap(format);
252
- const url = URL.createObjectURL(blob);
253
- const a = document.createElement('a');
254
- a.href = url;
255
- a.download = filename;
256
- a.click();
257
- URL.revokeObjectURL(url);
258
- } catch (error) {
259
- console.error('Failed to download frame:', error);
260
- }
261
- }
262
- updateBounds(bounds) {
263
- if (bounds.x !== undefined) this.x = bounds.x;
264
- if (bounds.y !== undefined) this.y = bounds.y;
265
- if (bounds.width !== undefined) this.width = bounds.width;
266
- if (bounds.height !== undefined) this.height = bounds.height;
267
- if (this.outline) {
268
- this.outline.setAttribute('x', this.x);
269
- this.outline.setAttribute('y', this.y);
270
- this.outline.setAttribute('width', this.width);
271
- this.outline.setAttribute('height', this.height);
272
- }
273
- }
274
- getBounds() {
275
- return {
276
- x: this.x,
277
- y: this.y,
278
- width: this.width,
279
- height: this.height
280
- };
281
- }
282
- destroy() {
283
- if (this.element.parentNode) {
284
- this.element.parentNode.removeChild(this.element);
285
- }
286
- }
1
+ export class FocusFrameManager {
2
+ /**
3
+ * @param {OMDCanvas} canvas - Canvas instance
4
+ */
5
+ constructor(canvas) {
6
+ this.canvas = canvas;
7
+ this.frames = new Map();
8
+ this.activeFrameId = null;
9
+ this.frameCounter = 0;
10
+ }
11
+
12
+ /**
13
+ * Create a new focus frame
14
+ * @param {Object} options - Frame options
15
+ * @param {number} options.x - X position
16
+ * @param {number} options.y - Y position
17
+ * @param {number} options.width - Frame width
18
+ * @param {number} options.height - Frame height
19
+ * @param {boolean} [options.showOutline=true] - Show frame outline
20
+ * @param {string} [options.outlineColor='#007bff'] - Outline color
21
+ * @param {number} [options.outlineWidth=2] - Outline width
22
+ * @param {boolean} [options.outlineDashed=false] - Dashed outline
23
+ * @returns {Object} {id, frame} Created frame
24
+ */
25
+ createFrame(options = {}) {
26
+ const id = `frame_${++this.frameCounter}`;
27
+ const frame = new FocusFrame(this.canvas, id, options);
28
+
29
+ this.frames.set(id, frame);
30
+
31
+ // Add to focus frame layer if available
32
+ if (this.canvas.focusFrameLayer) {
33
+ this.canvas.focusFrameLayer.appendChild(frame.element);
34
+ } else {
35
+ console.warn('Focus frame layer not found!');
36
+ }
37
+
38
+ this.canvas.emit('focusFrameCreated', { id, frame });
39
+
40
+ return { id, frame };
41
+ }
42
+
43
+ /**
44
+ * Remove a focus frame
45
+ * @param {string} frameId - Frame ID
46
+ * @returns {boolean} True if frame was removed
47
+ */
48
+ removeFrame(frameId) {
49
+ const frame = this.frames.get(frameId);
50
+ if (!frame) return false;
51
+
52
+ if (this.activeFrameId === frameId) {
53
+ this.activeFrameId = null;
54
+ }
55
+
56
+ frame.destroy();
57
+ this.frames.delete(frameId);
58
+
59
+ this.canvas.emit('focusFrameRemoved', { frameId });
60
+
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Get frame by ID
66
+ * @param {string} frameId - Frame ID
67
+ * @returns {FocusFrame|undefined} Frame instance
68
+ */
69
+ getFrame(frameId) {
70
+ return this.frames.get(frameId);
71
+ }
72
+
73
+ /**
74
+ * Set active frame
75
+ * @param {string} frameId - Frame ID
76
+ * @returns {boolean} True if frame was set as active
77
+ */
78
+ setActiveFrame(frameId) {
79
+ const frame = this.frames.get(frameId);
80
+ if (!frame) return false;
81
+
82
+ // Deactivate previous frame
83
+ if (this.activeFrameId) {
84
+ const prevFrame = this.frames.get(this.activeFrameId);
85
+ if (prevFrame) {
86
+ prevFrame.setActive(false);
87
+ }
88
+ }
89
+
90
+ // Activate new frame
91
+ this.activeFrameId = frameId;
92
+ frame.setActive(true);
93
+
94
+ this.canvas.emit('focusFrameActivated', { frameId, frame });
95
+
96
+ return true;
97
+ }
98
+
99
+ /**
100
+ * Get active frame
101
+ * @returns {FocusFrame|null} Active frame or null
102
+ */
103
+ getActiveFrame() {
104
+ return this.activeFrameId ? this.frames.get(this.activeFrameId) : null;
105
+ }
106
+
107
+ /**
108
+ * Capture content from active frame
109
+ * @returns {string|null} SVG content or null
110
+ */
111
+ captureActiveFrame() {
112
+ const activeFrame = this.getActiveFrame();
113
+ return activeFrame ? activeFrame.capture() : null;
114
+ }
115
+
116
+ /**
117
+ * Capture all frames
118
+ * @returns {Map<string, string>} Map of frame IDs to SVG content
119
+ */
120
+ captureAllFrames() {
121
+ const captures = new Map();
122
+
123
+ for (const [id, frame] of this.frames) {
124
+ captures.set(id, frame.capture());
125
+ }
126
+
127
+ return captures;
128
+ }
129
+
130
+ /**
131
+ * Clear all frames
132
+ */
133
+ clearAllFrames() {
134
+ for (const [id, frame] of this.frames) {
135
+ frame.destroy();
136
+ }
137
+
138
+ this.frames.clear();
139
+ this.activeFrameId = null;
140
+
141
+ this.canvas.emit('focusFramesCleared');
142
+ }
143
+
144
+ /**
145
+ * Get all frame IDs
146
+ * @returns {Array<string>} Array of frame IDs
147
+ */
148
+ getFrameIds() {
149
+ return Array.from(this.frames.keys());
150
+ }
151
+
152
+ /**
153
+ * Destroy focus frame manager
154
+ */
155
+ destroy() {
156
+ this.clearAllFrames();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Individual focus frame for capturing canvas regions
162
+ */
163
+ class FocusFrame {
164
+ /**
165
+ * @param {OMDCanvas} canvas - Canvas instance
166
+ * @param {string} id - Frame ID
167
+ * @param {Object} options - Frame options
168
+ */
169
+ constructor(canvas, id, options = {}) {
170
+ this.canvas = canvas;
171
+ this.id = id;
172
+ this.x = options.x || 0;
173
+ this.y = options.y || 0;
174
+ this.width = options.width || 200;
175
+ this.height = options.height || 150;
176
+ this.showOutline = options.showOutline !== false;
177
+ this.outlineColor = options.outlineColor || '#007bff';
178
+ this.outlineWidth = options.outlineWidth || 2;
179
+ this.outlineDashed = options.outlineDashed || false;
180
+ this.isActive = false;
181
+ this._createElement();
182
+ }
183
+ /**
184
+ * Create the visual frame element
185
+ * @private
186
+ */
187
+ _createElement() {
188
+ this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
189
+ this.element.setAttribute('class', 'focus-frame');
190
+ this.element.setAttribute('data-frame-id', this.id);
191
+ this.element.style.pointerEvents = 'none'; // No pointer events
192
+ this.element.style.zIndex = '1000';
193
+ if (this.showOutline) {
194
+ this.outline = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
195
+ this.outline.setAttribute('x', this.x);
196
+ this.outline.setAttribute('y', this.y);
197
+ this.outline.setAttribute('width', this.width);
198
+ this.outline.setAttribute('height', this.height);
199
+ this.outline.setAttribute('fill', 'none');
200
+ this.outline.setAttribute('stroke', this.outlineColor);
201
+ this.outline.setAttribute('stroke-width', this.outlineWidth);
202
+ if (this.outlineDashed) {
203
+ this.outline.setAttribute('stroke-dasharray', '5,5');
204
+ }
205
+ this.outline.style.pointerEvents = 'none';
206
+ this.element.appendChild(this.outline);
207
+ }
208
+ }
209
+ setActive(active) {
210
+ this.isActive = active;
211
+ if (this.outline) {
212
+ this.outline.setAttribute('stroke-width', this.outlineWidth);
213
+ }
214
+ }
215
+ capture() {
216
+ const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
217
+ tempSvg.setAttribute('width', this.width);
218
+ tempSvg.setAttribute('height', this.height);
219
+ tempSvg.setAttribute('viewBox', `${this.x} ${this.y} ${this.width} ${this.height}`);
220
+ const drawingLayer = this.canvas.drawingLayer.cloneNode(true);
221
+ tempSvg.appendChild(drawingLayer);
222
+ return new XMLSerializer().serializeToString(tempSvg);
223
+ }
224
+ async toBitmap(format = 'png', quality = 1) {
225
+ const svgData = this.capture();
226
+ const canvas = document.createElement('canvas');
227
+ canvas.width = this.width;
228
+ canvas.height = this.height;
229
+ const ctx = canvas.getContext('2d');
230
+ const img = new Image();
231
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
232
+ const url = URL.createObjectURL(svgBlob);
233
+ try {
234
+ await new Promise((resolve, reject) => {
235
+ img.onload = resolve;
236
+ img.onerror = reject;
237
+ img.src = url;
238
+ });
239
+ ctx.drawImage(img, 0, 0);
240
+ return new Promise(resolve => {
241
+ canvas.toBlob(resolve, `image/${format}`, quality);
242
+ });
243
+ } finally {
244
+ URL.revokeObjectURL(url);
245
+ }
246
+ }
247
+ async downloadAsBitmap(filename = `focus-frame-${this.id}.png`, format = 'png') {
248
+ try {
249
+ const blob = await this.toBitmap(format);
250
+ const url = URL.createObjectURL(blob);
251
+ const a = document.createElement('a');
252
+ a.href = url;
253
+ a.download = filename;
254
+ a.click();
255
+ URL.revokeObjectURL(url);
256
+ } catch (error) {
257
+ console.error('Failed to download frame:', error);
258
+ }
259
+ }
260
+ updateBounds(bounds) {
261
+ if (bounds.x !== undefined) this.x = bounds.x;
262
+ if (bounds.y !== undefined) this.y = bounds.y;
263
+ if (bounds.width !== undefined) this.width = bounds.width;
264
+ if (bounds.height !== undefined) this.height = bounds.height;
265
+ if (this.outline) {
266
+ this.outline.setAttribute('x', this.x);
267
+ this.outline.setAttribute('y', this.y);
268
+ this.outline.setAttribute('width', this.width);
269
+ this.outline.setAttribute('height', this.height);
270
+ }
271
+ }
272
+ getBounds() {
273
+ return {
274
+ x: this.x,
275
+ y: this.y,
276
+ width: this.width,
277
+ height: this.height
278
+ };
279
+ }
280
+ destroy() {
281
+ if (this.element.parentNode) {
282
+ this.element.parentNode.removeChild(this.element);
283
+ }
284
+ }
287
285
  }