@teachinglab/omd 0.3.8 → 0.3.9
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 +3 -3
- package/canvas/core/omdCanvas.js +479 -479
- package/canvas/events/eventManager.js +14 -4
- package/canvas/features/focusFrameManager.js +284 -286
- package/canvas/features/resizeHandleManager.js +482 -0
- package/canvas/index.js +2 -1
- package/canvas/tools/EraserTool.js +321 -322
- package/canvas/tools/PencilTool.js +321 -324
- package/canvas/tools/PointerTool.js +71 -0
- package/canvas/tools/SelectTool.js +902 -457
- package/canvas/tools/toolManager.js +389 -393
- package/canvas/ui/cursor.js +462 -437
- package/canvas/ui/toolbar.js +291 -290
- package/docs/omd-objects.md +258 -0
- package/jsvg/jsvg.js +898 -0
- package/jsvg/jsvgComponents.js +359 -0
- package/omd/nodes/omdEquationSequenceNode.js +1280 -1246
- package/package.json +1 -1
- package/src/json-schemas.md +546 -78
- package/src/omd.js +212 -109
- package/src/omdEquation.js +188 -162
- package/src/omdProblem.js +216 -11
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages resize handles for OMD visuals
|
|
3
|
+
* Provides functionality for creating, positioning, and handling resize operations
|
|
4
|
+
*/
|
|
5
|
+
export class ResizeHandleManager {
|
|
6
|
+
constructor(canvas) {
|
|
7
|
+
this.canvas = canvas;
|
|
8
|
+
this.selectedElement = null;
|
|
9
|
+
this.handles = [];
|
|
10
|
+
this.isResizing = false;
|
|
11
|
+
this.resizeData = null;
|
|
12
|
+
|
|
13
|
+
// Handle configuration
|
|
14
|
+
this.handleSize = 8;
|
|
15
|
+
this.handleColor = '#007bff';
|
|
16
|
+
this.handleStrokeColor = '#ffffff';
|
|
17
|
+
this.handleStrokeWidth = 1;
|
|
18
|
+
|
|
19
|
+
// Selection border config
|
|
20
|
+
this.selectionBorderColor = '#007bff';
|
|
21
|
+
this.selectionBorderWidth = 2;
|
|
22
|
+
this.selectionBorder = null;
|
|
23
|
+
|
|
24
|
+
// Resize constraints
|
|
25
|
+
this.minSize = 20;
|
|
26
|
+
this.maxSize = 800;
|
|
27
|
+
this.maintainAspectRatio = false; // Can be toggled with shift key
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Select an OMD visual element and show resize handles
|
|
32
|
+
* @param {SVGElement} element - The OMD wrapper element to select
|
|
33
|
+
*/
|
|
34
|
+
selectElement(element) {
|
|
35
|
+
this.clearSelection();
|
|
36
|
+
|
|
37
|
+
if (!element || !element.classList.contains('omd-item')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.selectedElement = element;
|
|
42
|
+
this._createSelectionBorder();
|
|
43
|
+
this._createResizeHandles();
|
|
44
|
+
this._updateHandlePositions();
|
|
45
|
+
|
|
46
|
+
// Add selected class for CSS styling
|
|
47
|
+
element.classList.add('omd-selected');
|
|
48
|
+
|
|
49
|
+
// Emit selection event
|
|
50
|
+
this.canvas.emit('omdElementSelected', { element });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Clear current selection and remove all handles
|
|
55
|
+
*/
|
|
56
|
+
clearSelection() {
|
|
57
|
+
if (this.selectedElement) {
|
|
58
|
+
this.selectedElement.classList.remove('omd-selected');
|
|
59
|
+
this.selectedElement = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this._removeSelectionBorder();
|
|
63
|
+
this._removeResizeHandles();
|
|
64
|
+
this.isResizing = false;
|
|
65
|
+
this.resizeData = null;
|
|
66
|
+
|
|
67
|
+
// Emit deselection event
|
|
68
|
+
this.canvas.emit('omdElementDeselected');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a point is over a resize handle
|
|
73
|
+
* @param {number} x - X coordinate in canvas coordinates
|
|
74
|
+
* @param {number} y - Y coordinate in canvas coordinates
|
|
75
|
+
* @returns {Object|null} Handle data if hit, null otherwise
|
|
76
|
+
*/
|
|
77
|
+
getHandleAtPoint(x, y) {
|
|
78
|
+
const tolerance = this.handleSize / 2 + 3; // Slightly larger hit area
|
|
79
|
+
|
|
80
|
+
for (const handle of this.handles) {
|
|
81
|
+
// Get handle position from its attributes
|
|
82
|
+
const handleX = parseFloat(handle.element.getAttribute('x')) + this.handleSize / 2;
|
|
83
|
+
const handleY = parseFloat(handle.element.getAttribute('y')) + this.handleSize / 2;
|
|
84
|
+
|
|
85
|
+
const distance = Math.hypot(x - handleX, y - handleY);
|
|
86
|
+
if (distance <= tolerance) {
|
|
87
|
+
return handle;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Start resize operation
|
|
95
|
+
* @param {Object} handle - Handle data
|
|
96
|
+
* @param {number} startX - Starting X coordinate
|
|
97
|
+
* @param {number} startY - Starting Y coordinate
|
|
98
|
+
* @param {boolean} maintainAspectRatio - Whether to maintain aspect ratio
|
|
99
|
+
*/
|
|
100
|
+
startResize(handle, startX, startY, maintainAspectRatio = false) {
|
|
101
|
+
if (!this.selectedElement || !handle) return;
|
|
102
|
+
|
|
103
|
+
this.isResizing = true;
|
|
104
|
+
this.maintainAspectRatio = maintainAspectRatio;
|
|
105
|
+
|
|
106
|
+
// Get current transform and bounds
|
|
107
|
+
const transform = this.selectedElement.getAttribute('transform') || 'translate(0,0)';
|
|
108
|
+
const match = transform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
|
|
109
|
+
const currentX = match ? parseFloat(match[1]) : 0;
|
|
110
|
+
const currentY = match ? parseFloat(match[2]) : 0;
|
|
111
|
+
|
|
112
|
+
// Get current scale
|
|
113
|
+
const scaleMatch = transform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
|
|
114
|
+
const currentScaleX = scaleMatch ? parseFloat(scaleMatch[1]) : 1;
|
|
115
|
+
const currentScaleY = scaleMatch ? (scaleMatch[2] ? parseFloat(scaleMatch[2]) : currentScaleX) : 1;
|
|
116
|
+
|
|
117
|
+
// Get bounding box of the content
|
|
118
|
+
const bbox = this._getElementBounds();
|
|
119
|
+
|
|
120
|
+
this.resizeData = {
|
|
121
|
+
handle,
|
|
122
|
+
startX,
|
|
123
|
+
startY,
|
|
124
|
+
originalTransform: transform,
|
|
125
|
+
currentX,
|
|
126
|
+
currentY,
|
|
127
|
+
currentScaleX,
|
|
128
|
+
currentScaleY,
|
|
129
|
+
originalBounds: bbox,
|
|
130
|
+
startWidth: bbox.width * currentScaleX,
|
|
131
|
+
startHeight: bbox.height * currentScaleY
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Add resizing class
|
|
135
|
+
this.selectedElement.classList.add('omd-resizing');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update resize operation
|
|
140
|
+
* @param {number} currentX - Current X coordinate
|
|
141
|
+
* @param {number} currentY - Current Y coordinate
|
|
142
|
+
*/
|
|
143
|
+
updateResize(currentX, currentY) {
|
|
144
|
+
if (!this.isResizing || !this.resizeData) return;
|
|
145
|
+
|
|
146
|
+
const { handle, startX, startY, originalBounds, startWidth, startHeight } = this.resizeData;
|
|
147
|
+
|
|
148
|
+
const deltaX = currentX - startX;
|
|
149
|
+
const deltaY = currentY - startY;
|
|
150
|
+
|
|
151
|
+
let newWidth = startWidth;
|
|
152
|
+
let newHeight = startHeight;
|
|
153
|
+
let offsetX = 0;
|
|
154
|
+
let offsetY = 0;
|
|
155
|
+
|
|
156
|
+
// Calculate new dimensions based on handle type
|
|
157
|
+
switch (handle.type) {
|
|
158
|
+
case 'nw': // Top-left corner
|
|
159
|
+
newWidth = startWidth - deltaX;
|
|
160
|
+
newHeight = startHeight - deltaY;
|
|
161
|
+
offsetX = deltaX;
|
|
162
|
+
offsetY = deltaY;
|
|
163
|
+
break;
|
|
164
|
+
case 'ne': // Top-right corner
|
|
165
|
+
newWidth = startWidth + deltaX;
|
|
166
|
+
newHeight = startHeight - deltaY;
|
|
167
|
+
offsetY = deltaY;
|
|
168
|
+
break;
|
|
169
|
+
case 'sw': // Bottom-left corner
|
|
170
|
+
newWidth = startWidth - deltaX;
|
|
171
|
+
newHeight = startHeight + deltaY;
|
|
172
|
+
offsetX = deltaX;
|
|
173
|
+
break;
|
|
174
|
+
case 'se': // Bottom-right corner
|
|
175
|
+
newWidth = startWidth + deltaX;
|
|
176
|
+
newHeight = startHeight + deltaY;
|
|
177
|
+
break;
|
|
178
|
+
case 'n': // Top edge
|
|
179
|
+
newHeight = startHeight - deltaY;
|
|
180
|
+
offsetY = deltaY;
|
|
181
|
+
break;
|
|
182
|
+
case 's': // Bottom edge
|
|
183
|
+
newHeight = startHeight + deltaY;
|
|
184
|
+
break;
|
|
185
|
+
case 'w': // Left edge
|
|
186
|
+
newWidth = startWidth - deltaX;
|
|
187
|
+
offsetX = deltaX;
|
|
188
|
+
break;
|
|
189
|
+
case 'e': // Right edge
|
|
190
|
+
newWidth = startWidth + deltaX;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Maintain aspect ratio if requested
|
|
195
|
+
if (this.maintainAspectRatio) {
|
|
196
|
+
const aspectRatio = startWidth / startHeight;
|
|
197
|
+
|
|
198
|
+
if (handle.type.includes('e') || handle.type.includes('w')) {
|
|
199
|
+
// Width-based resize
|
|
200
|
+
newHeight = newWidth / aspectRatio;
|
|
201
|
+
} else if (handle.type.includes('n') || handle.type.includes('s')) {
|
|
202
|
+
// Height-based resize
|
|
203
|
+
newWidth = newHeight * aspectRatio;
|
|
204
|
+
} else {
|
|
205
|
+
// Corner resize - use the dimension with larger change
|
|
206
|
+
const widthChange = Math.abs(newWidth - startWidth);
|
|
207
|
+
const heightChange = Math.abs(newHeight - startHeight);
|
|
208
|
+
|
|
209
|
+
if (widthChange > heightChange) {
|
|
210
|
+
newHeight = newWidth / aspectRatio;
|
|
211
|
+
} else {
|
|
212
|
+
newWidth = newHeight * aspectRatio;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Apply size constraints
|
|
218
|
+
newWidth = Math.max(this.minSize, Math.min(this.maxSize, newWidth));
|
|
219
|
+
newHeight = Math.max(this.minSize, Math.min(this.maxSize, newHeight));
|
|
220
|
+
|
|
221
|
+
// Calculate scale factors
|
|
222
|
+
const scaleX = newWidth / originalBounds.width;
|
|
223
|
+
const scaleY = newHeight / originalBounds.height;
|
|
224
|
+
|
|
225
|
+
// Calculate new position
|
|
226
|
+
const newX = this.resizeData.currentX + offsetX;
|
|
227
|
+
const newY = this.resizeData.currentY + offsetY;
|
|
228
|
+
|
|
229
|
+
// Apply transform
|
|
230
|
+
this.selectedElement.setAttribute('transform',
|
|
231
|
+
`translate(${newX}, ${newY}) scale(${scaleX}, ${scaleY})`);
|
|
232
|
+
|
|
233
|
+
// Update handle positions
|
|
234
|
+
this._updateHandlePositions();
|
|
235
|
+
this._updateSelectionBorder();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Finish resize operation
|
|
240
|
+
*/
|
|
241
|
+
finishResize() {
|
|
242
|
+
if (!this.isResizing) return;
|
|
243
|
+
|
|
244
|
+
this.isResizing = false;
|
|
245
|
+
|
|
246
|
+
if (this.selectedElement) {
|
|
247
|
+
this.selectedElement.classList.remove('omd-resizing');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update handle positions after resize is complete
|
|
251
|
+
this._updateHandlePositions();
|
|
252
|
+
this._updateSelectionBorder();
|
|
253
|
+
|
|
254
|
+
// Emit resize complete event
|
|
255
|
+
this.canvas.emit('omdElementResized', {
|
|
256
|
+
element: this.selectedElement,
|
|
257
|
+
transform: this.selectedElement.getAttribute('transform')
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
this.resizeData = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update handle positions for currently selected element (called externally)
|
|
265
|
+
*/
|
|
266
|
+
updateIfSelected(element) {
|
|
267
|
+
if (this.selectedElement && this.selectedElement === element) {
|
|
268
|
+
this._updateHandlePositions();
|
|
269
|
+
this._updateSelectionBorder();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get cursor for handle type
|
|
275
|
+
* @param {string} handleType - Type of handle
|
|
276
|
+
* @returns {string} CSS cursor value
|
|
277
|
+
*/
|
|
278
|
+
getCursorForHandle(handleType) {
|
|
279
|
+
const cursors = {
|
|
280
|
+
'nw': 'nw-resize',
|
|
281
|
+
'n': 'n-resize',
|
|
282
|
+
'ne': 'ne-resize',
|
|
283
|
+
'e': 'e-resize',
|
|
284
|
+
'se': 'se-resize',
|
|
285
|
+
's': 's-resize',
|
|
286
|
+
'sw': 'sw-resize',
|
|
287
|
+
'w': 'w-resize'
|
|
288
|
+
};
|
|
289
|
+
return cursors[handleType] || 'default';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create selection border around element
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_createSelectionBorder() {
|
|
297
|
+
if (!this.selectedElement) return;
|
|
298
|
+
|
|
299
|
+
this.selectionBorder = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
300
|
+
this.selectionBorder.setAttribute('fill', 'none');
|
|
301
|
+
this.selectionBorder.setAttribute('stroke', this.selectionBorderColor);
|
|
302
|
+
this.selectionBorder.setAttribute('stroke-width', this.selectionBorderWidth);
|
|
303
|
+
this.selectionBorder.setAttribute('stroke-dasharray', '4,2');
|
|
304
|
+
this.selectionBorder.style.pointerEvents = 'none';
|
|
305
|
+
this.selectionBorder.classList.add('omd-selection-border');
|
|
306
|
+
|
|
307
|
+
this.canvas.uiLayer.appendChild(this.selectionBorder);
|
|
308
|
+
this._updateSelectionBorder();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Update selection border position and size
|
|
313
|
+
* @private
|
|
314
|
+
*/
|
|
315
|
+
_updateSelectionBorder() {
|
|
316
|
+
if (!this.selectionBorder || !this.selectedElement) return;
|
|
317
|
+
|
|
318
|
+
const bounds = this._getTransformedBounds();
|
|
319
|
+
const padding = 3;
|
|
320
|
+
|
|
321
|
+
this.selectionBorder.setAttribute('x', bounds.x - padding);
|
|
322
|
+
this.selectionBorder.setAttribute('y', bounds.y - padding);
|
|
323
|
+
this.selectionBorder.setAttribute('width', bounds.width + padding * 2);
|
|
324
|
+
this.selectionBorder.setAttribute('height', bounds.height + padding * 2);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Remove selection border
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
_removeSelectionBorder() {
|
|
332
|
+
if (this.selectionBorder) {
|
|
333
|
+
this.selectionBorder.remove();
|
|
334
|
+
this.selectionBorder = null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create resize handles around the selected element
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
_createResizeHandles() {
|
|
343
|
+
if (!this.selectedElement) return;
|
|
344
|
+
|
|
345
|
+
const handleTypes = [
|
|
346
|
+
{ type: 'nw', pos: 'top-left' },
|
|
347
|
+
{ type: 'n', pos: 'top-center' },
|
|
348
|
+
{ type: 'ne', pos: 'top-right' },
|
|
349
|
+
{ type: 'e', pos: 'middle-right' },
|
|
350
|
+
{ type: 'se', pos: 'bottom-right' },
|
|
351
|
+
{ type: 's', pos: 'bottom-center' },
|
|
352
|
+
{ type: 'sw', pos: 'bottom-left' },
|
|
353
|
+
{ type: 'w', pos: 'middle-left' }
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
handleTypes.forEach(handleDef => {
|
|
357
|
+
const handle = this._createHandle(handleDef.type, handleDef.pos);
|
|
358
|
+
this.handles.push(handle);
|
|
359
|
+
this.canvas.uiLayer.appendChild(handle.element);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Create individual resize handle
|
|
365
|
+
* @private
|
|
366
|
+
*/
|
|
367
|
+
_createHandle(type, position) {
|
|
368
|
+
const handle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
369
|
+
|
|
370
|
+
handle.setAttribute('width', this.handleSize);
|
|
371
|
+
handle.setAttribute('height', this.handleSize);
|
|
372
|
+
handle.setAttribute('fill', this.handleColor);
|
|
373
|
+
handle.setAttribute('stroke', this.handleStrokeColor);
|
|
374
|
+
handle.setAttribute('stroke-width', this.handleStrokeWidth);
|
|
375
|
+
handle.setAttribute('rx', 1);
|
|
376
|
+
handle.style.cursor = this.getCursorForHandle(type);
|
|
377
|
+
handle.classList.add('resize-handle', `resize-handle-${type}`);
|
|
378
|
+
|
|
379
|
+
// Add hover effects
|
|
380
|
+
handle.addEventListener('mouseenter', () => {
|
|
381
|
+
handle.setAttribute('fill', '#0056b3');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
handle.addEventListener('mouseleave', () => {
|
|
385
|
+
handle.setAttribute('fill', this.handleColor);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
element: handle,
|
|
390
|
+
type,
|
|
391
|
+
position
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Update positions of all resize handles
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
_updateHandlePositions() {
|
|
400
|
+
if (!this.selectedElement || this.handles.length === 0) return;
|
|
401
|
+
|
|
402
|
+
const bounds = this._getTransformedBounds();
|
|
403
|
+
const halfHandle = this.handleSize / 2;
|
|
404
|
+
|
|
405
|
+
const positions = {
|
|
406
|
+
'nw': { x: bounds.x - halfHandle, y: bounds.y - halfHandle },
|
|
407
|
+
'n': { x: bounds.x + bounds.width/2 - halfHandle, y: bounds.y - halfHandle },
|
|
408
|
+
'ne': { x: bounds.x + bounds.width - halfHandle, y: bounds.y - halfHandle },
|
|
409
|
+
'e': { x: bounds.x + bounds.width - halfHandle, y: bounds.y + bounds.height/2 - halfHandle },
|
|
410
|
+
'se': { x: bounds.x + bounds.width - halfHandle, y: bounds.y + bounds.height - halfHandle },
|
|
411
|
+
's': { x: bounds.x + bounds.width/2 - halfHandle, y: bounds.y + bounds.height - halfHandle },
|
|
412
|
+
'sw': { x: bounds.x - halfHandle, y: bounds.y + bounds.height - halfHandle },
|
|
413
|
+
'w': { x: bounds.x - halfHandle, y: bounds.y + bounds.height/2 - halfHandle }
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
this.handles.forEach(handle => {
|
|
417
|
+
const pos = positions[handle.type];
|
|
418
|
+
if (pos) {
|
|
419
|
+
handle.element.setAttribute('x', pos.x);
|
|
420
|
+
handle.element.setAttribute('y', pos.y);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Remove all resize handles
|
|
427
|
+
* @private
|
|
428
|
+
*/
|
|
429
|
+
_removeResizeHandles() {
|
|
430
|
+
this.handles.forEach(handle => {
|
|
431
|
+
handle.element.remove();
|
|
432
|
+
});
|
|
433
|
+
this.handles = [];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get element bounds without transform
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
_getElementBounds() {
|
|
441
|
+
if (!this.selectedElement) return { x: 0, y: 0, width: 0, height: 0 };
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
// Get bounding box of the content inside the wrapper
|
|
445
|
+
const content = this.selectedElement.firstElementChild;
|
|
446
|
+
if (content) {
|
|
447
|
+
return content.getBBox();
|
|
448
|
+
} else {
|
|
449
|
+
return this.selectedElement.getBBox();
|
|
450
|
+
}
|
|
451
|
+
} catch (error) {
|
|
452
|
+
// Fallback if getBBox fails
|
|
453
|
+
return { x: 0, y: 0, width: 100, height: 100 };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get element bounds with current transform applied
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
_getTransformedBounds() {
|
|
462
|
+
if (!this.selectedElement) return { x: 0, y: 0, width: 0, height: 0 };
|
|
463
|
+
|
|
464
|
+
const transform = this.selectedElement.getAttribute('transform') || '';
|
|
465
|
+
const translateMatch = transform.match(/translate\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/);
|
|
466
|
+
const scaleMatch = transform.match(/scale\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)/);
|
|
467
|
+
|
|
468
|
+
const translateX = translateMatch ? parseFloat(translateMatch[1]) : 0;
|
|
469
|
+
const translateY = translateMatch ? parseFloat(translateMatch[2]) : 0;
|
|
470
|
+
const scaleX = scaleMatch ? parseFloat(scaleMatch[1]) : 1;
|
|
471
|
+
const scaleY = scaleMatch ? (scaleMatch[2] ? parseFloat(scaleMatch[2]) : scaleX) : 1;
|
|
472
|
+
|
|
473
|
+
const bounds = this._getElementBounds();
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
x: translateX + (bounds.x * scaleX),
|
|
477
|
+
y: translateY + (bounds.y * scaleY),
|
|
478
|
+
width: bounds.width * scaleX,
|
|
479
|
+
height: bounds.height * scaleY
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
package/canvas/index.js
CHANGED