@teachinglab/omd 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.
Files changed (144) hide show
  1. package/README.md +138 -0
  2. package/canvas/core/canvasConfig.js +203 -0
  3. package/canvas/core/omdCanvas.js +475 -0
  4. package/canvas/drawing/segment.js +168 -0
  5. package/canvas/drawing/stroke.js +386 -0
  6. package/canvas/events/eventManager.js +435 -0
  7. package/canvas/events/pointerEventHandler.js +263 -0
  8. package/canvas/features/focusFrameManager.js +287 -0
  9. package/canvas/index.js +49 -0
  10. package/canvas/tools/eraserTool.js +322 -0
  11. package/canvas/tools/pencilTool.js +319 -0
  12. package/canvas/tools/selectTool.js +457 -0
  13. package/canvas/tools/tool.js +223 -0
  14. package/canvas/tools/toolManager.js +394 -0
  15. package/canvas/ui/cursor.js +438 -0
  16. package/canvas/ui/toolbar.js +304 -0
  17. package/canvas/utils/boundingBox.js +378 -0
  18. package/canvas/utils/mathUtils.js +259 -0
  19. package/docs/api/configuration-options.md +104 -0
  20. package/docs/api/eventManager.md +68 -0
  21. package/docs/api/focusFrameManager.md +150 -0
  22. package/docs/api/index.md +91 -0
  23. package/docs/api/main.md +58 -0
  24. package/docs/api/omdBinaryExpressionNode.md +227 -0
  25. package/docs/api/omdCanvas.md +142 -0
  26. package/docs/api/omdConfigManager.md +192 -0
  27. package/docs/api/omdConstantNode.md +117 -0
  28. package/docs/api/omdDisplay.md +121 -0
  29. package/docs/api/omdEquationNode.md +161 -0
  30. package/docs/api/omdEquationSequenceNode.md +301 -0
  31. package/docs/api/omdEquationStack.md +139 -0
  32. package/docs/api/omdFunctionNode.md +141 -0
  33. package/docs/api/omdGroupNode.md +182 -0
  34. package/docs/api/omdHelpers.md +96 -0
  35. package/docs/api/omdLeafNode.md +163 -0
  36. package/docs/api/omdNode.md +101 -0
  37. package/docs/api/omdOperationDisplayNode.md +139 -0
  38. package/docs/api/omdOperatorNode.md +127 -0
  39. package/docs/api/omdParenthesisNode.md +122 -0
  40. package/docs/api/omdPopup.md +117 -0
  41. package/docs/api/omdPowerNode.md +127 -0
  42. package/docs/api/omdRationalNode.md +128 -0
  43. package/docs/api/omdSequenceNode.md +128 -0
  44. package/docs/api/omdSimplification.md +110 -0
  45. package/docs/api/omdSqrtNode.md +79 -0
  46. package/docs/api/omdStepVisualizer.md +115 -0
  47. package/docs/api/omdStepVisualizerHighlighting.md +61 -0
  48. package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
  49. package/docs/api/omdStepVisualizerLayout.md +60 -0
  50. package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
  51. package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
  52. package/docs/api/omdToolbar.md +102 -0
  53. package/docs/api/omdTranscriptionService.md +76 -0
  54. package/docs/api/omdTreeDiff.md +134 -0
  55. package/docs/api/omdUnaryExpressionNode.md +174 -0
  56. package/docs/api/omdUtilities.md +70 -0
  57. package/docs/api/omdVariableNode.md +148 -0
  58. package/docs/api/selectTool.md +74 -0
  59. package/docs/api/simplificationEngine.md +98 -0
  60. package/docs/api/simplificationRules.md +77 -0
  61. package/docs/api/simplificationUtils.md +64 -0
  62. package/docs/api/transcribe.md +43 -0
  63. package/docs/api-reference.md +85 -0
  64. package/docs/index.html +454 -0
  65. package/docs/user-guide.md +9 -0
  66. package/index.js +67 -0
  67. package/omd/config/omdConfigManager.js +267 -0
  68. package/omd/core/index.js +150 -0
  69. package/omd/core/omdEquationStack.js +347 -0
  70. package/omd/core/omdUtilities.js +115 -0
  71. package/omd/display/omdDisplay.js +443 -0
  72. package/omd/display/omdToolbar.js +502 -0
  73. package/omd/nodes/omdBinaryExpressionNode.js +460 -0
  74. package/omd/nodes/omdConstantNode.js +142 -0
  75. package/omd/nodes/omdEquationNode.js +1223 -0
  76. package/omd/nodes/omdEquationSequenceNode.js +1273 -0
  77. package/omd/nodes/omdFunctionNode.js +352 -0
  78. package/omd/nodes/omdGroupNode.js +68 -0
  79. package/omd/nodes/omdLeafNode.js +77 -0
  80. package/omd/nodes/omdNode.js +557 -0
  81. package/omd/nodes/omdOperationDisplayNode.js +322 -0
  82. package/omd/nodes/omdOperatorNode.js +109 -0
  83. package/omd/nodes/omdParenthesisNode.js +293 -0
  84. package/omd/nodes/omdPowerNode.js +236 -0
  85. package/omd/nodes/omdRationalNode.js +295 -0
  86. package/omd/nodes/omdSqrtNode.js +308 -0
  87. package/omd/nodes/omdUnaryExpressionNode.js +178 -0
  88. package/omd/nodes/omdVariableNode.js +123 -0
  89. package/omd/simplification/omdSimplification.js +171 -0
  90. package/omd/simplification/omdSimplificationEngine.js +886 -0
  91. package/omd/simplification/package.json +6 -0
  92. package/omd/simplification/rules/binaryRules.js +1037 -0
  93. package/omd/simplification/rules/functionRules.js +111 -0
  94. package/omd/simplification/rules/index.js +48 -0
  95. package/omd/simplification/rules/parenthesisRules.js +19 -0
  96. package/omd/simplification/rules/powerRules.js +143 -0
  97. package/omd/simplification/rules/rationalRules.js +475 -0
  98. package/omd/simplification/rules/sqrtRules.js +48 -0
  99. package/omd/simplification/rules/unaryRules.js +37 -0
  100. package/omd/simplification/simplificationRules.js +32 -0
  101. package/omd/simplification/simplificationUtils.js +1056 -0
  102. package/omd/step-visualizer/omdStepVisualizer.js +597 -0
  103. package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
  104. package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
  105. package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
  106. package/omd/utils/omdNodeOverlay.js +638 -0
  107. package/omd/utils/omdPopup.js +1084 -0
  108. package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
  109. package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
  110. package/omd/utils/omdTranscriptionService.js +125 -0
  111. package/omd/utils/omdTreeDiff.js +734 -0
  112. package/package.json +46 -0
  113. package/src/index.js +62 -0
  114. package/src/json-schemas.md +109 -0
  115. package/src/omd-json-samples.js +115 -0
  116. package/src/omd.js +109 -0
  117. package/src/omdApp.js +391 -0
  118. package/src/omdAppCanvas.js +336 -0
  119. package/src/omdBalanceHanger.js +172 -0
  120. package/src/omdColor.js +13 -0
  121. package/src/omdCoordinatePlane.js +467 -0
  122. package/src/omdEquation.js +125 -0
  123. package/src/omdExpression.js +104 -0
  124. package/src/omdFunction.js +113 -0
  125. package/src/omdMetaExpression.js +287 -0
  126. package/src/omdNaturalExpression.js +564 -0
  127. package/src/omdNode.js +384 -0
  128. package/src/omdNumber.js +53 -0
  129. package/src/omdNumberLine.js +107 -0
  130. package/src/omdNumberTile.js +119 -0
  131. package/src/omdOperator.js +73 -0
  132. package/src/omdPowerExpression.js +92 -0
  133. package/src/omdProblem.js +55 -0
  134. package/src/omdRatioChart.js +232 -0
  135. package/src/omdRationalExpression.js +115 -0
  136. package/src/omdSampleData.js +215 -0
  137. package/src/omdShapes.js +476 -0
  138. package/src/omdSpinner.js +148 -0
  139. package/src/omdString.js +39 -0
  140. package/src/omdTable.js +369 -0
  141. package/src/omdTapeDiagram.js +245 -0
  142. package/src/omdTerm.js +92 -0
  143. package/src/omdTileEquation.js +349 -0
  144. package/src/omdVariable.js +51 -0
