@teachinglab/omd 0.7.5 → 0.7.7

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