@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.
- package/README.md +138 -0
- package/canvas/core/canvasConfig.js +203 -0
- package/canvas/core/omdCanvas.js +475 -0
- package/canvas/drawing/segment.js +168 -0
- package/canvas/drawing/stroke.js +386 -0
- package/canvas/events/eventManager.js +435 -0
- package/canvas/events/pointerEventHandler.js +263 -0
- package/canvas/features/focusFrameManager.js +287 -0
- package/canvas/index.js +49 -0
- package/canvas/tools/eraserTool.js +322 -0
- package/canvas/tools/pencilTool.js +319 -0
- package/canvas/tools/selectTool.js +457 -0
- package/canvas/tools/tool.js +223 -0
- package/canvas/tools/toolManager.js +394 -0
- package/canvas/ui/cursor.js +438 -0
- package/canvas/ui/toolbar.js +304 -0
- package/canvas/utils/boundingBox.js +378 -0
- package/canvas/utils/mathUtils.js +259 -0
- package/docs/api/configuration-options.md +104 -0
- package/docs/api/eventManager.md +68 -0
- package/docs/api/focusFrameManager.md +150 -0
- package/docs/api/index.md +91 -0
- package/docs/api/main.md +58 -0
- package/docs/api/omdBinaryExpressionNode.md +227 -0
- package/docs/api/omdCanvas.md +142 -0
- package/docs/api/omdConfigManager.md +192 -0
- package/docs/api/omdConstantNode.md +117 -0
- package/docs/api/omdDisplay.md +121 -0
- package/docs/api/omdEquationNode.md +161 -0
- package/docs/api/omdEquationSequenceNode.md +301 -0
- package/docs/api/omdEquationStack.md +139 -0
- package/docs/api/omdFunctionNode.md +141 -0
- package/docs/api/omdGroupNode.md +182 -0
- package/docs/api/omdHelpers.md +96 -0
- package/docs/api/omdLeafNode.md +163 -0
- package/docs/api/omdNode.md +101 -0
- package/docs/api/omdOperationDisplayNode.md +139 -0
- package/docs/api/omdOperatorNode.md +127 -0
- package/docs/api/omdParenthesisNode.md +122 -0
- package/docs/api/omdPopup.md +117 -0
- package/docs/api/omdPowerNode.md +127 -0
- package/docs/api/omdRationalNode.md +128 -0
- package/docs/api/omdSequenceNode.md +128 -0
- package/docs/api/omdSimplification.md +110 -0
- package/docs/api/omdSqrtNode.md +79 -0
- package/docs/api/omdStepVisualizer.md +115 -0
- package/docs/api/omdStepVisualizerHighlighting.md +61 -0
- package/docs/api/omdStepVisualizerInteractiveSteps.md +129 -0
- package/docs/api/omdStepVisualizerLayout.md +60 -0
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -0
- package/docs/api/omdStepVisualizerTextBoxes.md +68 -0
- package/docs/api/omdToolbar.md +102 -0
- package/docs/api/omdTranscriptionService.md +76 -0
- package/docs/api/omdTreeDiff.md +134 -0
- package/docs/api/omdUnaryExpressionNode.md +174 -0
- package/docs/api/omdUtilities.md +70 -0
- package/docs/api/omdVariableNode.md +148 -0
- package/docs/api/selectTool.md +74 -0
- package/docs/api/simplificationEngine.md +98 -0
- package/docs/api/simplificationRules.md +77 -0
- package/docs/api/simplificationUtils.md +64 -0
- package/docs/api/transcribe.md +43 -0
- package/docs/api-reference.md +85 -0
- package/docs/index.html +454 -0
- package/docs/user-guide.md +9 -0
- package/index.js +67 -0
- package/omd/config/omdConfigManager.js +267 -0
- package/omd/core/index.js +150 -0
- package/omd/core/omdEquationStack.js +347 -0
- package/omd/core/omdUtilities.js +115 -0
- package/omd/display/omdDisplay.js +443 -0
- package/omd/display/omdToolbar.js +502 -0
- package/omd/nodes/omdBinaryExpressionNode.js +460 -0
- package/omd/nodes/omdConstantNode.js +142 -0
- package/omd/nodes/omdEquationNode.js +1223 -0
- package/omd/nodes/omdEquationSequenceNode.js +1273 -0
- package/omd/nodes/omdFunctionNode.js +352 -0
- package/omd/nodes/omdGroupNode.js +68 -0
- package/omd/nodes/omdLeafNode.js +77 -0
- package/omd/nodes/omdNode.js +557 -0
- package/omd/nodes/omdOperationDisplayNode.js +322 -0
- package/omd/nodes/omdOperatorNode.js +109 -0
- package/omd/nodes/omdParenthesisNode.js +293 -0
- package/omd/nodes/omdPowerNode.js +236 -0
- package/omd/nodes/omdRationalNode.js +295 -0
- package/omd/nodes/omdSqrtNode.js +308 -0
- package/omd/nodes/omdUnaryExpressionNode.js +178 -0
- package/omd/nodes/omdVariableNode.js +123 -0
- package/omd/simplification/omdSimplification.js +171 -0
- package/omd/simplification/omdSimplificationEngine.js +886 -0
- package/omd/simplification/package.json +6 -0
- package/omd/simplification/rules/binaryRules.js +1037 -0
- package/omd/simplification/rules/functionRules.js +111 -0
- package/omd/simplification/rules/index.js +48 -0
- package/omd/simplification/rules/parenthesisRules.js +19 -0
- package/omd/simplification/rules/powerRules.js +143 -0
- package/omd/simplification/rules/rationalRules.js +475 -0
- package/omd/simplification/rules/sqrtRules.js +48 -0
- package/omd/simplification/rules/unaryRules.js +37 -0
- package/omd/simplification/simplificationRules.js +32 -0
- package/omd/simplification/simplificationUtils.js +1056 -0
- package/omd/step-visualizer/omdStepVisualizer.js +597 -0
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +206 -0
- package/omd/step-visualizer/omdStepVisualizerLayout.js +245 -0
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +163 -0
- package/omd/utils/omdNodeOverlay.js +638 -0
- package/omd/utils/omdPopup.js +1084 -0
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +491 -0
- package/omd/utils/omdStepVisualizerNodeUtils.js +268 -0
- package/omd/utils/omdTranscriptionService.js +125 -0
- package/omd/utils/omdTreeDiff.js +734 -0
- package/package.json +46 -0
- package/src/index.js +62 -0
- package/src/json-schemas.md +109 -0
- package/src/omd-json-samples.js +115 -0
- package/src/omd.js +109 -0
- package/src/omdApp.js +391 -0
- package/src/omdAppCanvas.js +336 -0
- package/src/omdBalanceHanger.js +172 -0
- package/src/omdColor.js +13 -0
- package/src/omdCoordinatePlane.js +467 -0
- package/src/omdEquation.js +125 -0
- package/src/omdExpression.js +104 -0
- package/src/omdFunction.js +113 -0
- package/src/omdMetaExpression.js +287 -0
- package/src/omdNaturalExpression.js +564 -0
- package/src/omdNode.js +384 -0
- package/src/omdNumber.js +53 -0
- package/src/omdNumberLine.js +107 -0
- package/src/omdNumberTile.js +119 -0
- package/src/omdOperator.js +73 -0
- package/src/omdPowerExpression.js +92 -0
- package/src/omdProblem.js +55 -0
- package/src/omdRatioChart.js +232 -0
- package/src/omdRationalExpression.js +115 -0
- package/src/omdSampleData.js +215 -0
- package/src/omdShapes.js +476 -0
- package/src/omdSpinner.js +148 -0
- package/src/omdString.js +39 -0
- package/src/omdTable.js +369 -0
- package/src/omdTapeDiagram.js +245 -0
- package/src/omdTerm.js +92 -0
- package/src/omdTileEquation.js +349 -0
- 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
|
+
}
|