@svgedit/svgcanvas 7.2.6 → 7.4.1
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/CHANGES.md +6 -0
- package/common/browser.js +104 -37
- package/common/logger.js +151 -0
- package/common/util.js +96 -155
- package/core/blur-event.js +106 -42
- package/core/clear.js +13 -3
- package/core/coords.js +214 -90
- package/core/copy-elem.js +27 -13
- package/core/dataStorage.js +84 -21
- package/core/draw.js +80 -40
- package/core/elem-get-set.js +161 -77
- package/core/event.js +143 -28
- package/core/history.js +51 -31
- package/core/historyrecording.js +4 -2
- package/core/json.js +54 -12
- package/core/layer.js +11 -17
- package/core/math.js +102 -23
- package/core/namespaces.js +5 -5
- package/core/paint.js +100 -23
- package/core/paste-elem.js +58 -19
- package/core/path-actions.js +812 -791
- package/core/path-method.js +236 -37
- package/core/path.js +45 -10
- package/core/recalculate.js +438 -24
- package/core/sanitize.js +71 -34
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +103 -29
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +327 -306
- package/core/undo.js +20 -5
- package/core/units.js +8 -6
- package/core/utilities.js +316 -203
- package/dist/svgcanvas.js +31616 -53281
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +55 -54
- package/publish.md +1 -6
- package/svgcanvas.d.ts +225 -0
- package/svgcanvas.js +9 -9
- package/vite.config.mjs +20 -0
- package/rollup.config.mjs +0 -38
package/core/recalculate.js
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { convertToNum } from './units.js'
|
|
8
|
-
import {
|
|
8
|
+
import { NS } from './namespaces.js'
|
|
9
|
+
import {
|
|
10
|
+
getRotationAngle,
|
|
11
|
+
getBBox,
|
|
12
|
+
getHref,
|
|
13
|
+
getRefElem,
|
|
14
|
+
findDefs
|
|
15
|
+
} from './utilities.js'
|
|
9
16
|
import { BatchCommand, ChangeElementCommand } from './history.js'
|
|
10
17
|
import { remapElement } from './coords.js'
|
|
11
18
|
import {
|
|
@@ -36,20 +43,93 @@ export const init = canvas => {
|
|
|
36
43
|
* @param {string} attr - The clip-path attribute value containing the clipPath's ID
|
|
37
44
|
* @param {number} tx - The translation's x value
|
|
38
45
|
* @param {number} ty - The translation's y value
|
|
39
|
-
* @
|
|
46
|
+
* @param {Element} elem - The element referencing the clipPath
|
|
47
|
+
* @returns {string|undefined} The clip-path attribute used after updates.
|
|
40
48
|
*/
|
|
41
|
-
export const updateClipPath = (attr, tx, ty) => {
|
|
49
|
+
export const updateClipPath = (attr, tx, ty, elem) => {
|
|
42
50
|
const clipPath = getRefElem(attr)
|
|
43
|
-
if (!clipPath) return
|
|
44
|
-
|
|
51
|
+
if (!clipPath) return undefined
|
|
52
|
+
if (elem && clipPath.id) {
|
|
53
|
+
const svgContent = svgCanvas.getSvgContent?.()
|
|
54
|
+
if (svgContent) {
|
|
55
|
+
const refSelector = `[clip-path="url(#${clipPath.id})"]`
|
|
56
|
+
const users = svgContent.querySelectorAll(refSelector)
|
|
57
|
+
if (users.length > 1) {
|
|
58
|
+
const newClipPath = clipPath.cloneNode(true)
|
|
59
|
+
newClipPath.id = svgCanvas.getNextId()
|
|
60
|
+
findDefs().append(newClipPath)
|
|
61
|
+
elem.setAttribute('clip-path', `url(#${newClipPath.id})`)
|
|
62
|
+
return updateClipPath(`url(#${newClipPath.id})`, tx, ty)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const path = clipPath.firstElementChild
|
|
67
|
+
if (!path) return attr
|
|
45
68
|
const cpXform = getTransformList(path)
|
|
69
|
+
if (!cpXform) {
|
|
70
|
+
const tag = (path.tagName || '').toLowerCase()
|
|
71
|
+
if (tag === 'rect') {
|
|
72
|
+
const x = convertToNum('x', path.getAttribute('x') || 0) + tx
|
|
73
|
+
const y = convertToNum('y', path.getAttribute('y') || 0) + ty
|
|
74
|
+
path.setAttribute('x', x)
|
|
75
|
+
path.setAttribute('y', y)
|
|
76
|
+
} else if (tag === 'circle' || tag === 'ellipse') {
|
|
77
|
+
const cx = convertToNum('cx', path.getAttribute('cx') || 0) + tx
|
|
78
|
+
const cy = convertToNum('cy', path.getAttribute('cy') || 0) + ty
|
|
79
|
+
path.setAttribute('cx', cx)
|
|
80
|
+
path.setAttribute('cy', cy)
|
|
81
|
+
} else if (tag === 'line') {
|
|
82
|
+
path.setAttribute('x1', convertToNum('x1', path.getAttribute('x1') || 0) + tx)
|
|
83
|
+
path.setAttribute('y1', convertToNum('y1', path.getAttribute('y1') || 0) + ty)
|
|
84
|
+
path.setAttribute('x2', convertToNum('x2', path.getAttribute('x2') || 0) + tx)
|
|
85
|
+
path.setAttribute('y2', convertToNum('y2', path.getAttribute('y2') || 0) + ty)
|
|
86
|
+
} else if (tag === 'polyline' || tag === 'polygon') {
|
|
87
|
+
const points = (path.getAttribute('points') || '').trim()
|
|
88
|
+
if (points) {
|
|
89
|
+
const updated = points.split(/\s+/).map((pair) => {
|
|
90
|
+
const [x, y] = pair.split(',')
|
|
91
|
+
const nx = Number(x) + tx
|
|
92
|
+
const ny = Number(y) + ty
|
|
93
|
+
return `${nx},${ny}`
|
|
94
|
+
})
|
|
95
|
+
path.setAttribute('points', updated.join(' '))
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
path.setAttribute('transform', `translate(${tx},${ty})`)
|
|
99
|
+
}
|
|
100
|
+
return attr
|
|
101
|
+
}
|
|
102
|
+
if (cpXform.numberOfItems) {
|
|
103
|
+
const translate = svgCanvas.getSvgRoot().createSVGMatrix()
|
|
104
|
+
translate.e = tx
|
|
105
|
+
translate.f = ty
|
|
106
|
+
const combined = matrixMultiply(transformListToTransform(cpXform).matrix, translate)
|
|
107
|
+
const merged = svgCanvas.getSvgRoot().createSVGTransform()
|
|
108
|
+
merged.setMatrix(combined)
|
|
109
|
+
cpXform.clear()
|
|
110
|
+
cpXform.appendItem(merged)
|
|
111
|
+
return attr
|
|
112
|
+
}
|
|
113
|
+
const tag = (path.tagName || '').toLowerCase()
|
|
114
|
+
if ((tag === 'polyline' || tag === 'polygon') && !path.points?.numberOfItems) {
|
|
115
|
+
const points = (path.getAttribute('points') || '').trim()
|
|
116
|
+
if (points) {
|
|
117
|
+
const updated = points.split(/\s+/).map((pair) => {
|
|
118
|
+
const [x, y] = pair.split(',')
|
|
119
|
+
const nx = Number(x) + tx
|
|
120
|
+
const ny = Number(y) + ty
|
|
121
|
+
return `${nx},${ny}`
|
|
122
|
+
})
|
|
123
|
+
path.setAttribute('points', updated.join(' '))
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
46
127
|
const newTranslate = svgCanvas.getSvgRoot().createSVGTransform()
|
|
47
128
|
newTranslate.setTranslate(tx, ty)
|
|
48
129
|
|
|
49
130
|
cpXform.appendItem(newTranslate)
|
|
50
|
-
|
|
51
|
-
// Update clipPath's dimensions
|
|
52
131
|
recalculateDimensions(path)
|
|
132
|
+
return attr
|
|
53
133
|
}
|
|
54
134
|
|
|
55
135
|
/**
|
|
@@ -60,6 +140,20 @@ export const updateClipPath = (attr, tx, ty) => {
|
|
|
60
140
|
*/
|
|
61
141
|
export const recalculateDimensions = selected => {
|
|
62
142
|
if (!selected) return null
|
|
143
|
+
|
|
144
|
+
// Don't recalculate dimensions for groups - this would push their transforms down to children
|
|
145
|
+
// Groups should maintain their transform attribute on the group element itself
|
|
146
|
+
if (selected.tagName === 'g' || selected.tagName === 'a') {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
(selected.getAttribute?.('clip-path')) &&
|
|
152
|
+
selected.querySelector?.('[clip-path]')
|
|
153
|
+
) {
|
|
154
|
+
// Keep transforms when clip-paths are present to avoid mutating defs.
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
63
157
|
const svgroot = svgCanvas.getSvgRoot()
|
|
64
158
|
const dataStorage = svgCanvas.getDataStorage()
|
|
65
159
|
const tlist = getTransformList(selected)
|
|
@@ -105,6 +199,11 @@ export const recalculateDimensions = selected => {
|
|
|
105
199
|
return null
|
|
106
200
|
}
|
|
107
201
|
|
|
202
|
+
// Avoid remapping transforms on <use> to preserve referenced positioning/rotation
|
|
203
|
+
if (selected.tagName === 'use') {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
108
207
|
// Set up undo command
|
|
109
208
|
const batchCmd = new BatchCommand('Transform')
|
|
110
209
|
|
|
@@ -206,14 +305,310 @@ export const recalculateDimensions = selected => {
|
|
|
206
305
|
|
|
207
306
|
// Handle group elements ('g' or 'a')
|
|
208
307
|
if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
308
|
+
const box = getBBox(selected)
|
|
309
|
+
|
|
310
|
+
oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
|
|
311
|
+
newcenter = transformPoint(
|
|
312
|
+
box.x + box.width / 2,
|
|
313
|
+
box.y + box.height / 2,
|
|
314
|
+
transformListToTransform(tlist).matrix
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
const gangle = getRotationAngle(selected)
|
|
318
|
+
if (gangle) {
|
|
319
|
+
const a = gangle * Math.PI / 180
|
|
320
|
+
const s = Math.abs(a) > (1.0e-10) ? Math.sin(a) / (1 - Math.cos(a)) : 2 / a
|
|
321
|
+
for (let i = 0; i < tlist.numberOfItems; ++i) {
|
|
322
|
+
const xform = tlist.getItem(i)
|
|
323
|
+
if (xform.type === SVGTransform.SVG_TRANSFORM_ROTATE) {
|
|
324
|
+
const rm = xform.matrix
|
|
325
|
+
oldcenter.y = (s * rm.e + rm.f) / 2
|
|
326
|
+
oldcenter.x = (rm.e - s * rm.f) / 2
|
|
327
|
+
tlist.removeItem(i)
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const N = tlist.numberOfItems
|
|
334
|
+
let tx = 0
|
|
335
|
+
let ty = 0
|
|
336
|
+
let operation = 0
|
|
337
|
+
|
|
338
|
+
let firstM
|
|
339
|
+
if (N) {
|
|
340
|
+
firstM = tlist.getItem(0).matrix
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let oldStartTransform
|
|
344
|
+
if (
|
|
345
|
+
N >= 3 &&
|
|
346
|
+
tlist.getItem(N - 2).type === SVGTransform.SVG_TRANSFORM_SCALE &&
|
|
347
|
+
tlist.getItem(N - 3).type === SVGTransform.SVG_TRANSFORM_TRANSLATE &&
|
|
348
|
+
tlist.getItem(N - 1).type === SVGTransform.SVG_TRANSFORM_TRANSLATE
|
|
349
|
+
) {
|
|
350
|
+
operation = 3 // scale
|
|
351
|
+
|
|
352
|
+
const tm = tlist.getItem(N - 3).matrix
|
|
353
|
+
const sm = tlist.getItem(N - 2).matrix
|
|
354
|
+
const tmn = tlist.getItem(N - 1).matrix
|
|
355
|
+
|
|
356
|
+
const children = selected.childNodes
|
|
357
|
+
let c = children.length
|
|
358
|
+
while (c--) {
|
|
359
|
+
const child = children.item(c)
|
|
360
|
+
if (child.nodeType !== 1) continue
|
|
361
|
+
|
|
362
|
+
const childTlist = getTransformList(child)
|
|
363
|
+
if (!childTlist) continue
|
|
364
|
+
|
|
365
|
+
const m = transformListToTransform(childTlist).matrix
|
|
366
|
+
|
|
367
|
+
const angle = getRotationAngle(child)
|
|
368
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
369
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
370
|
+
|
|
371
|
+
if (angle || hasMatrixTransform(childTlist)) {
|
|
372
|
+
const e2t = svgroot.createSVGTransform()
|
|
373
|
+
e2t.setMatrix(matrixMultiply(tm, sm, tmn, m))
|
|
374
|
+
childTlist.clear()
|
|
375
|
+
childTlist.appendItem(e2t)
|
|
376
|
+
} else {
|
|
377
|
+
const t2n = matrixMultiply(m.inverse(), tmn, m)
|
|
378
|
+
const t2 = svgroot.createSVGMatrix()
|
|
379
|
+
t2.e = -t2n.e
|
|
380
|
+
t2.f = -t2n.f
|
|
381
|
+
|
|
382
|
+
const s2 = matrixMultiply(
|
|
383
|
+
t2.inverse(),
|
|
384
|
+
m.inverse(),
|
|
385
|
+
tm,
|
|
386
|
+
sm,
|
|
387
|
+
tmn,
|
|
388
|
+
m,
|
|
389
|
+
t2n.inverse()
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
const translateOrigin = svgroot.createSVGTransform()
|
|
393
|
+
const scale = svgroot.createSVGTransform()
|
|
394
|
+
const translateBack = svgroot.createSVGTransform()
|
|
395
|
+
translateOrigin.setTranslate(t2n.e, t2n.f)
|
|
396
|
+
scale.setScale(s2.a, s2.d)
|
|
397
|
+
translateBack.setTranslate(t2.e, t2.f)
|
|
398
|
+
childTlist.appendItem(translateBack)
|
|
399
|
+
childTlist.appendItem(scale)
|
|
400
|
+
childTlist.appendItem(translateOrigin)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
404
|
+
if (recalculatedDimensions) {
|
|
405
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
406
|
+
}
|
|
407
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
tlist.removeItem(N - 1)
|
|
411
|
+
tlist.removeItem(N - 2)
|
|
412
|
+
tlist.removeItem(N - 3)
|
|
413
|
+
} else if (N >= 3 && tlist.getItem(N - 1).type === SVGTransform.SVG_TRANSFORM_MATRIX) {
|
|
414
|
+
operation = 3 // scale (matrix imposition)
|
|
415
|
+
const m = transformListToTransform(tlist).matrix
|
|
416
|
+
const e2t = svgroot.createSVGTransform()
|
|
417
|
+
e2t.setMatrix(m)
|
|
418
|
+
tlist.clear()
|
|
419
|
+
tlist.appendItem(e2t)
|
|
420
|
+
} else if (
|
|
421
|
+
(N === 1 ||
|
|
422
|
+
(N > 1 && tlist.getItem(1).type !== SVGTransform.SVG_TRANSFORM_SCALE)) &&
|
|
423
|
+
tlist.getItem(0).type === SVGTransform.SVG_TRANSFORM_TRANSLATE
|
|
424
|
+
) {
|
|
425
|
+
operation = 2 // translate
|
|
426
|
+
const tM = transformListToTransform(tlist).matrix
|
|
427
|
+
tlist.removeItem(0)
|
|
428
|
+
const mInv = transformListToTransform(tlist).matrix.inverse()
|
|
429
|
+
const m2 = matrixMultiply(mInv, tM)
|
|
430
|
+
|
|
431
|
+
tx = m2.e
|
|
432
|
+
ty = m2.f
|
|
433
|
+
|
|
434
|
+
if (tx !== 0 || ty !== 0) {
|
|
435
|
+
const selectedClipPath = selected.getAttribute?.('clip-path')
|
|
436
|
+
if (selectedClipPath) {
|
|
437
|
+
updateClipPath(selectedClipPath, tx, ty, selected)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const children = selected.childNodes
|
|
441
|
+
let c = children.length
|
|
442
|
+
|
|
443
|
+
const clipPathsDone = []
|
|
444
|
+
while (c--) {
|
|
445
|
+
const child = children.item(c)
|
|
446
|
+
if (child.nodeType !== 1) continue
|
|
447
|
+
|
|
448
|
+
const clipPathAttr = child.getAttribute('clip-path')
|
|
449
|
+
if (clipPathAttr && !clipPathsDone.includes(clipPathAttr)) {
|
|
450
|
+
const updatedAttr = updateClipPath(clipPathAttr, tx, ty, child)
|
|
451
|
+
clipPathsDone.push(updatedAttr || clipPathAttr)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const childTlist = getTransformList(child)
|
|
455
|
+
if (!childTlist) continue
|
|
456
|
+
|
|
457
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
458
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
459
|
+
|
|
460
|
+
const newxlate = svgroot.createSVGTransform()
|
|
461
|
+
newxlate.setTranslate(tx, ty)
|
|
462
|
+
if (childTlist.numberOfItems) {
|
|
463
|
+
childTlist.insertItemBefore(newxlate, 0)
|
|
464
|
+
} else {
|
|
465
|
+
childTlist.appendItem(newxlate)
|
|
466
|
+
}
|
|
467
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
468
|
+
if (recalculatedDimensions) {
|
|
469
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const uses = selected.getElementsByTagNameNS(NS.SVG, 'use')
|
|
473
|
+
const href = `#${child.id}`
|
|
474
|
+
let u = uses.length
|
|
475
|
+
while (u--) {
|
|
476
|
+
const useElem = uses.item(u)
|
|
477
|
+
if (href === getHref(useElem)) {
|
|
478
|
+
const usexlate = svgroot.createSVGTransform()
|
|
479
|
+
usexlate.setTranslate(-tx, -ty)
|
|
480
|
+
const useTlist = getTransformList(useElem)
|
|
481
|
+
useTlist?.insertItemBefore(usexlate, 0)
|
|
482
|
+
const useRecalc = recalculateDimensions(useElem)
|
|
483
|
+
if (useRecalc) {
|
|
484
|
+
batchCmd.addSubCommand(useRecalc)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else if (
|
|
493
|
+
N === 1 &&
|
|
494
|
+
tlist.getItem(0).type === SVGTransform.SVG_TRANSFORM_MATRIX &&
|
|
495
|
+
!gangle
|
|
496
|
+
) {
|
|
497
|
+
operation = 1
|
|
498
|
+
const m = tlist.getItem(0).matrix
|
|
499
|
+
const children = selected.childNodes
|
|
500
|
+
let c = children.length
|
|
501
|
+
while (c--) {
|
|
502
|
+
const child = children.item(c)
|
|
503
|
+
if (child.nodeType !== 1) continue
|
|
504
|
+
|
|
505
|
+
const childTlist = getTransformList(child)
|
|
506
|
+
if (!childTlist) continue
|
|
507
|
+
|
|
508
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
509
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
510
|
+
|
|
511
|
+
const em = matrixMultiply(m, transformListToTransform(childTlist).matrix)
|
|
512
|
+
const e2m = svgroot.createSVGTransform()
|
|
513
|
+
e2m.setMatrix(em)
|
|
514
|
+
childTlist.clear()
|
|
515
|
+
childTlist.appendItem(e2m)
|
|
516
|
+
|
|
517
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
518
|
+
if (recalculatedDimensions) {
|
|
519
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
520
|
+
}
|
|
521
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
522
|
+
|
|
523
|
+
const sw = child.getAttribute('stroke-width')
|
|
524
|
+
if (child.getAttribute('stroke') !== 'none' && !Number.isNaN(Number(sw))) {
|
|
525
|
+
const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2
|
|
526
|
+
child.setAttribute('stroke-width', Number(sw) * avg)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
tlist.clear()
|
|
530
|
+
} else {
|
|
531
|
+
if (gangle) {
|
|
532
|
+
const newRot = svgroot.createSVGTransform()
|
|
533
|
+
newRot.setRotate(gangle, newcenter.x, newcenter.y)
|
|
534
|
+
if (tlist.numberOfItems) {
|
|
535
|
+
tlist.insertItemBefore(newRot, 0)
|
|
536
|
+
} else {
|
|
537
|
+
tlist.appendItem(newRot)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (tlist.numberOfItems === 0) {
|
|
541
|
+
selected.removeAttribute('transform')
|
|
542
|
+
}
|
|
543
|
+
return null
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (operation === 2) {
|
|
547
|
+
if (gangle) {
|
|
548
|
+
newcenter = {
|
|
549
|
+
x: oldcenter.x + firstM.e,
|
|
550
|
+
y: oldcenter.y + firstM.f
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const newRot = svgroot.createSVGTransform()
|
|
554
|
+
newRot.setRotate(gangle, newcenter.x, newcenter.y)
|
|
555
|
+
if (tlist.numberOfItems) {
|
|
556
|
+
tlist.insertItemBefore(newRot, 0)
|
|
557
|
+
} else {
|
|
558
|
+
tlist.appendItem(newRot)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
} else if (operation === 3) {
|
|
562
|
+
const m = transformListToTransform(tlist).matrix
|
|
563
|
+
const roldt = svgroot.createSVGTransform()
|
|
564
|
+
roldt.setRotate(gangle, oldcenter.x, oldcenter.y)
|
|
565
|
+
const rold = roldt.matrix
|
|
566
|
+
const rnew = svgroot.createSVGTransform()
|
|
567
|
+
rnew.setRotate(gangle, newcenter.x, newcenter.y)
|
|
568
|
+
const rnewInv = rnew.matrix.inverse()
|
|
569
|
+
const mInv = m.inverse()
|
|
570
|
+
const extrat = matrixMultiply(mInv, rnewInv, rold, m)
|
|
571
|
+
|
|
572
|
+
tx = extrat.e
|
|
573
|
+
ty = extrat.f
|
|
574
|
+
|
|
575
|
+
if (tx !== 0 || ty !== 0) {
|
|
576
|
+
const children = selected.childNodes
|
|
577
|
+
let c = children.length
|
|
578
|
+
while (c--) {
|
|
579
|
+
const child = children.item(c)
|
|
580
|
+
if (child.nodeType !== 1) continue
|
|
581
|
+
|
|
582
|
+
const childTlist = getTransformList(child)
|
|
583
|
+
if (!childTlist) continue
|
|
584
|
+
|
|
585
|
+
oldStartTransform = svgCanvas.getStartTransform()
|
|
586
|
+
svgCanvas.setStartTransform(child.getAttribute('transform'))
|
|
587
|
+
|
|
588
|
+
const newxlate = svgroot.createSVGTransform()
|
|
589
|
+
newxlate.setTranslate(tx, ty)
|
|
590
|
+
if (childTlist.numberOfItems) {
|
|
591
|
+
childTlist.insertItemBefore(newxlate, 0)
|
|
592
|
+
} else {
|
|
593
|
+
childTlist.appendItem(newxlate)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const recalculatedDimensions = recalculateDimensions(child)
|
|
597
|
+
if (recalculatedDimensions) {
|
|
598
|
+
batchCmd.addSubCommand(recalculatedDimensions)
|
|
599
|
+
}
|
|
600
|
+
svgCanvas.setStartTransform(oldStartTransform)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (gangle) {
|
|
605
|
+
if (tlist.numberOfItems) {
|
|
606
|
+
tlist.insertItemBefore(rnew, 0)
|
|
607
|
+
} else {
|
|
608
|
+
tlist.appendItem(rnew)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
217
612
|
} else {
|
|
218
613
|
// Non-group elements
|
|
219
614
|
|
|
@@ -236,16 +631,35 @@ export const recalculateDimensions = selected => {
|
|
|
236
631
|
// Handle rotation transformations
|
|
237
632
|
const angle = getRotationAngle(selected)
|
|
238
633
|
if (angle) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
x
|
|
242
|
-
y
|
|
634
|
+
if (selected.localName === 'image') {
|
|
635
|
+
// Use the center of the image as the rotation center
|
|
636
|
+
const xAttr = convertToNum('x', selected.getAttribute('x') || '0')
|
|
637
|
+
const yAttr = convertToNum('y', selected.getAttribute('y') || '0')
|
|
638
|
+
const width = convertToNum('width', selected.getAttribute('width') || '0')
|
|
639
|
+
const height = convertToNum('height', selected.getAttribute('height') || '0')
|
|
640
|
+
const cx = xAttr + width / 2
|
|
641
|
+
const cy = yAttr + height / 2
|
|
642
|
+
oldcenter = { x: cx, y: cy }
|
|
643
|
+
const transform = transformListToTransform(tlist).matrix
|
|
644
|
+
newcenter = transformPoint(cx, cy, transform)
|
|
645
|
+
} else if (selected.localName === 'text') {
|
|
646
|
+
// Use the center of the bounding box as the rotation center for text
|
|
647
|
+
const cx = box.x + box.width / 2
|
|
648
|
+
const cy = box.y + box.height / 2
|
|
649
|
+
oldcenter = { x: cx, y: cy }
|
|
650
|
+
newcenter = transformPoint(cx, cy, transformListToTransform(tlist).matrix)
|
|
651
|
+
} else {
|
|
652
|
+
// Include x and y in the rotation center calculation for other elements
|
|
653
|
+
oldcenter = {
|
|
654
|
+
x: box.x + box.width / 2 + x,
|
|
655
|
+
y: box.y + box.height / 2 + y
|
|
656
|
+
}
|
|
657
|
+
newcenter = transformPoint(
|
|
658
|
+
box.x + box.width / 2 + x,
|
|
659
|
+
box.y + box.height / 2 + y,
|
|
660
|
+
transformListToTransform(tlist).matrix
|
|
661
|
+
)
|
|
243
662
|
}
|
|
244
|
-
newcenter = transformPoint(
|
|
245
|
-
box.x + box.width / 2 + x,
|
|
246
|
-
box.y + box.height / 2 + y,
|
|
247
|
-
transformListToTransform(tlist).matrix
|
|
248
|
-
)
|
|
249
663
|
|
|
250
664
|
// Remove the rotation transform from the list
|
|
251
665
|
for (let i = 0; i < tlist.numberOfItems; ++i) {
|