@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.
- package/canvas/core/canvasConfig.js +28 -17
- package/canvas/core/omdCanvas.js +46 -6
- package/canvas/features/resizeHandleManager.js +86 -23
- package/canvas/index.js +0 -1
- package/canvas/tools/PointerTool.js +51 -96
- package/canvas/ui/cursor.js +2 -4
- package/package.json +3 -3
- package/src/json-schemas.md +225 -25
- package/src/omdCoordinatePlane.js +12 -0
- package/canvas/tools/SelectTool.js +0 -1320
|
@@ -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'
|
|
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'
|
|
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
|
|
package/canvas/core/omdCanvas.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
// Correct offsetY for top-anchored corners
|
|
207
|
+
if (handle.type.includes('n')) {
|
|
208
|
+
offsetY = startHeight - newHeight;
|
|
209
|
+
}
|
|
208
210
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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',
|
|
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: '
|
|
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',
|
|
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
|
|
12
|
-
* Combined
|
|
13
|
-
*
|
|
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
|
|
29
|
+
this.description = 'Select and interact with components';
|
|
29
30
|
this.icon = 'pointer';
|
|
30
31
|
this.shortcut = 'V';
|
|
31
|
-
this.category = '
|
|
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');
|
|
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 '
|
|
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
|
-
//
|
|
818
|
-
const
|
|
743
|
+
// Get the bounding box of the item
|
|
744
|
+
const bbox = item.getBBox();
|
|
819
745
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
|
package/canvas/ui/cursor.js
CHANGED
|
@@ -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
|
-
//
|
|
199
|
+
// Standard arrow pointer cursor
|
|
202
200
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
203
|
-
path.setAttribute('d', 'M
|
|
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.
|
|
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",
|