@teachinglab/omd 0.7.16 → 0.7.18

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.
@@ -14,7 +14,7 @@ export class CanvasConfig {
14
14
  this.gridSpacing = options.gridSpacing || 20;
15
15
 
16
16
  // Tool configuration
17
- this.enabledTools = options.enabledTools || ['pointer', 'pencil', 'eraser', 'select'];
17
+ this.enabledTools = options.enabledTools || ['pointer', 'pencil', 'eraser'];
18
18
  this.defaultTool = options.defaultTool || 'pointer';
19
19
 
20
20
  // Feature flags
@@ -36,11 +36,6 @@ export class CanvasConfig {
36
36
  size: 20,
37
37
  hardness: 0.8,
38
38
  ...options.tools?.eraser
39
- },
40
- select: {
41
- selectionColor: '#007bff',
42
- selectionOpacity: 0.3,
43
- ...options.tools?.select
44
39
  }
45
40
  };
46
41
 
@@ -53,7 +48,26 @@ export class CanvasConfig {
53
48
  danger: '#dc3545',
54
49
  ...options.theme
55
50
  };
56
-
51
+
52
+ // Selection / resize-handle styling
53
+ this.selection = {
54
+ // Selection border
55
+ border: {
56
+ color: options.selection?.border?.color ?? '#007bff',
57
+ width: options.selection?.border?.width ?? 2,
58
+ dasharray: options.selection?.border?.dasharray ?? '4,2',
59
+ cornerRadius: options.selection?.border?.cornerRadius ?? undefined,
60
+ },
61
+ // Resize handles
62
+ handle: {
63
+ size: options.selection?.handle?.size ?? 8,
64
+ color: options.selection?.handle?.color ?? '#007bff',
65
+ strokeColor: options.selection?.handle?.strokeColor ?? '#ffffff',
66
+ strokeWidth: options.selection?.handle?.strokeWidth ?? 1,
67
+ cornerRadius: options.selection?.handle?.cornerRadius ?? 1,
68
+ },
69
+ };
70
+
57
71
  // Validate configuration
58
72
  this._validate();
59
73
  }
@@ -69,13 +83,13 @@ export class CanvasConfig {
69
83
  }
70
84
 
71
85
  // Validate enabled tools
72
- const availableTools = ['pointer', 'pencil', 'eraser', 'select'];
86
+ const availableTools = ['pointer', 'pencil', 'eraser'];
73
87
  const invalidTools = this.enabledTools.filter(tool => !availableTools.includes(tool));
74
88
  if (invalidTools.length > 0) {
75
89
  console.warn(`Invalid tools specified: ${invalidTools.join(', ')}`);
76
90
  this.enabledTools = this.enabledTools.filter(tool => availableTools.includes(tool));
77
91
  }
78
-
92
+
79
93
  // Ensure at least one tool is enabled
