@teachinglab/omd 0.7.5 → 0.7.6

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,7 @@ export class SelectTool extends Tool {
41
41
  /** @private - OMD dragging state */
42
42
  this.isDraggingOMD = false;
43
43
  this.draggedOMDElement = null;
44
+ this.selectedOMDElements = new Set();
44
45
 
45
46
  /** @private - Stroke dragging state */
46
47
  this.isDraggingStrokes = false;
@@ -100,8 +101,11 @@ export class SelectTool extends Tool {
100
101
  }
101
102
  return;
102
103
  } else {
103
- // Clicking on a stroke segment - clear OMD selection and handle segment selection
104
- this.resizeHandleManager.clearSelection();
104
+ // Clicking on a stroke segment
105
+ if (!event.shiftKey) {
106
+ this.resizeHandleManager.clearSelection();
107
+ this.selectedOMDElements.clear();
108
+ }
105
109
  this._handleSegmentClick(segmentSelection, event.shiftKey);
106
110
 
107
111
  // Prepare for drag immediately after selection
@@ -114,14 +118,33 @@ export class SelectTool extends Tool {
114
118
  }
115
119
  }
116
120
  } else if (omdElement) {
117
- // Clicking on an OMD visual - clear segment selection and select OMD
121
+ // Clicking on an OMD visual
122
+
123
+ // Check if already selected
124
+ if (this.selectedOMDElements.has(omdElement)) {
125
+ // Already selected - prepare for drag
126
+ this.isDraggingOMD = true;
127
+ this.draggedOMDElement = omdElement; // Primary drag target
128
+ this.startPoint = { x: event.x, y: event.y };
129
+
130
+ if (this.canvas.eventManager) {
131
+ this.canvas.eventManager.isDrawing = true;
132
+ }
133
+ return;
134
+ }
135
+
136
+ // New selection
118
137
  if (!event.shiftKey) {
119
138
  this.selectedSegments.clear();
120
139
  this._clearSelectionVisuals();
140
+ this.selectedOMDElements.clear();
141
+ this.resizeHandleManager.clearSelection();
121
142
  }
143
+
144
+ this.selectedOMDElements.add(omdElement);
122
145
  this.resizeHandleManager.selectElement(omdElement);
123
146
 
124
- // CRITICAL: Start tracking for potential drag operation
147
+ // Start tracking for potential drag operation
125
148
  this.isDraggingOMD = true;
126
149
  this.draggedOMDElement = omdElement;
127
150
  this.startPoint = { x: event.x, y: event.y };
@@ -131,11 +154,32 @@ export class SelectTool extends Tool {
131
154
  this.canvas.eventManager.isDrawing = true;
132
155
  }
133
156
 
134
- // Don't start box selection - we're either resizing or will be dragging
135
157
  return;
136
158
  } else {
159
+ // Check if clicking inside existing selection bounds
160
+ const selectionBounds = this._getSelectionBounds();
161
+ if (selectionBounds &&
162
+ event.x >= selectionBounds.x &&
163
+ event.x <= selectionBounds.x + selectionBounds.width &&
164
+ event.y >= selectionBounds.y &&
165
+ event.y <= selectionBounds.y + selectionBounds.height) {
166
+
167
+ // Drag the selection (strokes AND OMD elements)
168
+ this.isDraggingStrokes = true; // We reuse this flag for general dragging
169
+ this.isDraggingOMD = true; // Also set this for OMD elements
170
+ this.hasSeparatedForDrag = false;
171
+ this.dragStartPoint = { x: event.x, y: event.y };
172
+ this.startPoint = { x: event.x, y: event.y }; // For OMD dragging
173
+
174
+ if (this.canvas.eventManager) {
175
+ this.canvas.eventManager.isDrawing = true;
176
+ }
177
+ return;
178
+ }
179
+
137
180
  // Clicking on empty space - clear all selections and start box selection
138
181
  this.resizeHandleManager.clearSelection();
182
+ this.selectedOMDElements.clear();
139
183
  this._startBoxSelection(event.x, event.y, event.shiftKey);
140
184
 
141
185
  // CRITICAL: Set startPoint AFTER _startBoxSelection so it doesn't get cleared!
@@ -159,10 +203,12 @@ export class SelectTool extends Tool {
159
203
  return;
160
204
  }
161
205
 
206
+ let handled = false;
207
+
162
208
  // Handle OMD dragging if in progress
163
- if (this.isDraggingOMD && this.draggedOMDElement) {
164
- this._dragOMDElement(event.x, event.y);
165
- return;
209
+ if (this.isDraggingOMD) {
210
+ this._dragOMDElements(event.x, event.y);
211
+ handled = true;
166
212
  }
167
213
 
168
214
  // Handle stroke dragging if in progress
@@ -170,38 +216,40 @@ export class SelectTool extends Tool {
170
216
  const dx = event.x - this.dragStartPoint.x;
171
217
  const dy = event.y - this.dragStartPoint.y;
172
218
 
173
- if (dx === 0 && dy === 0) return;
174
-
175
- // If we moved, it's a drag, so cancel potential deselect
176
- this.potentialDeselect = null;
219
+ if (dx !== 0 || dy !== 0) {
220
+ // If we moved, it's a drag, so cancel potential deselect
221
+ this.potentialDeselect = null;
177
222
 
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);
223
+ // Separate selected parts if needed
224
+ if (!this.hasSeparatedForDrag) {
225
+ this._separateSelectedParts();
226
+ this.hasSeparatedForDrag = true;
191
227
  }
228
+
229
+ // Move all selected strokes
230
+ const movedStrokes = new Set();
231
+ for (const [strokeId, _] of this.selectedSegments) {
232
+ const stroke = this.canvas.strokes.get(strokeId);
233
+ if (stroke) {
234
+ stroke.move(dx, dy);
235
+ movedStrokes.add(strokeId);
236
+ }
237
+ }
238
+
239
+ this.dragStartPoint = { x: event.x, y: event.y };
240
+ this._updateSegmentSelectionVisuals();
241
+
242
+ // Emit event
243
+ this.canvas.emit('strokesMoved', {
244
+ dx, dy,
245
+ strokeIds: Array.from(movedStrokes)
246
+ });
192
247
  }
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;
248
+ handled = true;
203
249
  }
204
250
 
251
+ if (handled) return;
252
+
205
253
  // Handle box selection if in progress
206
254
  if (!this.isSelecting || !this.selectionBox) return;
207
255
 
@@ -389,6 +437,7 @@ export class SelectTool extends Tool {
389
437
  this.selectedSegments.clear();
390
438
 
391
439
  // Clear OMD selection
440
+ this.selectedOMDElements.clear();
392
441
  this.resizeHandleManager.clearSelection();
393
442
 
394
443
  // Remove selection box if it exists
@@ -494,19 +543,97 @@ export class SelectTool extends Tool {
494
543
  }
495
544
 
496
545
  /**
497
- * Drags the selected OMD element
546
+ * Gets the bounding box of the current selection (strokes + OMD).
547
+ * @private
548
+ * @returns {{x: number, y: number, width: number, height: number}|null}
549
+ */
550
+ _getSelectionBounds() {
551
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
552
+ let hasPoints = false;
553
+
554
+ // 1. Check strokes
555
+ if (this.selectedSegments.size > 0) {
556
+ for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
557
+ const stroke = this.canvas.strokes.get(strokeId);
558
+ if (!stroke || !stroke.points) continue;
559
+
560
+ for (const segmentIndex of segmentSet) {
561
+ if (segmentIndex >= stroke.points.length - 1) continue;
562
+ const p1 = stroke.points[segmentIndex];
563
+ const p2 = stroke.points[segmentIndex + 1];
564
+
565
+ minX = Math.min(minX, p1.x, p2.x);
566
+ minY = Math.min(minY, p1.y, p2.y);
567
+ maxX = Math.max(maxX, p1.x, p2.x);
568
+ maxY = Math.max(maxY, p1.y, p2.y);
569
+ hasPoints = true;
570
+ }
571
+ }
572
+ }
573
+
574
+ // 2. Check OMD elements
575
+ if (this.selectedOMDElements.size > 0) {
576
+ for (const element of this.selectedOMDElements) {
577
+ const bbox = this._getOMDElementBounds(element);
578
+ if (bbox) {
579
+ minX = Math.min(minX, bbox.x);
580
+ minY = Math.min(minY, bbox.y);
581
+ maxX = Math.max(maxX, bbox.x + bbox.width);
582
+ maxY = Math.max(maxY, bbox.y + bbox.height);
583
+ hasPoints = true;
584
+ }
585
+ }
586
+ }
587
+
588
+ if (!hasPoints) return null;
589
+
590
+ // Add padding to match the visual box
591
+ const padding = 8;
592
+ return {
593
+ x: minX - padding,
594
+ y: minY - padding,
595
+ width: (maxX + padding) - (minX - padding),
596
+ height: (maxY + padding) - (minY - padding)
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Drags all selected OMD elements
498
602
  * @private
499
603
  * @param {number} x - Current pointer x coordinate
500
604
  * @param {number} y - Current pointer y coordinate
501
605
  */
502
- _dragOMDElement(x, y) {
503
- if (!this.draggedOMDElement || !this.startPoint) return;
606
+ _dragOMDElements(x, y) {
607
+ if (!this.startPoint) return;
504
608
 
505
609
  const dx = x - this.startPoint.x;
506
610
  const dy = y - this.startPoint.y;
507
611
 
612
+ if (dx === 0 && dy === 0) return;
613
+
614
+ for (const element of this.selectedOMDElements) {
615
+ this._moveOMDElement(element, dx, dy);
616
+ }
617
+
618
+ // Update start point for next move
619
+ this.startPoint = { x, y };
620
+
621
+ // Update resize handles if we have a single selection
622
+ if (this.selectedOMDElements.size === 1) {
623
+ const element = this.selectedOMDElements.values().next().value;
624
+ this.resizeHandleManager.updateIfSelected(element);
625
+ }
626
+
627
+ this._updateSegmentSelectionVisuals();
628
+ }
629
+
630
+ /**
631
+ * Moves a single OMD element
632
+ * @private
633
+ */
634
+ _moveOMDElement(element, dx, dy) {
508
635
  // Parse current transform
509
- const currentTransform = this.draggedOMDElement.getAttribute('transform') || '';
636
+ const currentTransform = element.getAttribute('transform') || '';
510
637
  const translateMatch = currentTransform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
511
638
  const scaleMatch = currentTransform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
512
639
 
@@ -526,13 +653,40 @@ export class SelectTool extends Tool {
526
653
  newTransform += ` scale(${scaleX}, ${scaleY})`;
527
654
  }
528
655
 
529
- this.draggedOMDElement.setAttribute('transform', newTransform);
530
-
531
- // Update start point for next move
532
- this.startPoint = { x, y };
533
-
534
- // Update resize handles if element is selected
535
- this.resizeHandleManager.updateIfSelected(this.draggedOMDElement);
656
+ element.setAttribute('transform', newTransform);
657
+ }
658
+
659
+ /**
660
+ * Gets the bounds of an OMD element including transform
661
+ * @private
662
+ */
663
+ _getOMDElementBounds(item) {
664
+ try {
665
+ const bbox = item.getBBox();
666
+ const transform = item.getAttribute('transform') || '';
667
+ let offsetX = 0, offsetY = 0, scaleX = 1, scaleY = 1;
668
+
669
+ const translateMatch = transform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
670
+ if (translateMatch) {
671
+ offsetX = parseFloat(translateMatch[1]) || 0;
672
+ offsetY = parseFloat(translateMatch[2]) || 0;
673
+ }
674
+
675
+ const scaleMatch = transform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
676
+ if (scaleMatch) {
677
+ scaleX = parseFloat(scaleMatch[1]) || 1;
678
+ scaleY = scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX;
679
+ }
680
+
681
+ return {
682
+ x: offsetX + (bbox.x * scaleX),
683
+ y: offsetY + (bbox.y * scaleY),
684
+ width: bbox.width * scaleX,
685
+ height: bbox.height * scaleY
686
+ };
687
+ } catch (e) {
688
+ return null;
689
+ }
536
690
  }
537
691
 
538
692
  /**
@@ -711,6 +865,7 @@ export class SelectTool extends Tool {
711
865
  const height = parseFloat(this.selectionBox.getAttribute('height'));
712
866
  const selectionBounds = new BoundingBox(x, y, width, height);
713
867
 
868
+ // 1. Select strokes
714
869
  for (const [id, stroke] of this.canvas.strokes) {
715
870
  if (!stroke.points || stroke.points.length < 2) continue;
716
871
  for (let i = 0; i < stroke.points.length - 1; i++) {
@@ -724,6 +879,36 @@ export class SelectTool extends Tool {
724
879
  }
725
880
  }
726
881
  }
882
+
883
+ // 2. Select OMD elements
884
+ const omdLayer = this.canvas.drawingLayer?.querySelector('.omd-layer');
885
+ if (omdLayer) {
886
+ const omdItems = omdLayer.querySelectorAll('.omd-item');
887
+ for (const item of omdItems) {
888
+ if (item?.dataset?.locked === 'true') continue;
889
+
890
+ const itemBounds = this._getOMDElementBounds(item);
891
+ if (itemBounds) {
892
+ // Check intersection
893
+ const intersects = !(itemBounds.x > x + width ||
894
+ itemBounds.x + itemBounds.width < x ||
895
+ itemBounds.y > y + height ||
896
+ itemBounds.y + itemBounds.height < y);
897
+
898
+ if (intersects) {
899
+ this.selectedOMDElements.add(item);
900
+ }
901
+ }
902
+ }
903
+ }
904
+
905
+ // Update resize handles
906
+ if (this.selectedOMDElements.size === 1 && this.selectedSegments.size === 0) {
907
+ this.resizeHandleManager.selectElement(this.selectedOMDElements.values().next().value);
908
+ } else {
909
+ this.resizeHandleManager.clearSelection();
910
+ }
911
+
727
912
  this._updateSegmentSelectionVisuals();
728
913
  }
729
914
 
@@ -784,48 +969,15 @@ export class SelectTool extends Tool {
784
969
  selectionLayer.removeChild(selectionLayer.firstChild);
785
970
  }
786
971
 
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
793
- for (const [strokeId, segmentSet] of this.selectedSegments.entries()) {
794
- const stroke = this.canvas.strokes.get(strokeId);
795
- if (!stroke || !stroke.points) continue;
796
-
797
- for (const segmentIndex of segmentSet) {
798
- if (segmentIndex >= stroke.points.length - 1) continue;
799
-
800
- const p1 = stroke.points[segmentIndex];
801
- const p2 = stroke.points[segmentIndex + 1];
802
-
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;
808
- }
809
- }
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;
972
+ const bounds = this._getSelectionBounds();
973
+ if (!bounds) return;
822
974
 
823
975
  // Create bounding box element
824
976
  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);
977
+ box.setAttribute('x', bounds.x);
978
+ box.setAttribute('y', bounds.y);
979
+ box.setAttribute('width', bounds.width);
980
+ box.setAttribute('height', bounds.height);
829
981
  box.setAttribute('fill', 'none');
830
982
  box.setAttribute('stroke', '#007bff'); // Selection color
831
983
  box.setAttribute('stroke-width', '1.5');
@@ -850,12 +1002,13 @@ export class SelectTool extends Tool {
850
1002
  }
851
1003
 
852
1004
  /**
853
- * Selects all segments on the canvas.
1005
+ * Selects all segments and OMD elements on the canvas.
854
1006
  * @private
855
1007
  */
856
1008
  _selectAllSegments() {
857
1009
  // Clear current selection
858
1010
  this.selectedSegments.clear();
1011
+ this.selectedOMDElements.clear();
859
1012
 
860
1013
  let totalSegments = 0;
861
1014
 
@@ -874,6 +1027,17 @@ export class SelectTool extends Tool {
874
1027
  }
875
1028
  }
876
1029
 
1030
+ // Select all OMD elements
1031
+ const omdLayer = this.canvas.drawingLayer?.querySelector('.omd-layer');
1032
+ if (omdLayer) {
1033
+ const omdItems = omdLayer.querySelectorAll('.omd-item');
1034
+ for (const item of omdItems) {
1035
+ if (item?.dataset?.locked !== 'true') {
1036
+ this.selectedOMDElements.add(item);
1037
+ }
1038
+ }
1039
+ }
1040
+
877
1041
  // Update visuals
878
1042
  this._updateSegmentSelectionVisuals();
879
1043
 
@@ -885,42 +1049,55 @@ export class SelectTool extends Tool {
885
1049
  }
886
1050
 
887
1051
  /**
888
- * Deletes all currently selected segments efficiently.
1052
+ * Deletes all currently selected items (segments and OMD elements).
889
1053
  * @private
890
1054
  */
891
1055
  _deleteSelectedSegments() {
892
- if (this.selectedSegments.size === 0) return;
1056
+ let changed = false;
893
1057
 
894
- // Process each stroke that has selected segments
895
- const strokesToProcess = Array.from(this.selectedSegments.entries());
896
-
897
- for (const [strokeId, segmentIndices] of strokesToProcess) {
898
- const stroke = this.canvas.strokes.get(strokeId);
899
- if (!stroke || !stroke.points || stroke.points.length < 2) continue;
1058
+ // Delete OMD elements
1059
+ if (this.selectedOMDElements.size > 0) {
1060
+ for (const element of this.selectedOMDElements) {
1061
+ element.remove();
1062
+ }
1063
+ this.selectedOMDElements.clear();
1064
+ this.resizeHandleManager.clearSelection();
1065
+ changed = true;
1066
+ }
900
1067
 
901
- const sortedIndices = Array.from(segmentIndices).sort((a, b) => a - b);
902
-
903
- // If all or most segments are selected, just delete the whole stroke
904
- const totalSegments = stroke.points.length - 1;
905
- const selectedCount = sortedIndices.length;
1068
+ if (this.selectedSegments.size > 0) {
1069
+ // Process each stroke that has selected segments
1070
+ const strokesToProcess = Array.from(this.selectedSegments.entries());
906
1071
 
907
- if (selectedCount >= totalSegments * 0.8) {
908
- // Delete entire stroke
909
- this.canvas.removeStroke(strokeId);
910
- continue;
911
- }
1072
+ for (const [strokeId, segmentIndices] of strokesToProcess) {
1073
+ const stroke = this.canvas.strokes.get(strokeId);
1074
+ if (!stroke || !stroke.points || stroke.points.length < 2) continue;
912
1075
 
913
- // Split the stroke, keeping only unselected segments
914
- this._splitStrokeKeepingUnselected(stroke, sortedIndices);
1076
+ const sortedIndices = Array.from(segmentIndices).sort((a, b) => a - b);
1077
+
1078
+ // If all or most segments are selected, just delete the whole stroke
1079
+ const totalSegments = stroke.points.length - 1;
1080
+ const selectedCount = sortedIndices.length;
1081
+
1082
+ if (selectedCount >= totalSegments * 0.8) {
1083
+ // Delete entire stroke
1084
+ this.canvas.removeStroke(strokeId);
1085
+ continue;
1086
+ }
1087
+
1088
+ // Split the stroke, keeping only unselected segments
1089
+ this._splitStrokeKeepingUnselected(stroke, sortedIndices);
1090
+ }
1091
+ changed = true;
915
1092
  }
916
1093
 
917
- // Clear selection and update UI
918
- this.clearSelection();
919
-
920
- // Emit deletion event
921
- this.canvas.emit('segmentsDeleted', {
922
- strokesAffected: strokesToProcess.length
923
- });
1094
+ if (changed) {
1095
+ // Clear selection and update UI
1096
+ this.clearSelection();
1097
+
1098
+ // Emit deletion event
1099
+ this.canvas.emit('selectionDeleted');
1100
+ }
924
1101
  }
925
1102
 
926
1103
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",