@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,457 @@
1
+ import { Tool } from './tool.js';
2
+ import { BoundingBox } from '../utils/boundingBox.js';
3
+ import { Stroke } from '../drawing/stroke.js';
4
+ import {omdColor} from '../../src/omdColor.js';
5
+ const SELECTION_TOLERANCE = 10;
6
+ const SELECTION_COLOR = '#007bff';
7
+ const SELECTION_OPACITY = 0.3;
8
+
9
+ /**
10
+ * A tool for selecting, moving, and deleting stroke segments.
11
+ * @extends Tool
12
+ */
13
+ export class SelectTool extends Tool {
14
+ /**
15
+ * @param {OMDCanvas} canvas - The canvas instance.
16
+ * @param {object} [options={}] - Configuration options for the tool.
17
+ */
18
+ constructor(canvas, options = {}) {
19
+ super(canvas, {
20
+ selectionColor: SELECTION_COLOR,
21
+ selectionOpacity: SELECTION_OPACITY,
22
+ ...options
23
+ });
24
+
25
+ this.displayName = 'Select';
26
+ this.description = 'Select and manipulate segments';
27
+ this.icon = 'select';
28
+ this.shortcut = 'S';
29
+ this.category = 'selection';
30
+
31
+ /** @private */
32
+ this.isSelecting = false;
33
+ /** @private */
34
+ this.selectionBox = null;
35
+ /** @private */
36
+ this.startPoint = null;
37
+ /** @type {Map<string, Set<number>>} */
38
+ this.selectedSegments = new Map();
39
+ }
40
+
41
+ /**
42
+ * Handles the pointer down event to start a selection.
43
+ * @param {PointerEvent} event - The pointer event.
44
+ */
45
+ onPointerDown(event) {
46
+ if (!this.canUse()) return;
47
+
48
+ this.startPoint = { x: event.x, y: event.y };
49
+ const segmentSelection = this._findSegmentAtPoint(event.x, event.y);
50
+
51
+ if (segmentSelection) {
52
+ this._handleSegmentClick(segmentSelection, event.shiftKey);
53
+ } else {
54
+ this._startBoxSelection(event.x, event.y, event.shiftKey);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Handles the pointer move event to update the selection box.
60
+ * @param {PointerEvent} event - The pointer event.
61
+ */
62
+ onPointerMove(event) {
63
+ if (!this.isSelecting || !this.selectionBox) return;
64
+
65
+ this._updateSelectionBox(event.x, event.y);
66
+ this._updateBoxSelection();
67
+ }
68
+
69
+ /**
70
+ * Handles the pointer up event to complete the selection.
71
+ * @param {PointerEvent} event - The pointer event.
72
+ */
73
+ onPointerUp(event) {
74
+ if (this.isSelecting) {
75
+ this._finishBoxSelection();
76
+ }
77
+ this.isSelecting = false;
78
+ this._removeSelectionBox();
79
+ }
80
+
81
+ /**
82
+ * Cancels the current selection operation.
83
+ */
84
+ onCancel() {
85
+ this.isSelecting = false;
86
+ this._removeSelectionBox();
87
+ this.clearSelection();
88
+ super.onCancel();
89
+ }
90
+
91
+ /**
92
+ * Handles keyboard shortcuts for selection-related actions.
93
+ * @param {string} key - The key that was pressed.
94
+ * @param {KeyboardEvent} event - The keyboard event.
95
+ * @returns {boolean} - True if the shortcut was handled, false otherwise.
96
+ */
97
+ onKeyboardShortcut(key, event) {
98
+ if (event.ctrlKey || event.metaKey) {
99
+ if (key === 'a') {
100
+ this._selectAllSegments();
101
+ return true;
102
+ }
103
+ }
104
+
105
+ if (key === 'delete' || key === 'backspace') {
106
+ this._deleteSelectedSegments();
107
+ return true;
108
+ }
109
+
110
+ return false;
111
+ }
112
+
113
+ /**
114
+ * Gets the cursor for the tool.
115
+ * @returns {string} The CSS cursor name.
116
+ */
117
+ getCursor() {
118
+ return 'default';
119
+ }
120
+
121
+ /**
122
+ * Clears the current selection.
123
+ */
124
+ clearSelection() {
125
+ this.selectedSegments.clear();
126
+ this._updateSegmentSelectionVisuals();
127
+ this.canvas.emit('selectionChanged', { selected: [] });
128
+ }
129
+
130
+ /**
131
+ * @private
132
+ */
133
+ _handleSegmentClick({ strokeId, segmentIndex }, shiftKey) {
134
+ const segmentSet = this.selectedSegments.get(strokeId) || new Set();
135
+ if (segmentSet.has(segmentIndex)) {
136
+ segmentSet.delete(segmentIndex);
137
+ if (segmentSet.size === 0) {
138
+ this.selectedSegments.delete(strokeId);
139
+ }
140
+ } else {
141
+ if (!shiftKey) {
142
+ this.selectedSegments.clear();
143
+ }
144
+ if (!this.selectedSegments.has(strokeId)) {
145
+ this.selectedSegments.set(strokeId, new Set());
146
+ }
147
+ this.selectedSegments.get(strokeId).add(segmentIndex);
148
+ }
149
+ this._updateSegmentSelectionVisuals();
150
+ this.canvas.emit('selectionChanged', { selected: this._getSelectedSegmentsAsArray() });
151
+ }
152
+
153
+ /**
154
+ * @private
155
+ */
156
+ _startBoxSelection(x, y, shiftKey) {
157
+ if (!shiftKey) {
158
+ this.clearSelection();
159
+ }
160
+ this.isSelecting = true;
161
+ this._createSelectionBox(x, y);
162
+ }
163
+
164
+ /**
165
+ * @private
166
+ */
167
+ _finishBoxSelection() {
168
+ this._updateSegmentSelectionVisuals();
169
+ this.canvas.emit('selectionChanged', { selected: this._getSelectedSegmentsAsArray() });
170
+ }
171
+
172
+ /**
173
+ * @private
174
+ */
175
+ _getSelectedSegmentsAsArray() {
176
+ const selected = [];
177
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
178
+ for (const segmentIndex of segmentSet) {
179
+ selected.push({ strokeId, segmentIndex });
180
+ }
181
+ }
182
+ return selected;
183
+ }
184
+
185
+ /**
186
+ * Finds the closest segment to a given point.
187
+ * @private
188
+ * @param {number} x - The x-coordinate of the point.
189
+ * @param {number} y - The y-coordinate of the point.
190
+ * @returns {{strokeId: string, segmentIndex: number}|null}
191
+ */
192
+ _findSegmentAtPoint(x, y) {
193
+ let closest = null;
194
+ let minDist = SELECTION_TOLERANCE;
195
+ for (const [id, stroke] of this.canvas.strokes) {
196
+ if (!stroke.points || stroke.points.length < 2) continue;
197
+ for (let i = 0; i < stroke.points.length - 1; i++) {
198
+ const p1 = stroke.points[i];
199
+ const p2 = stroke.points[i + 1];
200
+ const dist = this._pointToSegmentDistance(x, y, p1, p2);
201
+ if (dist < minDist) {
202
+ minDist = dist;
203
+ closest = { strokeId: id, segmentIndex: i };
204
+ }
205
+ }
206
+ }
207
+ return closest;
208
+ }
209
+
210
+ /**
211
+ * Calculates the distance from a point to a line segment.
212
+ * @private
213
+ */
214
+ _pointToSegmentDistance(x, y, p1, p2) {
215
+ const l2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
216
+ if (l2 === 0) return Math.hypot(x - p1.x, y - p1.y);
217
+ let t = ((x - p1.x) * (p2.x - p1.x) + (y - p1.y) * (p2.y - p1.y)) / l2;
218
+ t = Math.max(0, Math.min(1, t));
219
+ const projX = p1.x + t * (p2.x - p1.x);
220
+ const projY = p1.y + t * (p2.y - p1.y);
221
+ return Math.hypot(x - projX, y - projY);
222
+ }
223
+
224
+ /**
225
+ * Creates the selection box element.
226
+ * @private
227
+ */
228
+ _createSelectionBox(x, y) {
229
+ this.selectionBox = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
230
+ this.selectionBox.setAttribute('x', x);
231
+ this.selectionBox.setAttribute('y', y);
232
+ this.selectionBox.setAttribute('width', 0);
233
+ this.selectionBox.setAttribute('height', 0);
234
+ this.selectionBox.setAttribute('fill', this.config.selectionColor);
235
+ this.selectionBox.setAttribute('fill-opacity', this.config.selectionOpacity);
236
+ this.selectionBox.setAttribute('stroke', this.config.selectionColor);
237
+ this.selectionBox.setAttribute('stroke-width', 1);
238
+ this.selectionBox.setAttribute('stroke-dasharray', '5,5');
239
+ this.selectionBox.style.pointerEvents = 'none';
240
+ this.canvas.uiLayer.appendChild(this.selectionBox);
241
+ }
242
+
243
+ /**
244
+ * Updates the dimensions of the selection box.
245
+ * @private
246
+ */
247
+ _updateSelectionBox(x, y) {
248
+ if (!this.selectionBox || !this.startPoint) return;
249
+ const minX = Math.min(this.startPoint.x, x);
250
+ const minY = Math.min(this.startPoint.y, y);
251
+ const width = Math.abs(this.startPoint.x - x);
252
+ const height = Math.abs(this.startPoint.y - y);
253
+ this.selectionBox.setAttribute('x', minX);
254
+ this.selectionBox.setAttribute('y', minY);
255
+ this.selectionBox.setAttribute('width', width);
256
+ this.selectionBox.setAttribute('height', height);
257
+ }
258
+
259
+ /**
260
+ * Removes the selection box element.
261
+ * @private
262
+ */
263
+ _removeSelectionBox() {
264
+ if (this.selectionBox) {
265
+ this.selectionBox.remove();
266
+ this.selectionBox = null;
267
+ }
268
+ this.startPoint = null;
269
+ }
270
+
271
+ /**
272
+ * Updates the set of selected segments based on the selection box.
273
+ * @private
274
+ */
275
+ _updateBoxSelection() {
276
+ if (!this.selectionBox) return;
277
+
278
+ const x = parseFloat(this.selectionBox.getAttribute('x'));
279
+ const y = parseFloat(this.selectionBox.getAttribute('y'));
280
+ const width = parseFloat(this.selectionBox.getAttribute('width'));
281
+ const height = parseFloat(this.selectionBox.getAttribute('height'));
282
+ const selectionBounds = new BoundingBox(x, y, width, height);
283
+
284
+ for (const [id, stroke] of this.canvas.strokes) {
285
+ if (!stroke.points || stroke.points.length < 2) continue;
286
+ for (let i = 0; i < stroke.points.length - 1; i++) {
287
+ const p1 = stroke.points[i];
288
+ const p2 = stroke.points[i + 1];
289
+ if (this._segmentIntersectsBox(p1, p2, selectionBounds)) {
290
+ if (!this.selectedSegments.has(id)) {
291
+ this.selectedSegments.set(id, new Set());
292
+ }
293
+ this.selectedSegments.get(id).add(i);
294
+ }
295
+ }
296
+ }
297
+ this._updateSegmentSelectionVisuals();
298
+ }
299
+
300
+ /**
301
+ * Checks if a line segment intersects with a bounding box.
302
+ * @private
303
+ * @param {{x: number, y: number}} p1 - The start point of the segment.
304
+ * @param {{x: number, y: number}} p2 - The end point of the segment.
305
+ * @param {BoundingBox} box - The bounding box.
306
+ * @returns {boolean}
307
+ */
308
+ _segmentIntersectsBox(p1, p2, box) {
309
+ if (box.containsPoint(p1.x, p1.y) || box.containsPoint(p2.x, p2.y)) {
310
+ return true;
311
+ }
312
+
313
+ const { x, y, width, height } = box;
314
+ const right = x + width;
315
+ const bottom = y + height;
316
+
317
+ const lines = [
318
+ { a: { x, y }, b: { x: right, y } },
319
+ { a: { x: right, y }, b: { x: right, y: bottom } },
320
+ { a: { x: right, y: bottom }, b: { x, y: bottom } },
321
+ { a: { x, y: bottom }, b: { x, y } }
322
+ ];
323
+
324
+ for (const line of lines) {
325
+ if (this._lineIntersectsLine(p1, p2, line.a, line.b)) {
326
+ return true;
327
+ }
328
+ }
329
+ return false;
330
+ }
331
+
332
+ /**
333
+ * Checks if two line segments intersect.
334
+ * @private
335
+ */
336
+ _lineIntersectsLine(p1, p2, p3, p4) {
337
+ const det = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
338
+ if (det === 0) return false;
339
+ const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / det;
340
+ const u = -((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) / det;
341
+ return t >= 0 && t <= 1 && u >= 0 && u <= 1;
342
+ }
343
+
344
+ /**
345
+ * Updates the visual representation of selected segments.
346
+ * @private
347
+ */
348
+ _updateSegmentSelectionVisuals() {
349
+ const selectionLayer = this.canvas.uiLayer.querySelector('.segment-selection-layer') || this._createSelectionLayer();
350
+ selectionLayer.innerHTML = '';
351
+
352
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
353
+ const stroke = this.canvas.strokes.get(strokeId);
354
+ if (!stroke || !stroke.points) continue;
355
+
356
+ for (const segmentIndex of segmentSet) {
357
+ if (segmentIndex >= stroke.points.length - 1) continue;
358
+ const p1 = stroke.points[segmentIndex];
359
+ const p2 = stroke.points[segmentIndex + 1];
360
+ const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'line');
361
+ highlight.setAttribute('x1', p1.x);
362
+ highlight.setAttribute('y1', p1.y);
363
+ highlight.setAttribute('x2', p2.x);
364
+ highlight.setAttribute('y2', p2.y);
365
+ highlight.setAttribute('stroke', omdColor.hiliteColor);
366
+ highlight.setAttribute('stroke-width', '3');
367
+ highlight.setAttribute('stroke-opacity', '0.7');
368
+ highlight.style.pointerEvents = 'none';
369
+ selectionLayer.appendChild(highlight);
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * @private
376
+ */
377
+ _createSelectionLayer() {
378
+ const layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
379
+ layer.classList.add('segment-selection-layer');
380
+ this.canvas.uiLayer.appendChild(layer);
381
+ return layer;
382
+ }
383
+
384
+ /**
385
+ * Selects all segments on the canvas.
386
+ * @private
387
+ */
388
+ _selectAllSegments() {
389
+ this.selectedSegments.clear();
390
+ for (const [id, stroke] of this.canvas.strokes) {
391
+ if (!stroke.points || stroke.points.length < 2) continue;
392
+ const segmentIndices = new Set();
393
+ for (let i = 0; i < stroke.points.length - 1; i++) {
394
+ segmentIndices.add(i);
395
+ }
396
+ this.selectedSegments.set(id, segmentIndices);
397
+ }
398
+ this._updateSegmentSelectionVisuals();
399
+ }
400
+
401
+ /**
402
+ * Deletes all currently selected segments using the eraser tool.
403
+ * @private
404
+ */
405
+ _deleteSelectedSegments() {
406
+ const eraser = this.canvas.toolManager?.getTool('eraser') || this.canvas.eraserTool;
407
+ if (!eraser || typeof eraser._eraseInRadius !== 'function') {
408
+ console.warn('Eraser tool not found or _eraseInRadius not available');
409
+ return;
410
+ }
411
+
412
+ const originalRadius = eraser.config.size;
413
+ const eraseRadius = 15;
414
+ eraser.config.size = eraseRadius;
415
+
416
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
417
+ const stroke = this.canvas.strokes.get(strokeId);
418
+ if (!stroke || !stroke.points) continue;
419
+
420
+ for (const segmentIndex of segmentSet) {
421
+ if (segmentIndex >= stroke.points.length - 1) continue;
422
+ const p1 = stroke.points[segmentIndex];
423
+ const p2 = stroke.points[segmentIndex + 1];
424
+
425
+ // Erase along the segment with 30 points
426
+ const numPoints = 30;
427
+ for (let i = 0; i <= numPoints; i++) {
428
+ const t = i / numPoints;
429
+ const x = p1.x + t * (p2.x - p1.x);
430
+ const y = p1.y + t * (p2.y - p1.y);
431
+ eraser._eraseInRadius(x, y);
432
+ }
433
+ }
434
+ }
435
+
436
+ eraser.config.size = originalRadius; // Restore original radius
437
+ this.clearSelection();
438
+ }
439
+
440
+ /**
441
+ * Creates a new stroke from a set of points.
442
+ * @private
443
+ * @param {Array<object>} points - The points for the new stroke.
444
+ * @param {Stroke} originalStroke - The original stroke to copy properties from.
445
+ */
446
+ _createNewStroke(points, originalStroke) {
447
+ const newStroke = new Stroke({
448
+ strokeWidth: originalStroke.strokeWidth,
449
+ strokeColor: originalStroke.strokeColor,
450
+ strokeOpacity: originalStroke.strokeOpacity,
451
+ tool: originalStroke.tool,
452
+ });
453
+ newStroke.points = points;
454
+ newStroke.finish();
455
+ this.canvas.addStroke(newStroke);
456
+ }
457
+ }
@@ -0,0 +1,223 @@
1
+ import { Stroke } from '../drawing/stroke.js';
2
+
3
+ /**
4
+ * Base class for all canvas tools
5
+ * All tools should extend this class and implement the required methods
6
+ */
7
+ export class Tool {
8
+ /**
9
+ * @param {OMDCanvas} canvas - Canvas instance
10
+ * @param {Object} options - Tool options
11
+ */
12
+ constructor(canvas, options = {}) {
13
+ this.canvas = canvas;
14
+ this.name = '';
15
+ this.displayName = '';
16
+ this.description = '';
17
+ this.icon = '';
18
+ this.shortcut = '';
19
+ this.category = 'general';
20
+
21
+ // Tool state
22
+ this.isActive = false;
23
+ this.isDrawing = false;
24
+ this.currentStroke = null;
25
+
26
+ // Configuration
27
+ this.config = {
28
+ strokeWidth: 5,
29
+ strokeColor: '#000000',
30
+ strokeOpacity: 1,
31
+ ...options
32
+ };
33
+
34
+ // Bind methods
35
+ this.onPointerDown = this.onPointerDown.bind(this);
36
+ this.onPointerMove = this.onPointerMove.bind(this);
37
+ this.onPointerUp = this.onPointerUp.bind(this);
38
+ }
39
+
40
+ /**
41
+ * Called when tool is activated
42
+ * Subclasses can override this to perform setup
43
+ */
44
+ onActivate() {
45
+ this.isActive = true;
46
+ this.canvas.emit('toolActivated', { tool: this, name: this.name });
47
+ }
48
+
49
+ /**
50
+ * Called when tool is deactivated
51
+ * Subclasses can override this to perform cleanup
52
+ */
53
+ onDeactivate() {
54
+ this.isActive = false;
55
+
56
+ // Cancel any ongoing drawing
57
+ if (this.isDrawing) {
58
+ this.onCancel();
59
+ }
60
+
61
+ this.canvas.emit('toolDeactivated', { tool: this, name: this.name });
62
+ }
63
+
64
+ /**
65
+ * Handle pointer down events
66
+ * Subclasses must implement this method
67
+ * @param {Object} event - Normalized pointer event
68
+ */
69
+ onPointerDown(event) {
70
+ throw new Error('Tool.onPointerDown() must be implemented by subclass');
71
+ }
72
+
73
+ /**
74
+ * Handle pointer move events
75
+ * Subclasses must implement this method
76
+ * @param {Object} event - Normalized pointer event
77
+ */
78
+ onPointerMove(event) {
79
+ throw new Error('Tool.onPointerMove() must be implemented by subclass');
80
+ }
81
+
82
+ /**
83
+ * Handle pointer up events
84
+ * Subclasses must implement this method
85
+ * @param {Object} event - Normalized pointer event
86
+ */
87
+ onPointerUp(event) {
88
+ throw new Error('Tool.onPointerUp() must be implemented by subclass');
89
+ }
90
+
91
+ /**
92
+ * Cancel current tool action
93
+ * Subclasses can override this to handle cancellation
94
+ */
95
+ onCancel() {
96
+ if (this.isDrawing) {
97
+ this.isDrawing = false;
98
+
99
+ // Remove incomplete stroke if any
100
+ if (this.currentStroke && this.currentStroke.id) {
101
+ this.canvas.removeStroke(this.currentStroke.id);
102
+ }
103
+
104
+ this.currentStroke = null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Update tool configuration
110
+ * @param {Object} newConfig - New configuration options
111
+ */
112
+ updateConfig(newConfig) {
113
+ this.config = { ...this.config, ...newConfig };
114
+ this.onConfigUpdate();
115
+ }
116
+
117
+ /**
118
+ * Called when configuration is updated
119
+ * Subclasses can override this to respond to config changes
120
+ */
121
+ onConfigUpdate() {
122
+ // Default implementation does nothing
123
+ }
124
+
125
+ /**
126
+ * Get current tool configuration
127
+ * @returns {Object} Current configuration
128
+ */
129
+ getConfig() {
130
+ return { ...this.config };
131
+ }
132
+
133
+ /**
134
+ * Check if tool is currently drawing
135
+ * @returns {boolean} True if drawing
136
+ */
137
+ isDrawingActive() {
138
+ return this.isDrawing;
139
+ }
140
+
141
+ /**
142
+ * Get tool cursor style
143
+ * @returns {string} CSS cursor value or tool name for custom cursor
144
+ */
145
+ getCursor() {
146
+ return this.name;
147
+ }
148
+
149
+ /**
150
+ * Get tool properties for serialization
151
+ * @returns {Object} Serializable tool properties
152
+ */
153
+ getProperties() {
154
+ return {
155
+ name: this.name,
156
+ displayName: this.displayName,
157
+ description: this.description,
158
+ icon: this.icon,
159
+ shortcut: this.shortcut,
160
+ category: this.category,
161
+ config: this.getConfig()
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Create a stroke with tool's current configuration
167
+ * @param {number} x - Starting X coordinate
168
+ * @param {number} y - Starting Y coordinate
169
+ * @returns {Stroke} New stroke instance
170
+ */
171
+ createStroke(x, y) {
172
+ const strokeConfig = {
173
+ x,
174
+ y,
175
+ strokeWidth: this.config.strokeWidth,
176
+ strokeColor: this.config.strokeColor,
177
+ strokeOpacity: this.config.strokeOpacity,
178
+ tool: this.name
179
+ };
180
+
181
+ return new Stroke(strokeConfig);
182
+ }
183
+
184
+ /**
185
+ * Calculate stroke width based on pressure (if supported)
186
+ * @param {number} pressure - Pressure value (0-1)
187
+ * @returns {number} Calculated stroke width
188
+ */
189
+ calculateStrokeWidth(pressure = 0.5) {
190
+ const baseWidth = this.config.strokeWidth;
191
+ const minWidth = Math.max(1, baseWidth * 0.3);
192
+ const maxWidth = baseWidth * 1.5;
193
+
194
+ return minWidth + (maxWidth - minWidth) * pressure;
195
+ }
196
+
197
+ /**
198
+ * Handle keyboard shortcut
199
+ * @param {string} key - Key that was pressed
200
+ * @param {Object} event - Keyboard event
201
+ * @returns {boolean} True if shortcut was handled
202
+ */
203
+ onKeyboardShortcut(key, event) {
204
+ // Default implementation - subclasses can override
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * Get help text for this tool
210
+ * @returns {string} Help text
211
+ */
212
+ getHelpText() {
213
+ return `${this.displayName}: ${this.description}`;
214
+ }
215
+
216
+ /**
217
+ * Validate if tool can be used with current canvas state
218
+ * @returns {boolean} True if tool can be used
219
+ */
220
+ canUse() {
221
+ return !this.canvas.isDestroyed && this.isActive;
222
+ }
223
+ }