@svgedit/svgcanvas 7.2.7 → 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/core/select.js CHANGED
@@ -10,12 +10,37 @@ import { isWebkit } from '../common/browser.js'
10
10
  import { getRotationAngle, getBBox, getStrokedBBox } from './utilities.js'
11
11
  import { transformListToTransform, transformBox, transformPoint, matrixMultiply, getTransformList } from './math.js'
12
12
  import { NS } from './namespaces'
13
+ import { warn } from '../common/logger.js'
13
14
 
14
15
  let svgCanvas
15
- let selectorManager_ // A Singleton
16
16
  // change radius if touch screen
17
17
  const gripRadius = window.ontouchstart ? 10 : 4
18
18
 
19
+ /**
20
+ * Private singleton manager for selector state
21
+ */
22
+ class SelectModule {
23
+ #selectorManager = null
24
+
25
+ /**
26
+ * Initialize the select module with canvas
27
+ * @param {Object} canvas - The SVG canvas instance
28
+ * @returns {void}
29
+ */
30
+ init (canvas) {
31
+ svgCanvas = canvas
32
+ this.#selectorManager = new SelectorManager()
33
+ }
34
+
35
+ /**
36
+ * Get the singleton SelectorManager instance
37
+ * @returns {SelectorManager} The SelectorManager instance
38
+ */
39
+ getSelectorManager () {
40
+ return this.#selectorManager
41
+ }
42
+ }
43
+
19
44
  /**
20
45
  * Private class for DOM element selection boxes.
21
46
  */
