@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,322 +1,321 @@
1
- import { Tool } from './tool.js';
2
- import { Stroke } from '../drawing/stroke.js';
3
-
4
- /**
5
- * Eraser tool for removing strokes
6
- */
7
- export class EraserTool extends Tool {
8
- constructor(canvas, options = {}) {
9
- super(canvas, {
10
- size: 12,
11
- hardness: 0.8,
12
- mode: 'radius', // 'stroke' or 'radius'
13
- ...options
14
- });
15
-
16
- this.displayName = 'Eraser';
17
- this.description = 'Erase strokes (M to toggle mode)';
18
- this.icon = 'eraser';
19
- this.shortcut = 'E';
20
- this.category = 'editing';
21
-
22
- // Eraser state
23
- this.isErasing = false;
24
- this.erasedPoints = new Set(); // Track erased points for radius mode
25
- }
26
-
27
- /**
28
- * Start erasing
29
- */
30
- onPointerDown(event) {
31
- if (!this.canUse()) return;
32
-
33
- this.isErasing = true;
34
- this._eraseAtPoint(event.x, event.y);
35
-
36
- this.canvas.emit('eraseStarted', {
37
- tool: this.name,
38
- point: { x: event.x, y: event.y }
39
- });
40
- }
41
-
42
- /**
43
- * Continue erasing
44
- */
45
- onPointerMove(event) {
46
- if (!this.isErasing) return;
47
-
48
- this._eraseAtPoint(event.x, event.y);
49
- }
50
-
51
- /**
52
- * Stop erasing
53
- */
54
- onPointerUp(event) {
55
- if (!this.isErasing) return;
56
-
57
- this.isErasing = false;
58
-
59
- this.canvas.emit('eraseCompleted', {
60
- tool: this.name
61
- });
62
- }
63
-
64
- /**
65
- * Cancel erasing
66
- */
67
- onCancel() {
68
- this.isErasing = false;
69
- super.onCancel();
70
- }
71
-
72
- /**
73
- * Erase strokes at point
74
- * @private
75
- */
76
- _eraseAtPoint(x, y) {
77
- if (this.config.mode === 'stroke') {
78
- this._eraseWholeStrokes(x, y);
79
- } else {
80
- this._eraseInRadius(x, y);
81
- }
82
- }
83
-
84
- /**
85
- * Erase whole strokes (original behavior)
86
- * @private
87
- */
88
- _eraseWholeStrokes(x, y) {
89
- const tolerance = this.config.size || 20;
90
- const strokesToRemove = [];
91
-
92
- // Check each stroke to see if it's near the eraser point
93
- for (const [id, stroke] of this.canvas.strokes) {
94
- if (stroke.isNearPoint(x, y, tolerance)) {
95
- strokesToRemove.push(id);
96
- }
97
- }
98
-
99
- // Remove the strokes
100
- strokesToRemove.forEach(id => {
101
- this.canvas.removeStroke(id);
102
- });
103
- }
104
-
105
- /**
106
- * Erase within radius (traditional eraser behavior)
107
- * @private
108
- */
109
- _eraseInRadius(x, y) {
110
- const radius = this.config.size || 20;
111
- const radiusSquared = radius * radius;
112
- const strokesToModify = [];
113
-
114
- // Find strokes that intersect with the eraser circle
115
- for (const [id, stroke] of this.canvas.strokes) {
116
- const boundingBox = stroke.getBoundingBox();
117
-
118
- // Quick bounding box check first
119
- if (this._circleIntersectsRect(x, y, radius, boundingBox)) {
120
- // Check individual points
121
- const pointsToRemove = [];
122
-
123
- for (let i = 0; i < stroke.points.length; i++) {
124
- const point = stroke.points[i];
125
- const dx = point.x - x;
126
- const dy = point.y - y;
127
- const distanceSquared = dx * dx + dy * dy;
128
-
129
- if (distanceSquared <= radiusSquared) {
130
- pointsToRemove.push(i);
131
- }
132
- }
133
-
134
- if (pointsToRemove.length > 0) {
135
- strokesToModify.push({ id, stroke, pointsToRemove });
136
- }
137
- }
138
- }
139
-
140
- // Modify or remove strokes
141
- strokesToModify.forEach(({ id, stroke, pointsToRemove }) => {
142
- if (pointsToRemove.length >= stroke.points.length * 0.8) {
143
- // If most points are erased, remove the whole stroke
144
- this.canvas.removeStroke(id);
145
- } else {
146
- // Remove points and split stroke if necessary
147
- this._splitStrokeAtErasedPoints(stroke, pointsToRemove);
148
- }
149
- });
150
- }
151
-
152
- /**
153
- * Check if circle intersects with rectangle
154
- * @private
155
- */
156
- _circleIntersectsRect(cx, cy, radius, rect) {
157
- const closestX = Math.max(rect.left, Math.min(cx, rect.right));
158
- const closestY = Math.max(rect.top, Math.min(cy, rect.bottom));
159
-
160
- const dx = cx - closestX;
161
- const dy = cy - closestY;
162
-
163
- return (dx * dx + dy * dy) <= (radius * radius);
164
- }
165
-
166
- /**
167
- * Split stroke at erased points or remove segments
168
- * @private
169
- */
170
- _splitStrokeAtErasedPoints(stroke, pointsToRemove) {
171
- if (pointsToRemove.length === 0) return;
172
-
173
- // Sort indices in ascending order for processing
174
- pointsToRemove.sort((a, b) => a - b);
175
-
176
- // Find continuous segments to keep
177
- const segments = [];
178
- let startIndex = 0;
179
-
180
- for (let i = 0; i < pointsToRemove.length; i++) {
181
- const removeIndex = pointsToRemove[i];
182
-
183
- // If there's a gap before this point, create a segment
184
- if (removeIndex > startIndex) {
185
- const segmentPoints = stroke.points.slice(startIndex, removeIndex);
186
- if (segmentPoints.length >= 2) {
187
- segments.push(segmentPoints);
188
- }
189
- }
190
-
191
- startIndex = removeIndex + 1;
192
- }
193
-
194
- // Add final segment if there are remaining points
195
- if (startIndex < stroke.points.length) {
196
- const finalSegment = stroke.points.slice(startIndex);
197
- if (finalSegment.length >= 2) {
198
- segments.push(finalSegment);
199
- }
200
- }
201
-
202
- // Remove original stroke
203
- this.canvas.removeStroke(stroke.id);
204
-
205
- // Create new strokes for each segment
206
- segments.forEach((segmentPoints, index) => {
207
- const newStroke = new Stroke({
208
- strokeWidth: stroke.strokeWidth,
209
- strokeColor: stroke.strokeColor,
210
- strokeOpacity: stroke.strokeOpacity,
211
- tool: stroke.tool
212
- });
213
-
214
- // Add all points to the new stroke
215
- segmentPoints.forEach(point => {
216
- newStroke.addPoint(point);
217
- });
218
-
219
- newStroke.finish();
220
- this.canvas.addStroke(newStroke);
221
- });
222
- }
223
-
224
- /**
225
- * Get eraser cursor
226
- */
227
- getCursor() {
228
- return 'eraser';
229
- }
230
-
231
- /**
232
- * Handle keyboard shortcuts
233
- */
234
- onKeyboardShortcut(key, event) {
235
- switch (key) {
236
- case '[':
237
- // Decrease eraser size
238
- this.updateConfig({
239
- size: Math.max(5, this.config.size - 5)
240
- });
241
- return true;
242
- case ']':
243
- // Increase eraser size
244
- this.updateConfig({
245
- size: Math.min(100, this.config.size + 5)
246
- });
247
- return true;
248
- case 'm':
249
- // Toggle eraser mode
250
- this.toggleMode();
251
- return true;
252
- default:
253
- return super.onKeyboardShortcut(key, event);
254
- }
255
- }
256
-
257
- /**
258
- * Toggle between stroke and radius erasing modes
259
- */
260
- toggleMode() {
261
- const newMode = this.config.mode === 'stroke' ? 'radius' : 'stroke';
262
- this.updateConfig({ mode: newMode });
263
-
264
- // Update cursor appearance
265
- if (this.canvas.cursor) {
266
- this.canvas.cursor.updateFromToolConfig(this.config);
267
- }
268
-
269
- // Emit mode change event
270
- this.canvas.emit('eraserModeChanged', {
271
- mode: newMode,
272
- description: this._getModeDescription(newMode)
273
- });
274
-
275
- console.log(`Eraser mode changed to: ${this._getModeDescription(newMode)}`);
276
- }
277
-
278
- /**
279
- * Get mode description
280
- * @private
281
- */
282
- _getModeDescription(mode) {
283
- return mode === 'stroke' ? 'Whole Stroke Erasing' : 'Radius Erasing';
284
- }
285
-
286
- /**
287
- * Update configuration
288
- */
289
- onConfigUpdate() {
290
- // Update cursor size if available
291
- if (this.canvas.cursor) {
292
- this.canvas.cursor.setSize(this.config.size);
293
- }
294
- }
295
-
296
- /**
297
- * Get help text
298
- */
299
- getHelpText() {
300
- return `${super.getHelpText()}\nShortcuts: [ ] to adjust size, M to toggle mode\nCurrent mode: ${this._getModeDescription(this.config.mode)}`;
301
- }
302
-
303
- /**
304
- * Get current eraser mode
305
- */
306
- getMode() {
307
- return this.config.mode;
308
- }
309
-
310
- /**
311
- * Set eraser mode
312
- * @param {string} mode - 'stroke' or 'radius'
313
- */
314
- setMode(mode) {
315
- if (mode === 'stroke' || mode === 'radius') {
316
- this.updateConfig({ mode });
317
- if (this.canvas.cursor) {
318
- this.canvas.cursor.updateFromToolConfig(this.config);
319
- }
320
- }
321
- }
322
- }
1
+ import { Tool } from './tool.js';
2
+ import { Stroke } from '../drawing/stroke.js';
3
+
4
+ /**
5
+ * Eraser tool for removing strokes
6
+ */
7
+ export class EraserTool extends Tool {
8
+ constructor(canvas, options = {}) {
9
+ super(canvas, {
10
+ size: 12,
11
+ hardness: 0.8,
12
+ mode: 'radius', // 'stroke' or 'radius'
13
+ ...options
14
+ });
15
+
16
+ this.displayName = 'Eraser';
17
+ this.description = 'Erase strokes (M to toggle mode)';
18
+ this.icon = 'eraser';
19
+ this.shortcut = 'E';
20
+ this.category = 'editing';
21
+
22
+ // Eraser state
23
+ this.isErasing = false;
24
+ this.erasedPoints = new Set(); // Track erased points for radius mode
25
+ }
26
+
27
+ /**
28
+ * Start erasing
29
+ */
30
+ onPointerDown(event) {
31
+ if (!this.canUse()) return;
32
+
33
+ this.isErasing = true;
34
+ this._eraseAtPoint(event.x, event.y);
35
+
36
+ this.canvas.emit('eraseStarted', {
37
+ tool: this.name,
38
+ point: { x: event.x, y: event.y }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Continue erasing
44
+ */
45
+ onPointerMove(event) {
46
+ if (!this.isErasing) return;
47
+
48
+ this._eraseAtPoint(event.x, event.y);
49
+ }
50
+
51
+ /**
52
+ * Stop erasing
53
+ */
54
+ onPointerUp(event) {
55
+ if (!this.isErasing) return;
56
+
57
+ this.isErasing = false;
58
+
59
+ this.canvas.emit('eraseCompleted', {
60
+ tool: this.name
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Cancel erasing
66
+ */
67
+ onCancel() {
68
+ this.isErasing = false;
69
+ super.onCancel();
70
+ }
71
+
72
+ /**
73
+ * Erase strokes at point
74
+ * @private
75
+ */
76
+ _eraseAtPoint(x, y) {
77
+ if (this.config.mode === 'stroke') {
78
+ this._eraseWholeStrokes(x, y);
79
+ } else {
80
+ this._eraseInRadius(x, y);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Erase whole strokes (original behavior)
86
+ * @private
87
+ */
88
+ _eraseWholeStrokes(x, y) {
89
+ const tolerance = this.config.size || 20;
90
+ const strokesToRemove = [];
91
+
92
+ // Check each stroke to see if it's near the eraser point
93
+ for (const [id, stroke] of this.canvas.strokes) {
94
+ if (stroke.isNearPoint(x, y, tolerance)) {
95
+ strokesToRemove.push(id);
96
+ }
97
+ }
98
+
99
+ // Remove the strokes
100
+ strokesToRemove.forEach(id => {
101
+ this.canvas.removeStroke(id);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Erase within radius (traditional eraser behavior)
107
+ * @private
108
+ */
109
+ _eraseInRadius(x, y) {
110
+ const radius = this.config.size || 20;
111
+ const radiusSquared = radius * radius;
112
+ const strokesToModify = [];
113
+
114
+ // Find strokes that intersect with the eraser circle
115
+ for (const [id, stroke] of this.canvas.strokes) {
116
+ const boundingBox = stroke.getBoundingBox();
117
+
118
+ // Quick bounding box check first
119
+ if (this._circleIntersectsRect(x, y, radius, boundingBox)) {
120
+ // Check individual points
121
+ const pointsToRemove = [];
122
+
123
+ for (let i = 0; i < stroke.points.length; i++) {
124
+ const point = stroke.points[i];
125
+ const dx = point.x - x;
126
+ const dy = point.y - y;
127
+ const distanceSquared = dx * dx + dy * dy;
128
+
129
+ if (distanceSquared <= radiusSquared) {
130
+ pointsToRemove.push(i);
131
+ }
132
+ }
133
+
134
+ if (pointsToRemove.length > 0) {
135
+ strokesToModify.push({ id, stroke, pointsToRemove });
136
+ }
137
+ }
138
+ }
139
+
140
+ // Modify or remove strokes
141
+ strokesToModify.forEach(({ id, stroke, pointsToRemove }) => {
142
+ if (pointsToRemove.length >= stroke.points.length * 0.8) {
143
+ // If most points are erased, remove the whole stroke
144
+ this.canvas.removeStroke(id);
145
+ } else {
146
+ // Remove points and split stroke if necessary
147
+ this._splitStrokeAtErasedPoints(stroke, pointsToRemove);
148
+ }
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Check if circle intersects with rectangle
154
+ * @private
155
+ */
156
+ _circleIntersectsRect(cx, cy, radius, rect) {
157
+ const closestX = Math.max(rect.left, Math.min(cx, rect.right));
158
+ const closestY = Math.max(rect.top, Math.min(cy, rect.bottom));
159
+
160
+ const dx = cx - closestX;
161
+ const dy = cy - closestY;
162
+
163
+ return (dx * dx + dy * dy) <= (radius * radius);
164
+ }
165
+
166
+ /**
167
+ * Split stroke at erased points or remove segments
168
+ * @private
169
+ */
170
+ _splitStrokeAtErasedPoints(stroke, pointsToRemove) {
171
+ if (pointsToRemove.length === 0) return;
172
+
173
+ // Sort indices in ascending order for processing
174
+ pointsToRemove.sort((a, b) => a - b);
175
+
176
+ // Find continuous segments to keep
177
+ const segments = [];
178
+ let startIndex = 0;
179
+
180
+ for (let i = 0; i < pointsToRemove.length; i++) {
181
+ const removeIndex = pointsToRemove[i];
182
+
183
+ // If there's a gap before this point, create a segment
184
+ if (removeIndex > startIndex) {
185
+ const segmentPoints = stroke.points.slice(startIndex, removeIndex);
186
+ if (segmentPoints.length >= 2) {
187
+ segments.push(segmentPoints);
188
+ }
189
+ }
190
+
191
+ startIndex = removeIndex + 1;
192
+ }
193
+
194
+ // Add final segment if there are remaining points
195
+ if (startIndex < stroke.points.length) {
196
+ const finalSegment = stroke.points.slice(startIndex);
197
+ if (finalSegment.length >= 2) {
198
+ segments.push(finalSegment);
199
+ }
200
+ }
201
+
202
+ // Remove original stroke
203
+ this.canvas.removeStroke(stroke.id);
204
+
205
+ // Create new strokes for each segment
206
+ segments.forEach((segmentPoints, index) => {
207
+ const newStroke = new Stroke({
208
+ strokeWidth: stroke.strokeWidth,
209
+ strokeColor: stroke.strokeColor,
210
+ strokeOpacity: stroke.strokeOpacity,
211
+ tool: stroke.tool
212
+ });
213
+
214
+ // Add all points to the new stroke
215
+ segmentPoints.forEach(point => {
216
+ newStroke.addPoint(point);
217
+ });
218
+
219
+ newStroke.finish();
220
+ this.canvas.addStroke(newStroke);
221
+ });
222
+ }
223
+
224
+ /**
225
+ * Get eraser cursor
226
+ */
227
+ getCursor() {
228
+ return 'eraser';
229
+ }
230
+
231
+ /**
232
+ * Handle keyboard shortcuts
233
+ */
234
+ onKeyboardShortcut(key, event) {
235
+ switch (key) {
236
+ case '[':
237
+ // Decrease eraser size
238
+ this.updateConfig({
239
+ size: Math.max(5, this.config.size - 5)
240
+ });
241
+ return true;
242
+ case ']':
243
+ // Increase eraser size
244
+ this.updateConfig({
245
+ size: Math.min(100, this.config.size + 5)
246
+ });
247
+ return true;
248
+ case 'm':
249
+ // Toggle eraser mode
250
+ this.toggleMode();
251
+ return true;
252
+ default:
253
+ return super.onKeyboardShortcut(key, event);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Toggle between stroke and radius erasing modes
259
+ */
260
+ toggleMode() {
261
+ const newMode = this.config.mode === 'stroke' ? 'radius' : 'stroke';
262
+ this.updateConfig({ mode: newMode });
263
+
264
+ // Update cursor appearance
265
+ if (this.canvas.cursor) {
266
+ this.canvas.cursor.updateFromToolConfig(this.config);
267
+ }
268
+
269
+ // Emit mode change event
270
+ this.canvas.emit('eraserModeChanged', {
271
+ mode: newMode,
272
+ description: this._getModeDescription(newMode)
273
+ });
274
+
275
+ }
276
+
277
+ /**
278
+ * Get mode description
279
+ * @private
280
+ */
281
+ _getModeDescription(mode) {
282
+ return mode === 'stroke' ? 'Whole Stroke Erasing' : 'Radius Erasing';
283
+ }
284
+
285
+ /**
286
+ * Update configuration
287
+ */
288
+ onConfigUpdate() {
289
+ // Update cursor size if available
290
+ if (this.canvas.cursor) {
291
+ this.canvas.cursor.setSize(this.config.size);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Get help text
297
+ */
298
+ getHelpText() {
299
+ return `${super.getHelpText()}\nShortcuts: [ ] to adjust size, M to toggle mode\nCurrent mode: ${this._getModeDescription(this.config.mode)}`;
300
+ }
301
+
302
+ /**
303
+ * Get current eraser mode
304
+ */
305
+ getMode() {
306
+ return this.config.mode;
307
+ }
308
+
309
+ /**
310
+ * Set eraser mode
311
+ * @param {string} mode - 'stroke' or 'radius'
312
+ */
313
+ setMode(mode) {
314
+ if (mode === 'stroke' || mode === 'radius') {
315
+ this.updateConfig({ mode });
316
+ if (this.canvas.cursor) {
317
+ this.canvas.cursor.updateFromToolConfig(this.config);
318
+ }
319
+ }
320
+ }
321
+ }