@teachinglab/omd 0.7.3 → 0.7.5
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/tools/SelectTool.js +205 -24
- package/package.json +1 -1
|
@@ -41,6 +41,12 @@ export class SelectTool extends Tool {
|
|
|
41
41
|
/** @private - OMD dragging state */
|
|
42
42
|
this.isDraggingOMD = false;
|
|
43
43
|
this.draggedOMDElement = null;
|
|
44
|
+
|
|
45
|
+
/** @private - Stroke dragging state */
|
|
46
|
+
this.isDraggingStrokes = false;
|
|
47
|
+
this.dragStartPoint = null;
|
|
48
|
+
this.potentialDeselect = null;
|
|
49
|
+
this.hasSeparatedForDrag = false;
|
|
44
50
|
|
|
45
51
|
// Initialize resize handle manager for OMD visuals
|
|
46
52
|
this.resizeHandleManager = new ResizeHandleManager(canvas);
|
|
@@ -78,9 +84,35 @@ export class SelectTool extends Tool {
|
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
if (segmentSelection) {
|
|
81
|
-
//
|
|
82
|
-
this.
|
|
83
|
-
|
|
87
|
+
// Check if already selected
|
|
88
|
+
const isSelected = this._isSegmentSelected(segmentSelection);
|
|
89
|
+
|
|
90
|
+
if (isSelected) {
|
|
91
|
+
// Already selected - prepare for drag, but don't deselect yet
|
|
92
|
+
this.isDraggingStrokes = true;
|
|
93
|
+
this.hasSeparatedForDrag = false;
|
|
94
|
+
this.dragStartPoint = { x: event.x, y: event.y };
|
|
95
|
+
this.potentialDeselect = segmentSelection;
|
|
96
|
+
|
|
97
|
+
// Set isDrawing so we get pointermove events
|
|
98
|
+
if (this.canvas.eventManager) {
|
|
99
|
+
this.canvas.eventManager.isDrawing = true;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
} else {
|
|
103
|
+
// Clicking on a stroke segment - clear OMD selection and handle segment selection
|
|
104
|
+
this.resizeHandleManager.clearSelection();
|
|
105
|
+
this._handleSegmentClick(segmentSelection, event.shiftKey);
|
|
106
|
+
|
|
107
|
+
// Prepare for drag immediately after selection
|
|
108
|
+
this.isDraggingStrokes = true;
|
|
109
|
+
this.hasSeparatedForDrag = false;
|
|
110
|
+
this.dragStartPoint = { x: event.x, y: event.y };
|
|
111
|
+
|
|
112
|
+
if (this.canvas.eventManager) {
|
|
113
|
+
this.canvas.eventManager.isDrawing = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
84
116
|
} else if (omdElement) {
|
|
85
117
|
// Clicking on an OMD visual - clear segment selection and select OMD
|
|
86
118
|
if (!event.shiftKey) {
|
|
@@ -132,6 +164,43 @@ export class SelectTool extends Tool {
|
|
|
132
164
|
this._dragOMDElement(event.x, event.y);
|
|
133
165
|
return;
|
|
134
166
|
}
|
|
167
|
+
|
|
168
|
+
// Handle stroke dragging if in progress
|
|
169
|
+
if (this.isDraggingStrokes && this.dragStartPoint) {
|
|
170
|
+
const dx = event.x - this.dragStartPoint.x;
|
|
171
|
+
const dy = event.y - this.dragStartPoint.y;
|
|
172
|
+
|
|
173
|
+
if (dx === 0 && dy === 0) return;
|
|
174
|
+
|
|
175
|
+
// If we moved, it's a drag, so cancel potential deselect
|
|
176
|
+
this.potentialDeselect = null;
|
|
177
|
+
|
|
178
|
+
// Separate selected parts if needed
|
|
179
|
+
if (!this.hasSeparatedForDrag) {
|
|
180
|
+
this._separateSelectedParts();
|
|
181
|
+
this.hasSeparatedForDrag = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Move all selected strokes
|
|
185
|
+
const movedStrokes = new Set();
|
|
186
|
+
for (const [strokeId, _] of this.selectedSegments) {
|
|
187
|
+
const stroke = this.canvas.strokes.get(strokeId);
|
|
188
|
+
if (stroke) {
|
|
189
|
+
stroke.move(dx, dy);
|
|
190
|
+
movedStrokes.add(strokeId);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.dragStartPoint = { x: event.x, y: event.y };
|
|
195
|
+
this._updateSegmentSelectionVisuals();
|
|
196
|
+
|
|
197
|
+
// Emit event
|
|
198
|
+
this.canvas.emit('strokesMoved', {
|
|
199
|
+
dx, dy,
|
|
200
|
+
strokeIds: Array.from(movedStrokes)
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
135
204
|
|
|
136
205
|
// Handle box selection if in progress
|
|
137
206
|
if (!this.isSelecting || !this.selectionBox) return;
|
|
@@ -164,6 +233,23 @@ export class SelectTool extends Tool {
|
|
|
164
233
|
}
|
|
165
234
|
return;
|
|
166
235
|
}
|
|
236
|
+
|
|
237
|
+
// Handle stroke drag completion
|
|
238
|
+
if (this.isDraggingStrokes) {
|
|
239
|
+
if (this.potentialDeselect) {
|
|
240
|
+
// We clicked a selected segment but didn't drag -> toggle selection
|
|
241
|
+
this._handleSegmentClick(this.potentialDeselect, event.shiftKey);
|
|
242
|
+
this.potentialDeselect = null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.isDraggingStrokes = false;
|
|
246
|
+
this.dragStartPoint = null;
|
|
247
|
+
|
|
248
|
+
if (this.canvas.eventManager) {
|
|
249
|
+
this.canvas.eventManager.isDrawing = false;
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
167
253
|
|
|
168
254
|
// Handle box selection completion
|
|
169
255
|
if (this.isSelecting) {
|
|
@@ -396,6 +482,17 @@ export class SelectTool extends Tool {
|
|
|
396
482
|
return selected;
|
|
397
483
|
}
|
|
398
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Checks if a segment is currently selected.
|
|
487
|
+
* @private
|
|
488
|
+
* @param {{strokeId: string, segmentIndex: number}} selection - The selection to check.
|
|
489
|
+
* @returns {boolean}
|
|
490
|
+
*/
|
|
491
|
+
_isSegmentSelected({ strokeId, segmentIndex }) {
|
|
492
|
+
const segmentSet = this.selectedSegments.get(strokeId);
|
|
493
|
+
return segmentSet ? segmentSet.has(segmentIndex) : false;
|
|
494
|
+
}
|
|
495
|
+
|
|
399
496
|
/**
|
|
400
497
|
* Drags the selected OMD element
|
|
401
498
|
* @private
|
|
@@ -687,9 +784,12 @@ export class SelectTool extends Tool {
|
|
|
687
784
|
selectionLayer.removeChild(selectionLayer.firstChild);
|
|
688
785
|
}
|
|
689
786
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
787
|
+
if (this.selectedSegments.size === 0) return;
|
|
788
|
+
|
|
789
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
790
|
+
let hasSelection = false;
|
|
791
|
+
|
|
792
|
+
// Calculate bounding box of all selected segments
|
|
693
793
|
for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
|
|
694
794
|
const stroke = this.canvas.strokes.get(strokeId);
|
|
695
795
|
if (!stroke || !stroke.points) continue;
|
|
@@ -700,24 +800,43 @@ export class SelectTool extends Tool {
|
|
|
700
800
|
const p1 = stroke.points[segmentIndex];
|
|
701
801
|
const p2 = stroke.points[segmentIndex + 1];
|
|
702
802
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
highlight.setAttribute('y2', p2.y);
|
|
709
|
-
highlight.setAttribute('stroke', omdColor.hiliteColor);
|
|
710
|
-
highlight.setAttribute('stroke-width', '4');
|
|
711
|
-
highlight.setAttribute('stroke-opacity', '0.8');
|
|
712
|
-
highlight.setAttribute('stroke-linecap', 'round');
|
|
713
|
-
highlight.style.pointerEvents = 'none';
|
|
714
|
-
highlight.classList.add('selection-highlight');
|
|
715
|
-
|
|
716
|
-
selectionLayer.appendChild(highlight);
|
|
717
|
-
highlightCount++;
|
|
803
|
+
minX = Math.min(minX, p1.x, p2.x);
|
|
804
|
+
minY = Math.min(minY, p1.y, p2.y);
|
|
805
|
+
maxX = Math.max(maxX, p1.x, p2.x);
|
|
806
|
+
maxY = Math.max(maxY, p1.y, p2.y);
|
|
807
|
+
hasSelection = true;
|
|
718
808
|
}
|
|
719
809
|
}
|
|
720
|
-
|
|
810
|
+
|
|
811
|
+
if (!hasSelection) return;
|
|
812
|
+
|
|
813
|
+
// Add padding
|
|
814
|
+
const padding = 8;
|
|
815
|
+
minX -= padding;
|
|
816
|
+
minY -= padding;
|
|
817
|
+
maxX += padding;
|
|
818
|
+
maxY += padding;
|
|
819
|
+
|
|
820
|
+
const width = maxX - minX;
|
|
821
|
+
const height = maxY - minY;
|
|
822
|
+
|
|
823
|
+
// Create bounding box element
|
|
824
|
+
const box = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
825
|
+
box.setAttribute('x', minX);
|
|
826
|
+
box.setAttribute('y', minY);
|
|
827
|
+
box.setAttribute('width', width);
|
|
828
|
+
box.setAttribute('height', height);
|
|
829
|
+
box.setAttribute('fill', 'none');
|
|
830
|
+
box.setAttribute('stroke', '#007bff'); // Selection color
|
|
831
|
+
box.setAttribute('stroke-width', '1.5');
|
|
832
|
+
box.setAttribute('stroke-dasharray', '6, 4'); // Dotted/Dashed
|
|
833
|
+
box.setAttribute('stroke-opacity', '0.6'); // Light
|
|
834
|
+
box.setAttribute('rx', '8'); // Rounded corners
|
|
835
|
+
box.setAttribute('ry', '8');
|
|
836
|
+
box.style.pointerEvents = 'none';
|
|
837
|
+
box.classList.add('selection-bounds');
|
|
838
|
+
|
|
839
|
+
selectionLayer.appendChild(box);
|
|
721
840
|
}
|
|
722
841
|
|
|
723
842
|
/**
|
|
@@ -837,6 +956,66 @@ export class SelectTool extends Tool {
|
|
|
837
956
|
});
|
|
838
957
|
}
|
|
839
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Separates selected segments into new strokes so they can be moved independently.
|
|
961
|
+
* @private
|
|
962
|
+
*/
|
|
963
|
+
_separateSelectedParts() {
|
|
964
|
+
const newSelection = new Map();
|
|
965
|
+
const strokesToProcess = Array.from(this.selectedSegments.entries());
|
|
966
|
+
|
|
967
|
+
for (const [strokeId, selectedIndices] of strokesToProcess) {
|
|
968
|
+
const stroke = this.canvas.strokes.get(strokeId);
|
|
969
|
+
if (!stroke || !stroke.points || stroke.points.length < 2) continue;
|
|
970
|
+
|
|
971
|
+
const totalSegments = stroke.points.length - 1;
|
|
972
|
+
|
|
973
|
+
// If fully selected, just keep it as is
|
|
974
|
+
if (selectedIndices.size === totalSegments) {
|
|
975
|
+
newSelection.set(strokeId, selectedIndices);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// It's a partial selection - we need to split
|
|
980
|
+
const sortedSelectedIndices = Array.from(selectedIndices).sort((a, b) => a - b);
|
|
981
|
+
|
|
982
|
+
// Determine unselected indices
|
|
983
|
+
const unselectedIndices = [];
|
|
984
|
+
for (let i = 0; i < totalSegments; i++) {
|
|
985
|
+
if (!selectedIndices.has(i)) {
|
|
986
|
+
unselectedIndices.push(i);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Group segments
|
|
991
|
+
const selectedGroups = this._groupConsecutiveSegments(sortedSelectedIndices);
|
|
992
|
+
const unselectedGroups = this._groupConsecutiveSegments(unselectedIndices);
|
|
993
|
+
|
|
994
|
+
// Create new strokes for selected parts
|
|
995
|
+
selectedGroups.forEach(group => {
|
|
996
|
+
const newStroke = this._createStrokeFromSegments(stroke, group);
|
|
997
|
+
if (newStroke) {
|
|
998
|
+
// Add to new selection (all segments selected)
|
|
999
|
+
const newIndices = new Set();
|
|
1000
|
+
for (let i = 0; i < newStroke.points.length - 1; i++) {
|
|
1001
|
+
newIndices.add(i);
|
|
1002
|
+
}
|
|
1003
|
+
newSelection.set(newStroke.id, newIndices);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// Create new strokes for unselected parts (don't add to selection)
|
|
1008
|
+
unselectedGroups.forEach(group => {
|
|
1009
|
+
this._createStrokeFromSegments(stroke, group);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Remove original stroke
|
|
1013
|
+
this.canvas.removeStroke(strokeId);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
this.selectedSegments = newSelection;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
840
1019
|
/**
|
|
841
1020
|
* Groups consecutive segment indices into separate arrays.
|
|
842
1021
|
* @private
|
|
@@ -870,9 +1049,10 @@ export class SelectTool extends Tool {
|
|
|
870
1049
|
/**
|
|
871
1050
|
* Creates a new stroke from a group of segment indices.
|
|
872
1051
|
* @private
|
|
1052
|
+
* @returns {Stroke|null} The newly created stroke
|
|
873
1053
|
*/
|
|
874
1054
|
_createStrokeFromSegments(originalStroke, segmentIndices) {
|
|
875
|
-
if (segmentIndices.length === 0) return;
|
|
1055
|
+
if (segmentIndices.length === 0) return null;
|
|
876
1056
|
|
|
877
1057
|
// Collect points for the new stroke
|
|
878
1058
|
const newPoints = [];
|
|
@@ -886,7 +1066,7 @@ export class SelectTool extends Tool {
|
|
|
886
1066
|
}
|
|
887
1067
|
|
|
888
1068
|
// Only create stroke if we have at least 2 points
|
|
889
|
-
if (newPoints.length < 2) return;
|
|
1069
|
+
if (newPoints.length < 2) return null;
|
|
890
1070
|
|
|
891
1071
|
// Create new stroke with same properties
|
|
892
1072
|
const newStroke = new Stroke({
|
|
@@ -903,6 +1083,7 @@ export class SelectTool extends Tool {
|
|
|
903
1083
|
|
|
904
1084
|
newStroke.finish();
|
|
905
1085
|
this.canvas.addStroke(newStroke);
|
|
1086
|
+
return newStroke;
|
|
906
1087
|
}
|
|
907
1088
|
|
|
908
1089
|
}
|