@teachinglab/omd 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/canvas/core/canvasConfig.js +3 -3
- package/canvas/core/omdCanvas.js +479 -479
- package/canvas/events/eventManager.js +14 -4
- package/canvas/features/focusFrameManager.js +284 -286
- package/canvas/features/resizeHandleManager.js +482 -0
- package/canvas/index.js +2 -1
- package/canvas/tools/EraserTool.js +321 -322
- package/canvas/tools/PencilTool.js +321 -324
- package/canvas/tools/PointerTool.js +71 -0
- package/canvas/tools/SelectTool.js +902 -457
- package/canvas/tools/toolManager.js +389 -393
- package/canvas/ui/cursor.js +462 -437
- package/canvas/ui/toolbar.js +291 -290
- package/docs/omd-objects.md +258 -0
- package/jsvg/jsvg.js +898 -0
- package/jsvg/jsvgComponents.js +359 -0
- package/omd/nodes/omdEquationSequenceNode.js +1280 -1246
- package/package.json +1 -1
- package/src/json-schemas.md +546 -78
- package/src/omd.js +212 -109
- package/src/omdEquation.js +188 -156
- package/src/omdExpression.js +7 -0
- package/src/omdFunction.js +5 -3
- package/src/omdProblem.js +216 -11
- package/src/omdString.js +12 -1
- package/src/omdUtils.js +84 -0
- package/src/omdVariable.js +1 -0
|
@@ -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 {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @param {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
this.
|
|
232
|
-
this.
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
this.canvas.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
*
|
|
442
|
-
* @
|
|
443
|
-
* @
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
}
|