@@ -38,14 +63,14 @@ export class Selector {
38
63
  // this holds a reference to the <g> element that holds all visual elements of the selector
39
64
  this.selectorGroup = svgCanvas.createSVGElement({
40
65
  element: 'g',
41
- attr: { id: ('selectorGroup' + this.id) }
66
+ attr: { id: `selectorGroup${this.id}` }
42
67
  })
43
68
 
44
69
  // this holds a reference to the path rect
45
70
  this.selectorRect = svgCanvas.createSVGElement({
46
71
  element: 'path',
47
72
  attr: {
48
- id: ('selectedBox' + this.id),
73
+ id: `selectedBox${this.id}`,
49
74
  fill: 'none',
50
75
  stroke: '#22C',
51
76
  'stroke-width': '1',
@@ -91,11 +116,11 @@ export class Selector {
91
116
  */
92
117
  showGrips (show) {
93
118
  const bShow = show ? 'inline' : 'none'
94
- selectorManager_.selectorGripsGroup.setAttribute('display', bShow)
119
+ selectModule.getSelectorManager().selectorGripsGroup.setAttribute('display', bShow)
95
120
  const elem = this.selectedElement
96
121
  this.hasGrips = show
97
122
  if (elem && show) {
98
- this.selectorGroup.append(selectorManager_.selectorGripsGroup)
123
+ this.selectorGroup.append(selectModule.getSelectorManager().selectorGripsGroup)
99
124
  Selector.updateGripCursors(getRotationAngle(elem))
100
125
  }
101
126
  }
@@ -108,7 +133,7 @@ export class Selector {
108
133
  resize (bbox) {
109
134
  const dataStorage = svgCanvas.getDataStorage()
110
135
  const selectedBox = this.selectorRect
111
- const mgr = selectorManager_
136
+ const mgr = selectModule.getSelectorManager()
112
137
  const selectedGrips = mgr.selectorGrips
113
138
  const selected = this.selectedElement
114
139
  const zoom = svgCanvas.getZoom()
@@ -130,7 +155,7 @@ export class Selector {
130
155
  while (currentElt.parentNode) {
131
156
  if (currentElt.parentNode && currentElt.parentNode.tagName === 'g' && currentElt.parentNode.transform) {
132
157
  if (currentElt.parentNode.transform.baseVal.numberOfItems) {
133
- parentTransformationMatrix = matrixMultiply(transformListToTransform(getTransformList(selected.parentNode)).matrix, parentTransformationMatrix)
158
+ parentTransformationMatrix = matrixMultiply(transformListToTransform(getTransformList(currentElt.parentNode)).matrix, parentTransformationMatrix)
134
159
  }
135
160
  }
136
161
  currentElt = currentElt.parentNode
@@ -213,10 +238,7 @@ export class Selector {
213
238
  nbah = (maxy - miny)
214
239
  }
215
240
 
216
- const dstr = 'M' + nbax + ',' + nbay +
217
- ' L' + (nbax + nbaw) + ',' + nbay +
218
- ' ' + (nbax + nbaw) + ',' + (nbay + nbah) +
219
- ' ' + nbax + ',' + (nbay + nbah) + 'z'
241
+ const dstr = `M${nbax},${nbay} L${nbax + nbaw},${nbay} ${nbax + nbaw},${nbay + nbah} ${nbax},${nbay + nbah}z`
220
242
 
221
243
  const xform = angle ? 'rotate(' + [angle, cx, cy].join(',') + ')' : ''
222
244
 
@@ -257,15 +279,15 @@ export class Selector {
257
279
  * @returns {void}
258
280
  */
259
281
  static updateGripCursors (angle) {
260
- const dirArr = Object.keys(selectorManager_.selectorGrips)
282
+ const dirArr = Object.keys(selectModule.getSelectorManager().selectorGrips)
261
283
  let steps = Math.round(angle / 45)
262
284
  if (steps < 0) { steps += 8 }
263
285
  while (steps > 0) {
264
286
  dirArr.push(dirArr.shift())
265
287
  steps--
266
288
  }
267
- Object.values(selectorManager_.selectorGrips).forEach((gripElement, i) => {
268
- gripElement.setAttribute('style', ('cursor:' + dirArr[i] + '-resize'))
289
+ Object.values(selectModule.getSelectorManager().selectorGrips).forEach((gripElement, i) => {
290
+ gripElement.setAttribute('style', `cursor:${dirArr[i]}-resize`)
269
291
  })
270
292
  }
271
293
  }
@@ -341,10 +363,10 @@ export class SelectorManager {
341
363
  const grip = svgCanvas.createSVGElement({
342
364
  element: 'circle',
343
365
  attr: {
344
- id: ('selectorGrip_resize_' + dir),
366
+ id: `selectorGrip_resize_${dir}`,
345
367
  fill: '#22C',
346
368
  r: gripRadius,
347
- style: ('cursor:' + dir + '-resize'),
369
+ style: `cursor:${dir}-resize`,
348
370
  // This expands the mouse-able area of the grips making them
349
371
  // easier to grab with the mouse.
350
372
  // This works in Opera and WebKit, but does not work in Firefox
@@ -462,7 +484,7 @@ export class SelectorManager {
462
484
  const sel = this.selectorMap[elem.id]
463
485
  if (!sel?.locked) {
464
486
  // TODO(codedread): Ensure this exists in this module.
465
- console.warn('WARNING! selector was released but was already unlocked')
487
+ warn('WARNING! selector was released but was already unlocked', null, 'select')
466
488
  }
467
489
  for (let i = 0; i < N; ++i) {
468
490
  if (this.selectors[i] && this.selectors[i] === sel) {
@@ -541,6 +563,9 @@ export class SelectorManager {
541
563
  * @property {module:select.Dimensions} dimensions
542
564
  */
543
565
 
566
+ // Export singleton instance for backward compatibility
567
+ const selectModule = new SelectModule()
568
+
544
569
  /**
545
570
  * Initializes this module.
546
571
  * @function module:select.init
@@ -549,12 +574,11 @@ export class SelectorManager {
549
574
  * @returns {void}
550
575
  */
551
576
  export const init = (canvas) => {
552
- svgCanvas = canvas
553
- selectorManager_ = new SelectorManager()
577
+ selectModule.init(canvas)
554
578
  }
555
579
 
556
580
  /**
557
581
  * @function module:select.getSelectorManager
558
582
  * @returns {module:select.SelectorManager} The SelectorManager instance.
559
583
  */
560
- export const getSelectorManager = () => selectorManager_
584
+ export const getSelectorManager = () => selectModule.getSelectorManager()
@@ -9,6 +9,7 @@
9
9
  import { NS } from './namespaces.js'
10
10
  import * as hstry from './history.js'
11
11
  import * as pathModule from './path.js'
12
+ import { warn, error } from '../common/logger.js'
12
13
  import {
13
14
  getStrokedBBoxDefaultVisible,
14
15
  setHref,
@@ -63,6 +64,7 @@ export const init = canvas => {
63
64
  svgCanvas.updateCanvas = updateCanvas // Updates the editor canvas width/height/position after a zoom has occurred.
64
65
  svgCanvas.cycleElement = cycleElement // Select the next/previous element within the current layer.
65
66
  svgCanvas.deleteSelectedElements = deleteSelectedElements // Removes all selected elements from the DOM and adds the change to the history
67
+ svgCanvas.flipSelectedElements = flipSelectedElements // Flips selected elements horizontally or vertically
66
68
  }
67
69
 
68
70
  /**
@@ -103,14 +105,17 @@ const moveToBottomSelectedElem = () => {
103
105
  let t = selected
104
106
  const oldParent = t.parentNode
105
107
  const oldNextSibling = t.nextSibling
106
- let { firstChild } = t.parentNode
107
- if (firstChild.tagName === 'title') {
108
- firstChild = firstChild.nextSibling
108
+ let firstChild = t.parentNode.firstElementChild
109
+ if (firstChild?.tagName === 'title') {
110
+ firstChild = firstChild.nextElementSibling
109
111
  }
110
112
  // This can probably be removed, as the defs should not ever apppear
111
113
  // inside a layer group
112
- if (firstChild.tagName === 'defs') {
113
- firstChild = firstChild.nextSibling
114
+ if (firstChild?.tagName === 'defs') {
115
+ firstChild = firstChild.nextElementSibling
116
+ }
117
+ if (!firstChild) {
118
+ return
114
119
  }
115
120
  t = t.parentNode.insertBefore(t, firstChild)
116
121
  // If the element actually moved position, add the command and fire the changed
@@ -178,7 +183,7 @@ const moveUpDownSelected = dir => {
178
183
  // event handler.
179
184
  if (oldNextSibling !== t.nextSibling) {
180
185
  svgCanvas.addCommandToHistory(
181
- new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir)
186
+ new MoveElementCommand(t, oldNextSibling, oldParent, `Move ${dir}`)
182
187
  )
183
188
  svgCanvas.call('changed', [t])
184
189
  }
@@ -207,6 +212,9 @@ const moveSelectedElements = (dx, dy, undoable = true) => {
207
212
  const batchCmd = new BatchCommand('position')
208
213
  selectedElements.forEach((selected, i) => {
209
214
  if (selected) {
215
+ // Store the existing transform before modifying
216
+ const existingTransform = selected.getAttribute('transform') || ''
217
+
210
218
  const xform = svgCanvas.getSvgRoot().createSVGTransform()
211
219
  const tlist = getTransformList(selected)
212
220
 
@@ -226,6 +234,12 @@ const moveSelectedElements = (dx, dy, undoable = true) => {
226
234
  const cmd = recalculateDimensions(selected)
227
235
  if (cmd) {
228
236
  batchCmd.addSubCommand(cmd)
237
+ } else if ((selected.getAttribute('transform') || '') !== existingTransform) {
238
+ // For groups and other elements where recalculateDimensions returns null,
239
+ // record the transform change directly
240
+ batchCmd.addSubCommand(
241
+ new ChangeElementCommand(selected, { transform: existingTransform })
242
+ )
229
243
  }
230
244
 
231
245
  svgCanvas
@@ -264,9 +278,11 @@ const cloneSelectedElements = (x, y) => {
264
278
  const index = el => {
265
279
  if (!el) return -1
266
280
  let i = 0
281
+ let current = el
267
282
  do {
268
283
  i++
269
- } while (el === el.previousElementSibling)
284
+ current = current.previousElementSibling
285
+ } while (current)
270
286
  return i
271
287
  }
272
288
 
@@ -621,13 +637,87 @@ const deleteSelectedElements = () => {
621
637
  svgCanvas.clearSelection()
622
638
  }
623
639
 
640
+ /**
641
+ * Flips selected elements horizontally or vertically by transforming actual coordinates.
642
+ * @function module:selected-elem.SvgCanvas#flipSelectedElements
643
+ * @param {number} scaleX - Scale factor for X axis (-1 for horizontal flip, 1 for no flip)
644
+ * @param {number} scaleY - Scale factor for Y axis (1 for no flip, -1 for vertical flip)
645
+ * @fires module:selected-elem.SvgCanvas#event:changed
646
+ * @returns {void}
647
+ */
648
+ const flipSelectedElements = (scaleX, scaleY) => {
649
+ const selectedElements = svgCanvas.getSelectedElements()
650
+ const batchCmd = new BatchCommand('Flip Elements')
651
+ const svgRoot = svgCanvas.getSvgRoot()
652
+
653
+ selectedElements.forEach(selected => {
654
+ if (!selected) return
655
+
656
+ const bbox = getStrokedBBoxDefaultVisible([selected])
657
+ if (!bbox) return
658
+
659
+ const cx = bbox.x + bbox.width / 2
660
+ const cy = bbox.y + bbox.height / 2
661
+ const existingTransform = selected.getAttribute('transform') || ''
662
+
663
+ const flipMatrix = svgRoot
664
+ .createSVGMatrix()
665
+ .translate(cx, cy)
666
+ .scaleNonUniform(scaleX, scaleY)
667
+ .translate(-cx, -cy)
668
+
669
+ const tlist = getTransformList(selected)
670
+ const combinedMatrix = matrixMultiply(
671
+ transformListToTransform(tlist).matrix,
672
+ flipMatrix
673
+ )
674
+
675
+ const flipTransform = svgRoot.createSVGTransform()
676
+ flipTransform.setMatrix(combinedMatrix)
677
+
678
+ tlist.clear()
679
+ tlist.appendItem(flipTransform)
680
+
681
+ const prevStartTransform = svgCanvas.getStartTransform
682
+ ? svgCanvas.getStartTransform()
683
+ : null
684
+ if (svgCanvas.setStartTransform) {
685
+ svgCanvas.setStartTransform(existingTransform)
686
+ }
687
+
688
+ const cmd = recalculateDimensions(selected)
689
+
690
+ if (svgCanvas.setStartTransform) {
691
+ svgCanvas.setStartTransform(prevStartTransform)
692
+ }
693
+
694
+ if (cmd) {
695
+ batchCmd.addSubCommand(cmd)
696
+ } else if ((selected.getAttribute('transform') || '') !== existingTransform) {
697
+ batchCmd.addSubCommand(
698
+ new ChangeElementCommand(selected, { transform: existingTransform })
699
+ )
700
+ }
701
+
702
+ svgCanvas
703
+ .gettingSelectorManager()
704
+ .requestSelector(selected)
705
+ .resize()
706
+ })
707
+
708
+ if (!batchCmd.isEmpty()) {
709
+ svgCanvas.addCommandToHistory(batchCmd)
710
+ svgCanvas.call('changed', selectedElements.filter(Boolean))
711
+ }
712
+ }
713
+
624
714
  /**
625
715
  * Remembers the current selected elements on the clipboard.
626
716
  * @function module:selected-elem.SvgCanvas#copySelectedElements
627
717
  * @returns {void}
628
718
  */
629
719
  const copySelectedElements = () => {
630
- const selectedElements = svgCanvas.getSelectedElements()
720
+ const selectedElements = svgCanvas.getSelectedElements().filter(Boolean)
631
721
  const data = JSON.stringify(
632
722
  selectedElements.map(x => svgCanvas.getJsonFromSvgElements(x))
633
723
  )
@@ -637,7 +727,7 @@ const copySelectedElements = () => {
637
727
 
638
728
  // Context menu might not exist (it is provided by editor.js).
639
729
  const canvMenu = document.getElementById('se-cmenu_canvas')
640
- canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place')
730
+ canvMenu?.setAttribute('enablemenuitems', '#paste,#paste_in_place')
641
731
  }
642
732
 
643
733
  /**
@@ -791,10 +881,10 @@ const pushGroupProperty = (g, undoable) => {
791
881
  // Change this in future for different filters
792
882
  const suffix =
793
883
  blurElem?.tagName === 'feGaussianBlur' ? 'blur' : 'filter'
794
- gfilter.id = elem.id + '_' + suffix
884
+ gfilter.id = `${elem.id}_${suffix}`
795
885
  svgCanvas.changeSelectedAttribute(
796
886
  'filter',
797
- 'url(#' + gfilter.id + ')',
887
+ `url(#${gfilter.id})`,
798
888
  [elem]
799
889
  )
800
890
  }
@@ -901,20 +991,29 @@ const pushGroupProperty = (g, undoable) => {
901
991
  changes = {}
902
992
  changes.transform = oldxform || ''
903
993
 
994
+ // Simply prepend the group's transform to the child's transform list
995
+ // New transform = [group transform] [child transform]
996
+ // This preserves the correct application order
904
997
  const newxform = svgCanvas.getSvgRoot().createSVGTransform()
998
+ newxform.setMatrix(m)
905
999
 
906
- // [ gm ] [ chm ] = [ chm ] [ gm' ]
907
- // [ gm' ] = [ chmInv ] [ gm ] [ chm ]
908
- const chm = transformListToTransform(chtlist).matrix
909
- const chmInv = chm.inverse()
910
- const gm = matrixMultiply(chmInv, m, chm)
911
- newxform.setMatrix(gm)
912
- chtlist.appendItem(newxform)
913
- }
914
- const cmd = recalculateDimensions(elem)
915
- if (cmd) {
916
- batchCmd.addSubCommand(cmd)
1000
+ // Insert group's transform at the beginning of child's transform list
1001
+ if (chtlist.numberOfItems) {
1002
+ chtlist.insertItemBefore(newxform, 0)
1003
+ } else {
1004
+ chtlist.appendItem(newxform)
1005
+ }
1006
+
1007
+ // Record the transform change for undo/redo
1008
+ if (undoable) {
1009
+ batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
1010
+ }
917
1011
  }
1012
+ // NOTE: We intentionally do NOT call recalculateDimensions here because:
1013
+ // 1. It reorders transforms (moves rotate before translate), changing the visual result
1014
+ // 2. It recalculates rotation centers, causing elements to jump
1015
+ // 3. The prepended group transform is already in the correct position
1016
+ // Just leave the transforms as-is after prepending the group's transform
918
1017
  }
919
1018
  }
920
1019
 
@@ -972,6 +1071,10 @@ const convertToGroup = elem => {
972
1071
  svgCanvas.call('selected', [elem])
973
1072
  } else if (dataStorage.has($elem, 'symbol')) {
974
1073
  elem = dataStorage.get($elem, 'symbol')
1074
+ if (!elem) {
1075
+ warn('Unable to convert <use>: missing symbol reference', null, 'selected-elem')
1076
+ return
1077
+ }
975
1078
 
976
1079
  ts = $elem.getAttribute('transform') || ''
977
1080
  const pos = {
@@ -990,14 +1093,15 @@ const convertToGroup = elem => {
990
1093
  // Not ideal, but works
991
1094
  ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
992
1095
 
993
- const prev = $elem.previousElementSibling
1096
+ const useParent = $elem.parentNode
1097
+ const useNextSibling = $elem.nextSibling
994
1098
 
995
1099
  // Remove <use> element
996
1100
  batchCmd.addSubCommand(
997
1101
  new RemoveElementCommand(
998
1102
  $elem,
999
- $elem.nextElementSibling,
1000
- $elem.parentNode
1103
+ useNextSibling,
1104
+ useParent
1001
1105
  )
1002
1106
  )
1003
1107
  $elem.remove()
@@ -1049,7 +1153,9 @@ const convertToGroup = elem => {
1049
1153
  // now give the g itself a new id
1050
1154
  g.id = svgCanvas.getNextId()
1051
1155
 
1052
- prev.after(g)
1156
+ if (useParent) {
1157
+ useParent.insertBefore(g, useNextSibling)
1158
+ }
1053
1159
 
1054
1160
  if (parent) {
1055
1161
  if (!hasMore) {
@@ -1077,7 +1183,7 @@ const convertToGroup = elem => {
1077
1183
  try {
1078
1184
  recalculateDimensions(n)
1079
1185
  } catch (e) {
1080
- console.error(e)
1186
+ error('Error recalculating dimensions', e, 'selected-elem')
1081
1187
  }
1082
1188
  })
1083
1189
 
@@ -1098,7 +1204,7 @@ const convertToGroup = elem => {
1098
1204
 
1099
1205
  svgCanvas.addCommandToHistory(batchCmd)
1100
1206
  } else {
1101
- console.warn('Unexpected element to ungroup:', elem)
1207
+ warn('Unexpected element to ungroup:', elem, 'selected-elem')
1102
1208
  }
1103
1209
  }
1104
1210
 
@@ -1122,7 +1228,16 @@ const ungroupSelectedElement = () => {
1122
1228
  }
1123
1229
  if (g.tagName === 'use') {
1124
1230
  // Somehow doesn't have data set, so retrieve
1125
- const symbol = getElement(getHref(g).substr(1))
1231
+ const href = getHref(g)
1232
+ if (!href || !href.startsWith('#')) {
1233
+ warn('Unexpected <use> without local reference:', g, 'selected-elem')
1234
+ return
1235
+ }
1236
+ const symbol = getElement(href.slice(1))
1237
+ if (!symbol) {
1238
+ warn('Unexpected <use> without resolved reference:', g, 'selected-elem')
1239
+ return
1240
+ }
1126
1241
  dataStorage.put(g, 'symbol', symbol)
1127
1242
  dataStorage.put(g, 'ref', symbol)
1128
1243
  convertToGroup(g)
@@ -1206,7 +1321,7 @@ const updateCanvas = (w, h) => {
1206
1321
  height: svgCanvas.contentH * zoom,
1207
1322
  x,
1208
1323
  y,
1209
- viewBox: '0 0 ' + svgCanvas.contentW + ' ' + svgCanvas.contentH
1324
+ viewBox: `0 0 ${svgCanvas.contentW} ${svgCanvas.contentH}`
1210
1325
  })
1211
1326
 
1212
1327
  assignAttributes(bg, {
@@ -1226,7 +1341,7 @@ const updateCanvas = (w, h) => {
1226
1341
 
1227
1342
  svgCanvas.selectorManager.selectorParentGroup.setAttribute(
1228
1343
  'transform',
1229
- 'translate(' + x + ',' + y + ')'
1344
+ `translate(${x},${y})`
1230
1345
  )
1231
1346
 
1232
1347
  /**
package/core/selection.js CHANGED
@@ -409,8 +409,11 @@ const setRotationAngle = (val, preventUndo) => {
409
409
  cy,
410
410
  transformListToTransform(tlist).matrix
411
411
  )
412
+ // Safety check: if center coordinates are invalid (NaN), fall back to untransformed bbox center
413
+ const centerX = Number.isFinite(center.x) ? center.x : cx
414
+ const centerY = Number.isFinite(center.y) ? center.y : cy
412
415
  const Rnc = svgCanvas.getSvgRoot().createSVGTransform()
413
- Rnc.setRotate(val, center.x, center.y)
416
+ Rnc.setRotate(val, centerX, centerY)
414
417
  if (tlist.numberOfItems) {
415
418
  tlist.insertItemBefore(Rnc, 0)
416
419
  } else {
@@ -424,13 +427,20 @@ const setRotationAngle = (val, preventUndo) => {
424
427
  // we need to undo it, then redo it so it can be undo-able! :)
425
428
  // TODO: figure out how to make changes to transform list undo-able cross-browser?
426
429
  let newTransform = elem.getAttribute('transform')
430
+
427
431
  // new transform is something like: 'rotate(5 1.39625e-8 -11)'
428
432
  // we round the x so it becomes 'rotate(5 0 -11)'
429
- if (newTransform) {
430
- const newTransformArray = newTransform.split(/[ ,]+/)
431
- const round = (num) => Math.round(Number(num) + Number.EPSILON)
432
- const x = round(newTransformArray[1])
433
- newTransform = `${newTransformArray[0]} ${x} ${newTransformArray[2]}`
433
+ // Only do this manipulation if the first transform is actually a rotation
434
+ if (newTransform && newTransform.startsWith('rotate(')) {
435
+ const match = newTransform.match(/^rotate\(([\d.\-e]+)\s+([\d.\-e]+)\s+([\d.\-e]+)\)(.*)/)
436
+ if (match) {
437
+ const angle = Number.parseFloat(match[1])
438
+ const round = (num) => Math.round(Number(num) + Number.EPSILON)
439
+ const x = round(match[2])
440
+ const y = round(match[3])
441
+ const restOfTransform = match[4] || '' // Preserve any transforms after the rotate
442
+ newTransform = `rotate(${angle} ${x} ${y})${restOfTransform}`
443
+ }
434
444
  }
435
445
 
436
446
  if (oldTransform) {