80
94
  if (this.enabledTools.length === 0) {
81
95
  this.enabledTools = ['pencil'];
@@ -110,13 +124,8 @@ export class CanvasConfig {
110
124
  if (this.tools.eraser.hardness < 0 || this.tools.eraser.hardness > 1) {
111
125
  this.tools.eraser.hardness = 0.8;
112
126
  }
113
-
114
- // Validate select config
115
- if (this.tools.select.selectionOpacity < 0 || this.tools.select.selectionOpacity > 1) {
116
- this.tools.select.selectionOpacity = 0.3;
117
- }
118
127
  }
119
-
128
+
120
129
  /**
121
130
  * Update configuration
122
131
  * @param {Object} updates - Configuration updates
@@ -166,7 +175,8 @@ export class CanvasConfig {
166
175
  enableKeyboardShortcuts: this.enableKeyboardShortcuts,
167
176
  enableMultiTouch: this.enableMultiTouch,
168
177
  tools: JSON.parse(JSON.stringify(this.tools)),
169
- theme: { ...this.theme }
178
+ theme: { ...this.theme },
179
+ selection: JSON.parse(JSON.stringify(this.selection))
170
180
  });
171
181
  }
172
182
 
@@ -188,7 +198,8 @@ export class CanvasConfig {
188
198
  enableKeyboardShortcuts: this.enableKeyboardShortcuts,
189
199
  enableMultiTouch: this.enableMultiTouch,
190
200
  tools: this.tools,
191
- theme: this.theme
201
+ theme: this.theme,
202
+ selection: this.selection
192
203
  };
193
204
  }
194
205
 
@@ -3,7 +3,6 @@ import { EventManager } from '../events/eventManager.js';
3
3
  import { ToolManager } from '../tools/toolManager.js';
4
4
  import { PencilTool } from '../tools/PencilTool.js';
5
5
  import { EraserTool } from '../tools/EraserTool.js';
6
- import { SelectTool } from '../tools/SelectTool.js';
7
6
  import { PointerTool } from '../tools/PointerTool.js';
8
7
  import { Cursor } from '../ui/cursor.js';
9
8
  import { Toolbar } from '../ui/toolbar.js';
@@ -176,10 +175,6 @@ export class omdCanvas {
176
175
  if (this.config.enabledTools.includes('eraser')) {
177
176
  this.toolManager.registerTool('eraser', new EraserTool(this));
178
177
  }
179
- if (this.config.enabledTools.includes('select')) {
180
- this.toolManager.registerTool('select', new SelectTool(this));
181
- }
182
-
183
178
  // Set default tool
184
179
  if (this.config.defaultTool && this.config.enabledTools.includes(this.config.defaultTool)) {
185
180
  this.toolManager.setActiveTool(this.config.defaultTool);
@@ -204,6 +199,13 @@ export class omdCanvas {
204
199
  if (this.config.enableFocusFrames) {
205
200
  this.focusFrameManager = new FocusFrameManager(this);
206
201
  }
202
+
203
+ // Apply any selection styles defined in config to the ResizeHandleManager
204
+ // (PointerTool registers the manager on canvas.resizeHandleManager during its constructor)
205
+ if (this.resizeHandleManager && this.config.selection) {
206
+ this.resizeHandleManager.setSelectionStyle(this.config.selection.border);
207
+ this.resizeHandleManager.setHandleStyle(this.config.selection.handle);
208
+ }
207
209
  }
208
210
 
209
211
  /**
@@ -552,4 +554,42 @@ export class omdCanvas {
552
554
  this.isDestroyed = true;
553
555
  this.emit('destroyed');
554
556
  }
555
- }
557
+ /**
558
+ * Style the selection border shown when an OMD visual is selected.
559
+ * Can be called at any time; changes take effect immediately.
560
+ * @param {Object} style
561
+ * @param {string} [style.color] - Border stroke colour (e.g. '#007bff')
562
+ * @param {number} [style.width] - Border stroke width in px
563
+ * @param {string} [style.dasharray] - SVG stroke-dasharray (e.g. '4,2' or 'none')
564
+ * @param {number} [style.cornerRadius] - Border corner radius
565
+ */
566
+ setSelectionStyle(style = {}) {
567
+ if (this.resizeHandleManager) {
568
+ this.resizeHandleManager.setSelectionStyle(style);
569
+ }
570
+ // Persist into config so clones / serialisation reflect the change
571
+ if (this.config.selection?.border) {
572
+ Object.assign(this.config.selection.border, style);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Style the resize handles shown when an OMD visual is selected.
578
+ * Can be called at any time; changes take effect immediately.
579
+ * @param {Object} style
580
+ * @param {number} [style.size] - Handle size in px
581
+ * @param {string} [style.color] - Handle fill colour
582
+ * @param {string} [style.strokeColor] - Handle border colour
583
+ * @param {number} [style.strokeWidth] - Handle border width in px
584
+ * @param {number} [style.cornerRadius] - Handle corner radius (0 = square, size/2 = circle)
585
+ */
586
+ setHandleStyle(style = {}) {
587
+ if (this.resizeHandleManager) {
588
+ this.resizeHandleManager.setHandleStyle(style);
589
+ }
590
+ // Persist into config
591
+ if (this.config.selection?.handle) {
592
+ Object.assign(this.config.selection.handle, style);
593
+ }
594
+ }
595
+ }
@@ -24,7 +24,7 @@ export class ResizeHandleManager {
24
24
  // Resize constraints
25
25
  this.minSize = 20;
26
26
  this.maxSize = 800;
27
- this.maintainAspectRatio = false; // Can be toggled with shift key
27
+ this.maintainAspectRatio = true; // Always maintain aspect ratio by default
28
28
  }
29
29
 
30
30
  /**
@@ -195,25 +195,23 @@ export class ResizeHandleManager {
195
195
  break;
196
196
  }
197
197
 
198
- // Maintain aspect ratio if requested
198
+ // Maintain aspect ratio use the larger delta to drive the resize
199
199
  if (this.maintainAspectRatio) {
200
200
  const aspectRatio = startWidth / startHeight;
201
-
202
- if (handle.type.includes('e') || handle.type.includes('w')) {
203
- // Width-based resize
201
+ const widthChange = Math.abs(newWidth - startWidth);
202
+ const heightChange = Math.abs(newHeight - startHeight);
203
+
204
+ if (widthChange >= heightChange) {
204
205
  newHeight = newWidth / aspectRatio;
205
- } else if (handle.type.includes('n') || handle.type.includes('s')) {
206
- // Height-based resize
207
- newWidth = newHeight * aspectRatio;
206
+ // Correct offsetY for top-anchored corners
207
+ if (handle.type.includes('n')) {
208
+ offsetY = startHeight - newHeight;
209
+ }
208
210
  } else {
209
- // Corner resize - use the dimension with larger change
210
- const widthChange = Math.abs(newWidth - startWidth);
211
- const heightChange = Math.abs(newHeight - startHeight);
212
-
213
- if (widthChange > heightChange) {
214
- newHeight = newWidth / aspectRatio;
215
- } else {
216
- newWidth = newHeight * aspectRatio;
211
+ newWidth = newHeight * aspectRatio;
212
+ // Correct offsetX for left-anchored corners
213
+ if (handle.type.includes('w')) {
214
+ offsetX = startWidth - newWidth;
217
215
  }
218
216
  }
219
217
  }
@@ -264,6 +262,68 @@ export class ResizeHandleManager {
264
262
  this.resizeData = null;
265
263
  }
266
264
 
265
+ /**
266
+ * Style the selection border.
267
+ * @param {Object} style
268
+ * @param {string} [style.color] - Stroke colour of the border (e.g. '#007bff')
269
+ * @param {number} [style.width] - Stroke width in px
270
+ * @param {string} [style.dasharray] - SVG stroke-dasharray value (e.g. '4,2' or 'none')
271
+ * @param {number} [style.cornerRadius]- rx/ry corner radius of the border rect
272
+ */
273
+ setSelectionStyle({ color, width, dasharray, cornerRadius } = {}) {
274
+ if (color !== undefined) this.selectionBorderColor = color;
275
+ if (width !== undefined) this.selectionBorderWidth = width;
276
+ if (dasharray !== undefined) this.selectionBorderDasharray = dasharray;
277
+ if (cornerRadius !== undefined) this.selectionBorderCornerRadius = cornerRadius;
278
+
279
+ // Re-apply to live border if one exists
280
+ if (this.selectionBorder) {
281
+ this.selectionBorder.setAttribute('stroke', this.selectionBorderColor);
282
+ this.selectionBorder.setAttribute('stroke-width', this.selectionBorderWidth);
283
+ this.selectionBorder.setAttribute('stroke-dasharray',
284
+ this.selectionBorderDasharray !== undefined ? this.selectionBorderDasharray : '4,2');
285
+ if (this.selectionBorderCornerRadius !== undefined) {
286
+ this.selectionBorder.setAttribute('rx', this.selectionBorderCornerRadius);
287
+ this.selectionBorder.setAttribute('ry', this.selectionBorderCornerRadius);
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Style the resize handles.
294
+ * @param {Object} style
295
+ * @param {number} [style.size] - Handle square size in px
296
+ * @param {string} [style.color] - Fill colour
297
+ * @param {string} [style.strokeColor] - Border colour
298
+ * @param {number} [style.strokeWidth] - Border width
299
+ * @param {number} [style.cornerRadius] - rx/ry corner radius (0 = square, size/2 = circle)
300
+ */
301
+ setHandleStyle({ size, color, strokeColor, strokeWidth, cornerRadius } = {}) {
302
+ if (size !== undefined) this.handleSize = size;
303
+ if (color !== undefined) this.handleColor = color;
304
+ if (strokeColor !== undefined) this.handleStrokeColor = strokeColor;
305
+ if (strokeWidth !== undefined) this.handleStrokeWidth = strokeWidth;
306
+ if (cornerRadius !== undefined) this.handleCornerRadius = cornerRadius;
307
+
308
+ // Re-apply to any live handles
309
+ const radius = this.handleCornerRadius !== undefined
310
+ ? this.handleCornerRadius
311
+ : 1;
312
+ this.handles.forEach(h => {
313
+ h.element.setAttribute('width', this.handleSize);
314
+ h.element.setAttribute('height', this.handleSize);
315
+ h.element.setAttribute('fill', this.handleColor);
316
+ h.element.setAttribute('stroke', this.handleStrokeColor);
317
+ h.element.setAttribute('stroke-width', this.handleStrokeWidth);
318
+ h.element.setAttribute('rx', radius);
319
+ });
320
+
321
+ // Reposition so centres stay correct after a size change
322
+ if (this.handles.length > 0) {
323
+ this._updateHandlePositions();
324
+ }
325
+ }
326
+
267
327
  /**
268
328
  * Update handle positions for currently selected element (called externally)
269
329
  */
@@ -304,7 +364,12 @@ export class ResizeHandleManager {
304
364
  this.selectionBorder.setAttribute('fill', 'none');
305
365
  this.selectionBorder.setAttribute('stroke', this.selectionBorderColor);
306
366
  this.selectionBorder.setAttribute('stroke-width', this.selectionBorderWidth);
307
- this.selectionBorder.setAttribute('stroke-dasharray', '4,2');
367
+ this.selectionBorder.setAttribute('stroke-dasharray',
368
+ this.selectionBorderDasharray !== undefined ? this.selectionBorderDasharray : '4,2');
369
+ if (this.selectionBorderCornerRadius !== undefined) {
370
+ this.selectionBorder.setAttribute('rx', this.selectionBorderCornerRadius);
371
+ this.selectionBorder.setAttribute('ry', this.selectionBorderCornerRadius);
372
+ }
308
373
  this.selectionBorder.style.pointerEvents = 'none';
309
374
  this.selectionBorder.classList.add('omd-selection-border');
310
375
 
@@ -346,15 +411,12 @@ export class ResizeHandleManager {
346
411
  _createResizeHandles() {
347
412
  if (!this.selectedElement) return;
348
413
 
414
+ // Only corner handles — mid-edge handles would break aspect ratio
349
415
  const handleTypes = [
350
416
  { type: 'nw', pos: 'top-left' },
351
- { type: 'n', pos: 'top-center' },
352
417
  { type: 'ne', pos: 'top-right' },
353
- { type: 'e', pos: 'middle-right' },
354
418
  { type: 'se', pos: 'bottom-right' },
355
- { type: 's', pos: 'bottom-center' },
356
- { type: 'sw', pos: 'bottom-left' },
357
- { type: 'w', pos: 'middle-left' }
419
+ { type: 'sw', pos: 'bottom-left' }
358
420
  ];
359
421
 
360
422
  handleTypes.forEach(handleDef => {
@@ -371,12 +433,13 @@ export class ResizeHandleManager {
371
433
  _createHandle(type, position) {
372
434
  const handle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
373
435
 
436
+ const cornerRadius = this.handleCornerRadius !== undefined ? this.handleCornerRadius : 1;
374
437
  handle.setAttribute('width', this.handleSize);
375
438
  handle.setAttribute('height', this.handleSize);
376
439
  handle.setAttribute('fill', this.handleColor);
377
440
  handle.setAttribute('stroke', this.handleStrokeColor);
378
441
  handle.setAttribute('stroke-width', this.handleStrokeWidth);
379
- handle.setAttribute('rx', 1);
442
+ handle.setAttribute('rx', cornerRadius);
380
443
  handle.style.cursor = this.getCursorForHandle(type);
381
444
  handle.classList.add('resize-handle', `resize-handle-${type}`);
382
445
 
package/canvas/index.js CHANGED
@@ -12,7 +12,6 @@ export { ToolManager } from './tools/toolManager.js';
12
12
  export { Tool } from './tools/tool.js';
13
13
  export { PencilTool } from './tools/PencilTool.js';
14
14
  export { EraserTool } from './tools/EraserTool.js';
15
- export { SelectTool } from './tools/SelectTool.js';
16
15
 
17
16
  // UI components
18
17
  export { Toolbar } from './ui/toolbar.js';
@@ -3,14 +3,15 @@ import { BoundingBox } from '../utils/boundingBox.js';
3
3
  import { Stroke } from '../drawing/stroke.js';
4
4
  import { ResizeHandleManager } from '../features/resizeHandleManager.js';
5
5
  import {omdColor} from '../../src/omdColor.js';
6
+
6
7
  const SELECTION_TOLERANCE = 10;
7
8
  const SELECTION_COLOR = '#007bff';
8
9
  const SELECTION_OPACITY = 0.3;
9
10
 
10
11
  /**
11
- * Pointer/Select tool
12
- * Combined tool for browsing, selecting, moving, and interacting with elements.
13
- * @extends Tool
12
+ * Pointer tool
13
+ * Combined functionality of Pointer and Select tools.
14
+ * Allows selecting, moving, deleting items, and interacting with components.
14
15
  */
15
16
  export class PointerTool extends Tool {
16
17
  /**
@@ -25,10 +26,10 @@ export class PointerTool extends Tool {
25
26
  });
26
27
 
27
28
  this.displayName = 'Pointer';
28
- this.description = 'Select, move, and interact';
29
+ this.description = 'Select and interact with components';
29
30
  this.icon = 'pointer';
30
31
  this.shortcut = 'V';
31
- this.category = 'selection';
32
+ this.category = 'navigation';
32
33
 
33
34
  /** @private */
34
35
  this.isSelecting = false;
@@ -96,12 +97,6 @@ export class PointerTool extends Tool {
96
97
  this.dragStartPoint = { x: event.x, y: event.y };
97
98
  this.potentialDeselect = segmentSelection;
98
99
 
99
- // Also enable OMD dragging if we have selected OMD elements
100
- if (this.selectedOMDElements.size > 0) {
101
- this.isDraggingOMD = true;
102
- this.startPoint = { x: event.x, y: event.y };
103
- }
104
-
105
100
  // Set isDrawing so we get pointermove events
106
101
  if (this.canvas.eventManager) {
107
102
  this.canvas.eventManager.isDrawing = true;
@@ -134,13 +129,6 @@ export class PointerTool extends Tool {
134
129
  this.draggedOMDElement = omdElement; // Primary drag target
135
130
  this.startPoint = { x: event.x, y: event.y };
136
131
 
137
- // Also enable stroke dragging if we have selected strokes
138
- if (this.selectedSegments.size > 0) {
139
- this.isDraggingStrokes = true;
140
- this.dragStartPoint = { x: event.x, y: event.y };
141
- this.hasSeparatedForDrag = false;
142
- }
143
-
144
132
  // Show resize handles if this is the only selected element
145
133
  if (this.selectedOMDElements.size === 1) {
146
134
  this.resizeHandleManager.selectElement(omdElement);
@@ -391,7 +379,7 @@ export class PointerTool extends Tool {
391
379
  // Ensure cursor is visible and properly set
392
380
  if (this.canvas.cursor) {
393
381
  this.canvas.cursor.show();
394
- this.canvas.cursor.setShape('pointer'); // Use pointer arrow cursor
382
+ this.canvas.cursor.setShape('pointer');
395
383
  }
396
384
 
397
385
  // Clear any existing selection to start fresh
@@ -434,7 +422,7 @@ export class PointerTool extends Tool {
434
422
  );
435
423
  }
436
424
 
437
- return 'default'; // Use default cursor instead of 'select'
425
+ return 'pointer';
438
426
  }
439
427
 
440
428
  /**
@@ -677,11 +665,11 @@ export class PointerTool extends Tool {
677
665
 
678
666
  /**
679
667
  * Gets the bounds of an OMD element including transform
680
- * Uses clip paths for accurate bounds when present (e.g., coordinate planes)
681
668
  * @private
682
669
  */
683
670
  _getOMDElementBounds(item) {
684
671
  try {
672
+ const bbox = item.getBBox();
685
673
  const transform = item.getAttribute('transform') || '';
686
674
  let offsetX = 0, offsetY = 0, scaleX = 1, scaleY = 1;
687
675
 
@@ -697,68 +685,6 @@ export class PointerTool extends Tool {
697
685
  scaleY = scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX;
698
686
  }
699
687
 
700
- // Get the content element (usually an SVG inside the wrapper)
701
- const content = item.firstElementChild;
702
- let bbox;
703
-
704
- if (content) {
705
- // Check for clip paths to get accurate visible bounds
706
- // This is important for coordinate planes where graph lines extend beyond visible area
707
- const clipPaths = content.querySelectorAll('clipPath');
708
-
709
- if (clipPaths.length > 0) {
710
- let maxArea = 0;
711
- let clipBounds = null;
712
-
713
- for (const clipPath of clipPaths) {
714
- const rect = clipPath.querySelector('rect');
715
- if (rect) {
716
- const w = parseFloat(rect.getAttribute('width')) || 0;
717
- const h = parseFloat(rect.getAttribute('height')) || 0;
718
- const x = parseFloat(rect.getAttribute('x')) || 0;
719
- const y = parseFloat(rect.getAttribute('y')) || 0;
720
-
721
- const area = w * h;
722
- if (area > maxArea) {
723
- maxArea = area;
724
-
725
- // Find the transform on the content group
726
- const contentGroup = content.firstElementChild;
727
- let tx = 0, ty = 0;
728
- if (contentGroup) {
729
- const contentTransform = contentGroup.getAttribute('transform');
730
- if (contentTransform) {
731
- const contentTranslateMatch = contentTransform.match(/translate\(\s*([^,]+)(?:,\s*([^)]+))?\s*\)/);
732
- if (contentTranslateMatch) {
733
- tx = parseFloat(contentTranslateMatch[1]) || 0;
734
- ty = parseFloat(contentTranslateMatch[2]) || 0;
735
- }
736
- }
737
- }
738
-
739
- clipBounds = {
740
- x: x + tx,
741
- y: y + ty,
742
- width: w,
743
- height: h
744
- };
745
- }
746
- }
747
- }
748
-
749
- if (clipBounds) {
750
- bbox = clipBounds;
751
- }
752
- }
753
-
754
- // Fallback to getBBox if no clip path found
755
- if (!bbox) {
756
- bbox = content.getBBox();
757
- }
758
- } else {
759
- bbox = item.getBBox();
760
- }
761
-
762
688
  return {
763
689
  x: offsetX + (bbox.x * scaleX),
764
690
  y: offsetY + (bbox.y * scaleY),
@@ -814,20 +740,42 @@ export class PointerTool extends Tool {
814
740
  continue;
815
741
  }
816
742
  try {
817
- // Use the shared bounds calculation method (handles clip paths correctly)
818
- const itemBounds = this._getOMDElementBounds(item);
743
+ // Get the bounding box of the item
744
+ const bbox = item.getBBox();
819
745
 
820
- if (itemBounds) {
821
- // Add some padding for easier clicking
822
- const padding = 10;
823
-
824
- // Check if point is within the bounds (with padding)
825
- if (x >= itemBounds.x - padding &&
826
- x <= itemBounds.x + itemBounds.width + padding &&
827
- y >= itemBounds.y - padding &&
828
- y <= itemBounds.y + itemBounds.height + padding) {
829
- return item;
830
- }
746
+ // Parse transform to get the actual position
747
+ const transform = item.getAttribute('transform') || '';
748
+ let offsetX = 0, offsetY = 0, scaleX = 1, scaleY = 1;
749
+
750
+ // Parse translate
751
+ const translateMatch = transform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
752
+ if (translateMatch) {
753
+ offsetX = parseFloat(translateMatch[1]) || 0;
754
+ offsetY = parseFloat(translateMatch[2]) || 0;
755
+ }
756
+
757
+ // Parse scale
758
+ const scaleMatch = transform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
759
+ if (scaleMatch) {
760
+ scaleX = parseFloat(scaleMatch[1]) || 1;
761
+ scaleY = scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX;
762
+ }
763
+
764
+ // Calculate the actual bounds including transform
765
+ const actualX = offsetX + (bbox.x * scaleX);
766
+ const actualY = offsetY + (bbox.y * scaleY);
767
+ const actualWidth = bbox.width * scaleX;
768
+ const actualHeight = bbox.height * scaleY;
769
+
770
+ // Add some padding for easier clicking
771
+ const padding = 10;
772
+
773
+ // Check if point is within the bounds (with padding)
774
+ if (x >= actualX - padding &&
775
+ x <= actualX + actualWidth + padding &&
776
+ y >= actualY - padding &&
777
+ y <= actualY + actualHeight + padding) {
778
+ return item;
831
779
  }
832
780
  } catch (error) {
833
781
  // Skip items that can't be measured (continue with next)
@@ -1024,6 +972,12 @@ export class PointerTool extends Tool {
1024
972
  selectionLayer.removeChild(selectionLayer.firstChild);
1025
973
  }
1026
974
 
975
+ // If the selection is pure-OMD (no strokes), ResizeHandleManager already draws
976
+ // the selection border + handles — don't draw a second box on top of it.
977
+ if (this.selectedSegments.size === 0 && this.selectedOMDElements.size > 0) {
978
+ return;
979
+ }
980
+
1027
981
  const bounds = this._getSelectionBounds();
1028
982
  if (!bounds) return;
1029
983
 
@@ -1320,3 +1274,4 @@ export class PointerTool extends Tool {
1320
1274
 
1321
1275
  }
1322
1276
 
1277
+
@@ -195,12 +195,10 @@ export class Cursor {
195
195
  _createPointerCursor() {
196
196
  const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
197
197
  group.setAttribute('data-shape', 'pointer');
198
- // Center the hotspot (approx 14, 5) to 0,0
199
- group.setAttribute('transform', 'translate(-14, -5)');
200
198
 
201
- // Custom arrow pointer cursor
199
+ // Standard arrow pointer cursor
202
200
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
203
- path.setAttribute('d', 'M 29.699219 47 C 29.578125 47 29.457031 46.976563 29.339844 46.933594 C 29.089844 46.835938 28.890625 46.644531 28.78125 46.398438 L 22.945313 32.90625 L 15.683594 39.730469 C 15.394531 40.003906 14.96875 40.074219 14.601563 39.917969 C 14.238281 39.761719 14 39.398438 14 39 L 14 6 C 14 5.601563 14.234375 5.242188 14.601563 5.082031 C 14.964844 4.925781 15.390625 4.996094 15.683594 5.269531 L 39.683594 27.667969 C 39.972656 27.9375 40.074219 28.355469 39.945313 28.726563 C 39.816406 29.101563 39.480469 29.363281 39.085938 29.398438 L 28.902344 30.273438 L 35.007813 43.585938 C 35.117188 43.824219 35.128906 44.101563 35.035156 44.351563 C 34.941406 44.601563 34.757813 44.800781 34.515625 44.910156 L 30.113281 46.910156 C 29.980469 46.96875 29.84375 47 29.699219 47 Z');
201
+ path.setAttribute('d', 'M 2,2 L 2,16 L 7,11 L 10,18 L 12,17 L 9,10 L 16,10 Z');
204
202
  path.setAttribute('fill', 'white');
205
203
  path.setAttribute('stroke', '#000000');
206
204
  path.setAttribute('stroke-width', '1.2');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teachinglab/omd",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
4
4
  "description": "omd",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -16,7 +16,7 @@
16
16
  "omd/",
17
17
  "docs/",
18
18
  "canvas/",
19
- "jsvg/" ,
19
+ "jsvg/",
20
20
  "npm-docs/",
21
21
  "README.md"
22
22
  ],
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@teachinglab/jsvg": "^0.1.1",
36
36
  "mathjs": "^14.5.2",
37
- "openai": "6.6.0"
37
+ "openai": "6.6.0"
38
38
  },
39
39
  "scripts": {
40
40
  "dev": "npm run build:docs && vite",