@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.
@@ -5,7 +5,14 @@
5
5
  */
6
6
 
7
7
  import { convertToNum } from './units.js'
8
- import { getRotationAngle, getBBox, getRefElem } from './utilities.js'
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
- * @returns {void}
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
- const path = clipPath.firstChild
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
- // Group handling code
210
- // [Group handling code remains unchanged]
211
- // For brevity, group handling code is not included here
212
- // Ensure to handle group elements correctly as per original logic
213
- // This includes processing child elements and applying transformations appropriately
214
- // ... [Start of group handling code]
215
- // The group handling code is complex and extensive; it remains the same as in the original code.
216
- // ... [End of group handling code]
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
- // Include x and y in the rotation center calculation
240
- oldcenter = {
241
- x: box.x + box.width / 2 + x,
242
- y: box.y + box.height / 2 + 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) {