@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.
@@ -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
- // Clicking on a stroke segment - clear OMD selection and handle segment selection
82
- this.resizeHandleManager.clearSelection();
83
- this._handleSegmentClick(segmentSelection, event.shiftKey);
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
- // Add highlights for currently selected segments
691
- let highlightCount = 0;
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
- // Create highlight element
704
- const highlight = document.createElementNS('http://www.w3.org/2000/svg', 'line');
705
- highlight.setAttribute('x1', p1.x);
706
- highlight.setAttribute('y1', p1.y);
707
- highlight.setAttribute('x2', p2.x);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",