@teachinglab/omd 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,457 +1,902 @@
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
- }
1
+ import { Tool } from './tool.js';
2
+ import { BoundingBox } from '../utils/boundingBox.js';
3
+ import { Stroke } from '../drawing/stroke.js';
4
+ import { ResizeHandleManager } from '../features/resizeHandleManager.js';
5
+ import {omdColor} from '../../src/omdColor.js';
6
+ const SELECTION_TOLERANCE = 10;
7
+ const SELECTION_COLOR = '#007bff';
8
+ const SELECTION_OPACITY = 0.3;
9
+
10
+ /**
11
+ * A tool for selecting, moving, and deleting stroke segments.
12
+ * @extends Tool
13
+ */
14
+ export class SelectTool extends Tool {
15
+ /**
16
+ * @param {OMDCanvas} canvas - The canvas instance.
17
+ * @param {object} [options={}] - Configuration options for the tool.
18
+ */
19
+ constructor(canvas, options = {}) {
20
+ super(canvas, {
21
+ selectionColor: SELECTION_COLOR,
22
+ selectionOpacity: SELECTION_OPACITY,
23
+ ...options
24
+ });
25
+
26
+ this.displayName = 'Select';
27
+ this.description = 'Select and manipulate segments';
28
+ this.icon = 'select';
29
+ this.shortcut = 'S';
30
+ this.category = 'selection';
31
+
32
+ /** @private */
33
+ this.isSelecting = false;
34
+ /** @private */
35
+ this.selectionBox = null;
36
+ /** @private */
37
+ this.startPoint = null;
38
+ /** @type {Map<string, Set<number>>} */
39
+ this.selectedSegments = new Map();
40
+
41
+ /** @private - OMD dragging state */
42
+ this.isDraggingOMD = false;
43
+ this.draggedOMDElement = null;
44
+
45
+ // Initialize resize handle manager for OMD visuals
46
+ this.resizeHandleManager = new ResizeHandleManager(canvas);
47
+
48
+ // Store reference on canvas for makeDraggable to access
49
+ if (canvas) {
50
+ canvas.resizeHandleManager = this.resizeHandleManager;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Handles the pointer down event to start a selection.
56
+ * @param {PointerEvent} event - The pointer event.
57
+ */
58
+ onPointerDown(event) {
59
+ if (!this.canUse()) {
60
+ return;
61
+ }
62
+
63
+ // Check for resize handle first (highest priority)
64
+ const handle = this.resizeHandleManager.getHandleAtPoint(event.x, event.y);
65
+ if (handle) {
66
+ // Start resize operation
67
+ this.resizeHandleManager.startResize(handle, event.x, event.y, event.shiftKey);
68
+ if (this.canvas.eventManager) {
69
+ this.canvas.eventManager.isDrawing = true;
70
+ }
71
+ return;
72
+ }
73
+
74
+ const segmentSelection = this._findSegmentAtPoint(event.x, event.y);
75
+ const omdElement = this._findOMDElementAtPoint(event.x, event.y);
76
+
77
+ if (segmentSelection) {
78
+ // Clicking on a stroke segment - clear OMD selection and handle segment selection
79
+ this.resizeHandleManager.clearSelection();
80
+ this._handleSegmentClick(segmentSelection, event.shiftKey);
81
+ } else if (omdElement) {
82
+ // Clicking on an OMD visual - clear segment selection and select OMD
83
+ if (!event.shiftKey) {
84
+ this.selectedSegments.clear();
85
+ this._clearSelectionVisuals();
86
+ }
87
+ this.resizeHandleManager.selectElement(omdElement);
88
+
89
+ // CRITICAL: Start tracking for potential drag operation
90
+ this.isDraggingOMD = true;
91
+ this.draggedOMDElement = omdElement;
92
+ this.startPoint = { x: event.x, y: event.y };
93
+
94
+ // Set isDrawing so we get pointermove events
95
+ if (this.canvas.eventManager) {
96
+ this.canvas.eventManager.isDrawing = true;
97
+ }
98
+
99
+ // Don't start box selection - we're either resizing or will be dragging
100
+ return;
101
+ } else {
102
+ // Clicking on empty space - clear all selections and start box selection
103
+ this.resizeHandleManager.clearSelection();
104
+ this._startBoxSelection(event.x, event.y, event.shiftKey);
105
+
106
+ // CRITICAL: Set startPoint AFTER _startBoxSelection so it doesn't get cleared!
107
+ this.startPoint = { x: event.x, y: event.y };
108
+
109
+ // CRITICAL: Tell the event manager we're "drawing" so pointer move events get sent to us
110
+ if (this.canvas.eventManager) {
111
+ this.canvas.eventManager.isDrawing = true;
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Handles the pointer move event to update the selection box.
118
+ * @param {PointerEvent} event - The pointer event.
119
+ */
120
+ onPointerMove(event) {
121
+ // Handle resize operation if in progress
122
+ if (this.resizeHandleManager.isResizing) {
123
+ this.resizeHandleManager.updateResize(event.x, event.y);
124
+ return;
125
+ }
126
+
127
+ // Handle OMD dragging if in progress
128
+ if (this.isDraggingOMD && this.draggedOMDElement) {
129
+ this._dragOMDElement(event.x, event.y);
130
+ return;
131
+ }
132
+
133
+ // Handle box selection if in progress
134
+ if (!this.isSelecting || !this.selectionBox) return;
135
+
136
+ this._updateSelectionBox(event.x, event.y);
137
+ this._updateBoxSelection();
138
+ }
139
+
140
+ /**
141
+ * Handles the pointer up event to complete the selection.
142
+ * @param {PointerEvent} event - The pointer event.
143
+ */
144
+ onPointerUp(event) {
145
+ // Handle resize completion
146
+ if (this.resizeHandleManager.isResizing) {
147
+ this.resizeHandleManager.finishResize();
148
+ if (this.canvas.eventManager) {
149
+ this.canvas.eventManager.isDrawing = false;
150
+ }
151
+ return;
152
+ }
153
+
154
+ // Handle OMD drag completion
155
+ if (this.isDraggingOMD) {
156
+ this.isDraggingOMD = false;
157
+ this.draggedOMDElement = null;
158
+ this.startPoint = null;
159
+ if (this.canvas.eventManager) {
160
+ this.canvas.eventManager.isDrawing = false;
161
+ }
162
+ return;
163
+ }
164
+
165
+ // Handle box selection completion
166
+ if (this.isSelecting) {
167
+ this._finishBoxSelection();
168
+ }
169
+ this.isSelecting = false;
170
+ this._removeSelectionBox();
171
+
172
+ // CRITICAL: Tell the event manager we're done "drawing"
173
+ if (this.canvas.eventManager) {
174
+ this.canvas.eventManager.isDrawing = false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Cancels the current selection operation.
180
+ */
181
+ onCancel() {
182
+ this.isSelecting = false;
183
+ this._removeSelectionBox();
184
+ this.clearSelection();
185
+
186
+ // Reset OMD drag state
187
+ this.isDraggingOMD = false;
188
+ this.draggedOMDElement = null;
189
+
190
+ // CRITICAL: Tell the event manager we're done "drawing"
191
+ if (this.canvas.eventManager) {
192
+ this.canvas.eventManager.isDrawing = false;
193
+ }
194
+
195
+ super.onCancel();
196
+ }
197
+
198
+ /**
199
+ * Handle tool deactivation - clean up everything.
200
+ */
201
+ onDeactivate() {
202
+ // Clear active state
203
+ this.isActive = false;
204
+
205
+ // Reset drag and resize states
206
+ this.isDraggingOMD = false;
207
+ this.draggedOMDElement = null;
208
+ this.isSelecting = false;
209
+ this.startPoint = null;
210
+
211
+ // Clear the event manager drawing state
212
+ if (this.canvas.eventManager) {
213
+ this.canvas.eventManager.isDrawing = false;
214
+ }
215
+
216
+ // Clean up selection
217
+ this.clearSelection();
218
+
219
+ super.onDeactivate();
220
+ }
221
+
222
+ /**
223
+ * Handle tool activation.
224
+ */
225
+ onActivate() {
226
+ // Set active state first
227
+ this.isActive = true;
228
+
229
+ // Reset all state flags
230
+ this.isDraggingOMD = false;
231
+ this.draggedOMDElement = null;
232
+ this.isSelecting = false;
233
+ this.startPoint = null;
234
+
235
+ // Ensure cursor is visible and properly set
236
+ if (this.canvas.cursor) {
237
+ this.canvas.cursor.show();
238
+ this.canvas.cursor.setShape('select');
239
+ }
240
+
241
+ // Clear any existing selection to start fresh
242
+ this.clearSelection();
243
+
244
+ super.onActivate();
245
+ }
246
+
247
+ /**
248
+ * Handles keyboard shortcuts for selection-related actions.
249
+ * @param {string} key - The key that was pressed.
250
+ * @param {KeyboardEvent} event - The keyboard event.
251
+ * @returns {boolean} - True if the shortcut was handled, false otherwise.
252
+ */
253
+ onKeyboardShortcut(key, event) {
254
+ if (event.ctrlKey || event.metaKey) {
255
+ if (key === 'a') {
256
+ this._selectAllSegments();
257
+ return true;
258
+ }
259
+ }
260
+
261
+ if (key === 'delete' || key === 'backspace') {
262
+ this._deleteSelectedSegments();
263
+ return true;
264
+ }
265
+
266
+ return false;
267
+ }
268
+
269
+ /**
270
+ * Gets the cursor for the tool.
271
+ * @returns {string} The CSS cursor name.
272
+ */
273
+ getCursor() {
274
+ // If resizing, return appropriate resize cursor
275
+ if (this.resizeHandleManager.isResizing) {
276
+ return this.resizeHandleManager.getCursorForHandle(
277
+ this.resizeHandleManager.resizeData?.handle?.type || 'se'
278
+ );
279
+ }
280
+
281
+ return 'select'; // Use custom SVG select cursor
282
+ }
283
+
284
+ /**
285
+ * Check if tool can be used
286
+ * @returns {boolean}
287
+ */
288
+ canUse() {
289
+ // Use the base class method which checks isActive and canvas state
290
+ const result = super.canUse();
291
+
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * Clears the current selection.
297
+ */
298
+ clearSelection() {
299
+ // Clear stroke selection data
300
+ this.selectedSegments.clear();
301
+
302
+ // Clear OMD selection
303
+ this.resizeHandleManager.clearSelection();
304
+
305
+ // Remove selection box if it exists
306
+ this._removeSelectionBox();
307
+
308
+ // Clear visual highlights
309
+ this._clearSelectionVisuals();
310
+
311
+ // Reset selection state
312
+ this.isSelecting = false;
313
+ this.startPoint = null;
314
+
315
+ // Emit selection change event
316
+ this.canvas.emit('selectionChanged', { selected: [] });
317
+ }
318
+
319
+ /**
320
+ * Thoroughly clears all selection visuals.
321
+ * @private
322
+ */
323
+ _clearSelectionVisuals() {
324
+ const selectionLayer = this.canvas.uiLayer.querySelector('.segment-selection-layer');
325
+ if (selectionLayer) {
326
+ // Remove all children
327
+ while (selectionLayer.firstChild) {
328
+ selectionLayer.removeChild(selectionLayer.firstChild);
329
+ }
330
+ }
331
+
332
+ // Also remove any orphaned selection elements
333
+ const orphanedHighlights = this.canvas.uiLayer.querySelectorAll('[stroke="' + this.config.selectionColor + '"]');
334
+ orphanedHighlights.forEach(el => {
335
+ if (el.parentNode) {
336
+ el.parentNode.removeChild(el);
337
+ }
338
+ });
339
+ }
340
+
341
+ /**
342
+ * @private
343
+ */
344
+ _handleSegmentClick({ strokeId, segmentIndex }, shiftKey) {
345
+ const segmentSet = this.selectedSegments.get(strokeId) || new Set();
346
+ if (segmentSet.has(segmentIndex)) {
347
+ segmentSet.delete(segmentIndex);
348
+ if (segmentSet.size === 0) {
349
+ this.selectedSegments.delete(strokeId);
350
+ }
351
+ } else {
352
+ if (!shiftKey) {
353
+ this.selectedSegments.clear();
354
+ }
355
+ if (!this.selectedSegments.has(strokeId)) {
356
+ this.selectedSegments.set(strokeId, new Set());
357
+ }
358
+ this.selectedSegments.get(strokeId).add(segmentIndex);
359
+ }
360
+ this._updateSegmentSelectionVisuals();
361
+ this.canvas.emit('selectionChanged', { selected: this._getSelectedSegmentsAsArray() });
362
+ }
363
+
364
+ /**
365
+ * @private
366
+ */
367
+ _startBoxSelection(x, y, shiftKey) {
368
+ if (!shiftKey) {
369
+ this.clearSelection();
370
+ }
371
+ this.isSelecting = true;
372
+ this._createSelectionBox(x, y);
373
+ }
374
+
375
+ /**
376
+ * @private
377
+ */
378
+ _finishBoxSelection() {
379
+ this._updateSegmentSelectionVisuals();
380
+ this.canvas.emit('selectionChanged', { selected: this._getSelectedSegmentsAsArray() });
381
+ }
382
+
383
+ /**
384
+ * @private
385
+ */
386
+ _getSelectedSegmentsAsArray() {
387
+ const selected = [];
388
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
389
+ for (const segmentIndex of segmentSet) {
390
+ selected.push({ strokeId, segmentIndex });
391
+ }
392
+ }
393
+ return selected;
394
+ }
395
+
396
+ /**
397
+ * Drags the selected OMD element
398
+ * @private
399
+ * @param {number} x - Current pointer x coordinate
400
+ * @param {number} y - Current pointer y coordinate
401
+ */
402
+ _dragOMDElement(x, y) {
403
+ if (!this.draggedOMDElement || !this.startPoint) return;
404
+
405
+ const dx = x - this.startPoint.x;
406
+ const dy = y - this.startPoint.y;
407
+
408
+ // Parse current transform
409
+ const currentTransform = this.draggedOMDElement.getAttribute('transform') || '';
410
+ const translateMatch = currentTransform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
411
+ const scaleMatch = currentTransform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
412
+
413
+ // Get current translate values
414
+ let currentX = translateMatch ? parseFloat(translateMatch[1]) : 0;
415
+ let currentY = translateMatch ? parseFloat(translateMatch[2]) : 0;
416
+
417
+ // Calculate new position
418
+ const newX = currentX + dx;
419
+ const newY = currentY + dy;
420
+
421
+ // Build new transform preserving scale
422
+ let newTransform = `translate(${newX}, ${newY})`;
423
+ if (scaleMatch) {
424
+ const scaleX = parseFloat(scaleMatch[1]) || 1;
425
+ const scaleY = scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX;
426
+ newTransform += ` scale(${scaleX}, ${scaleY})`;
427
+ }
428
+
429
+ this.draggedOMDElement.setAttribute('transform', newTransform);
430
+
431
+ // Update start point for next move
432
+ this.startPoint = { x, y };
433
+
434
+ // Update resize handles if element is selected
435
+ this.resizeHandleManager.updateIfSelected(this.draggedOMDElement);
436
+ }
437
+
438
+ /**
439
+ * Finds the closest segment to a given point.
440
+ * @private
441
+ * @param {number} x - The x-coordinate of the point.
442
+ * @param {number} y - The y-coordinate of the point.
443
+ * @returns {{strokeId: string, segmentIndex: number}|null}
444
+ */
445
+ _findSegmentAtPoint(x, y) {
446
+ let closest = null;
447
+ let minDist = SELECTION_TOLERANCE;
448
+ for (const [id, stroke] of this.canvas.strokes) {
449
+ if (!stroke.points || stroke.points.length < 2) continue;
450
+ for (let i = 0; i < stroke.points.length - 1; i++) {
451
+ const p1 = stroke.points[i];
452
+ const p2 = stroke.points[i + 1];
453
+ const dist = this._pointToSegmentDistance(x, y, p1, p2);
454
+ if (dist < minDist) {
455
+ minDist = dist;
456
+ closest = { strokeId: id, segmentIndex: i };
457
+ }
458
+ }
459
+ }
460
+ return closest;
461
+ }
462
+
463
+ /**
464
+ * Check if point is inside an OMD visual element
465
+ * @private
466
+ */
467
+ _findOMDElementAtPoint(x, y) {
468
+ // Find OMD layer directly from canvas
469
+ const omdLayer = this.canvas.drawingLayer?.querySelector('.omd-layer');
470
+ if (!omdLayer) {
471
+ return null;
472
+ }
473
+
474
+ // Check all OMD items in the layer
475
+ const omdItems = omdLayer.querySelectorAll('.omd-item');
476
+
477
+ for (const item of omdItems) {
478
+ try {
479
+ // Get the bounding box of the item
480
+ const bbox = item.getBBox();
481
+
482
+ // Parse transform to get the actual position
483
+ const transform = item.getAttribute('transform') || '';
484
+ let offsetX = 0, offsetY = 0, scaleX = 1, scaleY = 1;
485
+
486
+ // Parse translate
487
+ const translateMatch = transform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
488
+ if (translateMatch) {
489
+ offsetX = parseFloat(translateMatch[1]) || 0;
490
+ offsetY = parseFloat(translateMatch[2]) || 0;
491
+ }
492
+
493
+ // Parse scale
494
+ const scaleMatch = transform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
495
+ if (scaleMatch) {
496
+ scaleX = parseFloat(scaleMatch[1]) || 1;
497
+ scaleY = scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX;
498
+ }
499
+
500
+ // Calculate the actual bounds including transform
501
+ const actualX = offsetX + (bbox.x * scaleX);
502
+ const actualY = offsetY + (bbox.y * scaleY);
503
+ const actualWidth = bbox.width * scaleX;
504
+ const actualHeight = bbox.height * scaleY;
505
+
506
+ // Add some padding for easier clicking
507
+ const padding = 10;
508
+
509
+ // Check if point is within the bounds (with padding)
510
+ if (x >= actualX - padding &&
511
+ x <= actualX + actualWidth + padding &&
512
+ y >= actualY - padding &&
513
+ y <= actualY + actualHeight + padding) {
514
+ return item;
515
+ }
516
+ } catch (error) {
517
+ // Skip items that can't be measured (continue with next)
518
+ continue;
519
+ }
520
+ }
521
+
522
+ return null;
523
+ }
524
+
525
+ /**
526
+ * Calculates the distance from a point to a line segment.
527
+ * @private
528
+ */
529
+ _pointToSegmentDistance(x, y, p1, p2) {
530
+ const l2 = (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2;
531
+ if (l2 === 0) return Math.hypot(x - p1.x, y - p1.y);
532
+ let t = ((x - p1.x) * (p2.x - p1.x) + (y - p1.y) * (p2.y - p1.y)) / l2;
533
+ t = Math.max(0, Math.min(1, t));
534
+ const projX = p1.x + t * (p2.x - p1.x);
535
+ const projY = p1.y + t * (p2.y - p1.y);
536
+ return Math.hypot(x - projX, y - projY);
537
+ }
538
+
539
+ /**
540
+ * Creates the selection box element.
541
+ * @private
542
+ */
543
+ _createSelectionBox(x, y) {
544
+ this.selectionBox = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
545
+ this.selectionBox.setAttribute('x', x);
546
+ this.selectionBox.setAttribute('y', y);
547
+ this.selectionBox.setAttribute('width', 0);
548
+ this.selectionBox.setAttribute('height', 0);
549
+
550
+ // Clean blue selection box
551
+ this.selectionBox.setAttribute('fill', 'rgba(0, 123, 255, 0.2)'); // Blue with transparency
552
+ this.selectionBox.setAttribute('stroke', '#007bff'); // Blue stroke
553
+ this.selectionBox.setAttribute('stroke-width', '1'); // Thin stroke
554
+ this.selectionBox.setAttribute('stroke-dasharray', '4,2'); // Small dashes
555
+ this.selectionBox.style.pointerEvents = 'none';
556
+ this.selectionBox.setAttribute('data-selection-box', 'true');
557
+
558
+
559
+ if (this.canvas.uiLayer) {
560
+ this.canvas.uiLayer.appendChild(this.selectionBox);
561
+ } else if (this.canvas.svg) {
562
+ this.canvas.svg.appendChild(this.selectionBox);
563
+ } else {
564
+ console.error('No canvas layer found to add selection box!');
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Updates the dimensions of the selection box.
570
+ * @private
571
+ */
572
+ _updateSelectionBox(x, y) {
573
+ if (!this.selectionBox || !this.startPoint) return;
574
+
575
+ const minX = Math.min(this.startPoint.x, x);
576
+ const minY = Math.min(this.startPoint.y, y);
577
+ const width = Math.abs(this.startPoint.x - x);
578
+ const height = Math.abs(this.startPoint.y - y);
579
+
580
+ this.selectionBox.setAttribute('x', minX);
581
+ this.selectionBox.setAttribute('y', minY);
582
+ this.selectionBox.setAttribute('width', width);
583
+ this.selectionBox.setAttribute('height', height);
584
+ }
585
+
586
+ /**
587
+ * Removes the selection box element.
588
+ * @private
589
+ */
590
+ _removeSelectionBox() {
591
+ if (this.selectionBox) {
592
+ this.selectionBox.remove();
593
+ this.selectionBox = null;
594
+ }
595
+ this.startPoint = null;
596
+ }
597
+
598
+ /**
599
+ * Updates the set of selected segments based on the selection box.
600
+ * @private
601
+ */
602
+ _updateBoxSelection() {
603
+ if (!this.selectionBox) return;
604
+
605
+ const x = parseFloat(this.selectionBox.getAttribute('x'));
606
+ const y = parseFloat(this.selectionBox.getAttribute('y'));
607
+ const width = parseFloat(this.selectionBox.getAttribute('width'));
608
+ const height = parseFloat(this.selectionBox.getAttribute('height'));
609
+ const selectionBounds = new BoundingBox(x, y, width, height);
610
+
611
+ for (const [id, stroke] of this.canvas.strokes) {
612
+ if (!stroke.points || stroke.points.length < 2) continue;
613
+ for (let i = 0; i < stroke.points.length - 1; i++) {
614
+ const p1 = stroke.points[i];
615
+ const p2 = stroke.points[i + 1];
616
+ if (this._segmentIntersectsBox(p1, p2, selectionBounds)) {
617
+ if (!this.selectedSegments.has(id)) {
618
+ this.selectedSegments.set(id, new Set());
619
+ }
620
+ this.selectedSegments.get(id).add(i);
621
+ }
622
+ }
623
+ }
624
+ this._updateSegmentSelectionVisuals();
625
+ }
626
+
627
+ /**
628
+ * Checks if a line segment intersects with a bounding box.
629
+ * @private
630
+ * @param {{x: number, y: number}} p1 - The start point of the segment.
631
+ * @param {{x: number, y: number}} p2 - The end point of the segment.
632
+ * @param {BoundingBox} box - The bounding box.
633
+ * @returns {boolean}
634
+ */
635
+ _segmentIntersectsBox(p1, p2, box) {
636
+ if (box.containsPoint(p1.x, p1.y) || box.containsPoint(p2.x, p2.y)) {
637
+ return true;
638
+ }
639
+
640
+ const { x, y, width, height } = box;
641
+ const right = x + width;
642
+ const bottom = y + height;
643
+
644
+ const lines = [
645
+ { a: { x, y }, b: { x: right, y } },
646
+ { a: { x: right, y }, b: { x: right, y: bottom } },
647
+ { a: { x: right, y: bottom }, b: { x, y: bottom } },
648
+ { a: { x, y: bottom }, b: { x, y } }
649
+ ];
650
+
651
+ for (const line of lines) {
652
+ if (this._lineIntersectsLine(p1, p2, line.a, line.b)) {
653
+ return true;
654
+ }
655
+ }
656
+ return false;
657
+ }
658
+
659
+ /**
660
+ * Checks if two line segments intersect.
661
+ * @private
662
+ */
663
+ _lineIntersectsLine(p1, p2, p3, p4) {
664
+ const det = (p2.x - p1.x) * (p4.y - p3.y) - (p2.y - p1.y) * (p4.x - p3.x);
665
+ if (det === 0) return false;
666
+ const t = ((p3.x - p1.x) * (p4.y - p3.y) - (p3.y - p1.y) * (p4.x - p3.x)) / det;
667
+ const u = -((p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)) / det;
668
+ return t >= 0 && t <= 1 && u >= 0 && u <= 1;
669
+ }
670
+
671
+ /**
672
+ * Updates the visual representation of selected segments.
673
+ * @private
674
+ */
675
+ _updateSegmentSelectionVisuals() {
676
+ // Get or create selection layer
677
+ const selectionLayer = this.canvas.uiLayer.querySelector('.segment-selection-layer') || this._createSelectionLayer();
678
+
679
+ // Clear existing visuals efficiently
680
+ while (selectionLayer.firstChild) {
681
+ selectionLayer.removeChild(selectionLayer.firstChild);
682
+ }
683
+
684
+ // Add highlights for currently selected segments
685
+ let highlightCount = 0;
686
+
687
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
688
+ const stroke = this.canvas.strokes.get(strokeId);
689
+ if (!stroke || !stroke.points) continue;
690
+
691
+ for (const segmentIndex of segmentSet) {
692
+ if (segmentIndex >= stroke.points.length - 1) continue;
693
+
694
+ const p1 = stroke.points[segmentIndex];
695
+ const p2 = stroke.points[segmentIndex + 1];
696
+
697
+ // Create highlight element
698
+ const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'line');
699
+ highlight.setAttribute('x1', p1.x);
700
+ highlight.setAttribute('y1', p1.y);
701
+ highlight.setAttribute('x2', p2.x);
702
+ highlight.setAttribute('y2', p2.y);
703
+ highlight.setAttribute('stroke', omdColor.hiliteColor);
704
+ highlight.setAttribute('stroke-width', '4');
705
+ highlight.setAttribute('stroke-opacity', '0.8');
706
+ highlight.setAttribute('stroke-linecap', 'round');
707
+ highlight.style.pointerEvents = 'none';
708
+ highlight.classList.add('selection-highlight');
709
+
710
+ selectionLayer.appendChild(highlight);
711
+ highlightCount++;
712
+ }
713
+ }
714
+
715
+ }
716
+
717
+ /**
718
+ * @private
719
+ */
720
+ _createSelectionLayer() {
721
+ const layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
722
+ layer.classList.add('segment-selection-layer');
723
+ this.canvas.uiLayer.appendChild(layer);
724
+ return layer;
725
+ }
726
+
727
+ /**
728
+ * Selects all segments on the canvas.
729
+ * @private
730
+ */
731
+ _selectAllSegments() {
732
+ // Clear current selection
733
+ this.selectedSegments.clear();
734
+
735
+ let totalSegments = 0;
736
+
737
+ // Select all valid segments from all strokes
738
+ for (const [id, stroke] of this.canvas.strokes) {
739
+ if (!stroke.points || stroke.points.length < 2) continue;
740
+
741
+ const segmentIndices = new Set();
742
+ for (let i = 0; i < stroke.points.length - 1; i++) {
743
+ segmentIndices.add(i);
744
+ totalSegments++;
745
+ }
746
+
747
+ if (segmentIndices.size > 0) {
748
+ this.selectedSegments.set(id, segmentIndices);
749
+ }
750
+ }
751
+
752
+ // Update visuals
753
+ this._updateSegmentSelectionVisuals();
754
+
755
+ // Emit selection change event
756
+ this.canvas.emit('selectionChanged', {
757
+ selected: this._getSelectedSegmentsAsArray()
758
+ });
759
+
760
+ }
761
+
762
+ /**
763
+ * Deletes all currently selected segments efficiently.
764
+ * @private
765
+ */
766
+ _deleteSelectedSegments() {
767
+ if (this.selectedSegments.size === 0) return;
768
+
769
+ // Process each stroke that has selected segments
770
+ const strokesToProcess = Array.from(this.selectedSegments.entries());
771
+
772
+ for (const [strokeId, segmentIndices] of strokesToProcess) {
773
+ const stroke = this.canvas.strokes.get(strokeId);
774
+ if (!stroke || !stroke.points || stroke.points.length < 2) continue;
775
+
776
+ const sortedIndices = Array.from(segmentIndices).sort((a, b) => a - b);
777
+
778
+ // If all or most segments are selected, just delete the whole stroke
779
+ const totalSegments = stroke.points.length - 1;
780
+ const selectedCount = sortedIndices.length;
781
+
782
+ if (selectedCount >= totalSegments * 0.8) {
783
+ // Delete entire stroke
784
+ this.canvas.removeStroke(strokeId);
785
+ continue;
786
+ }
787
+
788
+ // Split the stroke, keeping only unselected segments
789
+ this._splitStrokeKeepingUnselected(stroke, sortedIndices);
790
+ }
791
+
792
+ // Clear selection and update UI
793
+ this.clearSelection();
794
+
795
+ // Emit deletion event
796
+ this.canvas.emit('segmentsDeleted', {
797
+ strokesAffected: strokesToProcess.length
798
+ });
799
+ }
800
+
801
+ /**
802
+ * Splits a stroke, keeping only the segments that weren't selected for deletion.
803
+ * @private
804
+ */
805
+ _splitStrokeKeepingUnselected(stroke, selectedSegmentIndices) {
806
+ const totalSegments = stroke.points.length - 1;
807
+ const keepSegments = [];
808
+
809
+ // Determine which segments to keep
810
+ for (let i = 0; i < totalSegments; i++) {
811
+ if (!selectedSegmentIndices.includes(i)) {
812
+ keepSegments.push(i);
813
+ }
814
+ }
815
+
816
+ if (keepSegments.length === 0) {
817
+ // All segments were selected, delete the whole stroke
818
+ this.canvas.removeStroke(stroke.id);
819
+ return;
820
+ }
821
+
822
+ // Group consecutive segments into separate strokes
823
+ const strokeSegments = this._groupConsecutiveSegments(keepSegments);
824
+
825
+ // Remove the original stroke
826
+ this.canvas.removeStroke(stroke.id);
827
+
828
+ // Create new strokes for each group of consecutive segments
829
+ strokeSegments.forEach(segmentGroup => {
830
+ this._createStrokeFromSegments(stroke, segmentGroup);
831
+ });
832
+ }
833
+
834
+ /**
835
+ * Groups consecutive segment indices into separate arrays.
836
+ * @private
837
+ */
838
+ _groupConsecutiveSegments(segmentIndices) {
839
+ if (segmentIndices.length === 0) return [];
840
+
841
+ const groups = [];
842
+ let currentGroup = [segmentIndices[0]];
843
+
844
+ for (let i = 1; i < segmentIndices.length; i++) {
845
+ const current = segmentIndices[i];
846
+ const previous = segmentIndices[i - 1];
847
+
848
+ if (current === previous + 1) {
849
+ // Consecutive segment
850
+ currentGroup.push(current);
851
+ } else {
852
+ // Gap found, start new group
853
+ groups.push(currentGroup);
854
+ currentGroup = [current];
855
+ }
856
+ }
857
+
858
+ // Add the last group
859
+ groups.push(currentGroup);
860
+
861
+ return groups;
862
+ }
863
+
864
+ /**
865
+ * Creates a new stroke from a group of segment indices.
866
+ * @private
867
+ */
868
+ _createStrokeFromSegments(originalStroke, segmentIndices) {
869
+ if (segmentIndices.length === 0) return;
870
+
871
+ // Collect points for the new stroke
872
+ const newPoints = [];
873
+
874
+ // Add the first point of the first segment
875
+ newPoints.push(originalStroke.points[segmentIndices[0]]);
876
+
877
+ // Add all subsequent points
878
+ for (const segmentIndex of segmentIndices) {
879
+ newPoints.push(originalStroke.points[segmentIndex + 1]);
880
+ }
881
+
882
+ // Only create stroke if we have at least 2 points
883
+ if (newPoints.length < 2) return;
884
+
885
+ // Create new stroke with same properties
886
+ const newStroke = new Stroke({
887
+ strokeWidth: originalStroke.strokeWidth,
888
+ strokeColor: originalStroke.strokeColor,
889
+ strokeOpacity: originalStroke.strokeOpacity,
890
+ tool: originalStroke.tool
891
+ });
892
+
893
+ // Add all points
894
+ newPoints.forEach(point => {
895
+ newStroke.addPoint(point);
896
+ });
897
+
898
+ newStroke.finish();
899
+ this.canvas.addStroke(newStroke);
900
+ }
901
+
902
+ }