@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/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 +50 -30
- 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 +95 -24
- 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 +410 -15
- package/core/sanitize.js +46 -14
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- 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 +282 -170
- package/dist/svgcanvas.js +31590 -53383
- 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/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:
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
268
|
-
gripElement.setAttribute('style',
|
|
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:
|
|
366
|
+
id: `selectorGrip_resize_${dir}`,
|
|
345
367
|
fill: '#22C',
|
|
346
368
|
r: gripRadius,
|
|
347
|
-
style:
|
|
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
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
584
|
+
export const getSelectorManager = () => selectModule.getSelectorManager()
|
package/core/selected-elem.js
CHANGED
|
@@ -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
|
|
107
|
-
if (firstChild
|
|
108
|
-
firstChild = firstChild.
|
|
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
|
|
113
|
-
firstChild = firstChild.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
884
|
+
gfilter.id = `${elem.id}_${suffix}`
|
|
795
885
|
svgCanvas.changeSelectedAttribute(
|
|
796
886
|
'filter',
|
|
797
|
-
|
|
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
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
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
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
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) {
|