@@ -0,0 +1,287 @@
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
+ }
287
+ }
@@ -0,0 +1,49 @@
1
+ // Core canvas system
2
+ export { omdCanvas } from './core/omdCanvas.js';
3
+ export { CanvasConfig as canvasConfig } from './core/canvasConfig.js';
4
+
5
+ // Event handling
6
+ export { EventManager } from './events/eventManager.js';
7
+ export { pointerEventHandler } from './events/pointerEventHandler.js';
8
+
9
+ // Tool system
10
+ export { ToolManager } from './tools/toolManager.js';
11
+ export { Tool } from './tools/tool.js';
12
+ export { PencilTool } from './tools/PencilTool.js';
13
+ export { EraserTool } from './tools/EraserTool.js';
14
+ export { SelectTool } from './tools/SelectTool.js';
15
+
16
+ // UI components
17
+ export { Toolbar } from './ui/toolbar.js';
18
+ export { Cursor } from './ui/cursor.js';
19
+
20
+ // Drawing objects
21
+ export { Stroke } from './drawing/stroke.js';
22
+ export { segment } from './drawing/Segment.js';
23
+
24
+ // Utilities
25
+ export { BoundingBox } from './utils/boundingBox.js';
26
+ export { mathUtils } from './utils/mathUtils.js';
27
+
28
+ // Focus frame system
29
+ export { FocusFrameManager } from './features/focusFrameManager.js';
30
+ import { omdCanvas } from './core/omdCanvas.js';
31
+ /**
32
+ * Quick setup function for common use cases
33
+ * @param {HTMLElement|string} container - Container element or selector
34
+ * @param {Object} options - Configuration options
35
+ * @returns {omdCanvas} Configured canvas instance
36
+ */
37
+ export function createCanvas(container, options = {}) {
38
+ const canvas = new omdCanvas(container, options);
39
+ return canvas;
40
+ }
41
+
42
+ /**
43
+ * Create multiple canvas instances for comparison or multi-canvas apps
44
+ * @param {Array} configs - Array of {container, options} objects
45
+ * @returns {Array<omdCanvas>} Array of canvas instances
46
+ */
47
+ export function createMultipleCanvases(configs) {
48
+ return configs.map(config => createCanvas(config.container, config.options));
49
+ }
@@ -0,0 +1,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
+ 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
+ }