@svgedit/svgcanvas 7.2.7 → 7.4.2
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 +247 -115
- package/core/event.js +171 -29
- package/core/history.js +105 -50
- 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 +51 -18
- package/core/select.js +44 -20
- package/core/selected-elem.js +159 -38
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +363 -307
- package/core/undo.js +47 -15
- package/core/units.js +8 -6
- package/core/utilities.js +282 -170
- package/dist/svgcanvas.js +40021 -55172
- 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 +11 -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
|
|
|
@@ -970,8 +1069,13 @@ const convertToGroup = elem => {
|
|
|
970
1069
|
tlist.appendItem(xform)
|
|
971
1070
|
recalculateDimensions(elem)
|
|
972
1071
|
svgCanvas.call('selected', [elem])
|
|
1072
|
+
return elem
|
|
973
1073
|
} else if (dataStorage.has($elem, 'symbol')) {
|
|
974
1074
|
elem = dataStorage.get($elem, 'symbol')
|
|
1075
|
+
if (!elem) {
|
|
1076
|
+
warn('Unable to convert <use>: missing symbol reference', null, 'selected-elem')
|
|
1077
|
+
return
|
|
1078
|
+
}
|
|
975
1079
|
|
|
976
1080
|
ts = $elem.getAttribute('transform') || ''
|
|
977
1081
|
const pos = {
|
|
@@ -990,14 +1094,15 @@ const convertToGroup = elem => {
|
|
|
990
1094
|
// Not ideal, but works
|
|
991
1095
|
ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
|
|
992
1096
|
|
|
993
|
-
const
|
|
1097
|
+
const useParent = $elem.parentNode
|
|
1098
|
+
const useNextSibling = $elem.nextSibling
|
|
994
1099
|
|
|
995
1100
|
// Remove <use> element
|
|
996
1101
|
batchCmd.addSubCommand(
|
|
997
1102
|
new RemoveElementCommand(
|
|
998
1103
|
$elem,
|
|
999
|
-
|
|
1000
|
-
|
|
1104
|
+
useNextSibling,
|
|
1105
|
+
useParent
|
|
1001
1106
|
)
|
|
1002
1107
|
)
|
|
1003
1108
|
$elem.remove()
|
|
@@ -1049,7 +1154,9 @@ const convertToGroup = elem => {
|
|
|
1049
1154
|
// now give the g itself a new id
|
|
1050
1155
|
g.id = svgCanvas.getNextId()
|
|
1051
1156
|
|
|
1052
|
-
|
|
1157
|
+
if (useParent) {
|
|
1158
|
+
useParent.insertBefore(g, useNextSibling)
|
|
1159
|
+
}
|
|
1053
1160
|
|
|
1054
1161
|
if (parent) {
|
|
1055
1162
|
if (!hasMore) {
|
|
@@ -1077,7 +1184,7 @@ const convertToGroup = elem => {
|
|
|
1077
1184
|
try {
|
|
1078
1185
|
recalculateDimensions(n)
|
|
1079
1186
|
} catch (e) {
|
|
1080
|
-
|
|
1187
|
+
error('Error recalculating dimensions', e, 'selected-elem')
|
|
1081
1188
|
}
|
|
1082
1189
|
})
|
|
1083
1190
|
|
|
@@ -1097,8 +1204,10 @@ const convertToGroup = elem => {
|
|
|
1097
1204
|
}
|
|
1098
1205
|
|
|
1099
1206
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
1207
|
+
return g
|
|
1100
1208
|
} else {
|
|
1101
|
-
|
|
1209
|
+
warn('Unexpected element to ungroup:', elem, 'selected-elem')
|
|
1210
|
+
return null
|
|
1102
1211
|
}
|
|
1103
1212
|
}
|
|
1104
1213
|
|
|
@@ -1116,17 +1225,29 @@ const ungroupSelectedElement = () => {
|
|
|
1116
1225
|
return
|
|
1117
1226
|
}
|
|
1118
1227
|
if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
|
|
1119
|
-
// Is svg, so
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1228
|
+
// Is svg (e.g. imported content), so convert to a real group first, then
|
|
1229
|
+
// fall through below to actually ungroup/flatten that new group - a
|
|
1230
|
+
// single Ungroup action on imported content should ungroup it, not just
|
|
1231
|
+
// silently swap its <use>/gsvg wrapper for a <g> that still needs a
|
|
1232
|
+
// second Ungroup to do anything.
|
|
1233
|
+
g = convertToGroup(g)
|
|
1234
|
+
if (!g) { return }
|
|
1235
|
+
} else if (g.tagName === 'use') {
|
|
1124
1236
|
// Somehow doesn't have data set, so retrieve
|
|
1125
|
-
const
|
|
1237
|
+
const href = getHref(g)
|
|
1238
|
+
if (!href || !href.startsWith('#')) {
|
|
1239
|
+
warn('Unexpected <use> without local reference:', g, 'selected-elem')
|
|
1240
|
+
return
|
|
1241
|
+
}
|
|
1242
|
+
const symbol = getElement(href.slice(1))
|
|
1243
|
+
if (!symbol) {
|
|
1244
|
+
warn('Unexpected <use> without resolved reference:', g, 'selected-elem')
|
|
1245
|
+
return
|
|
1246
|
+
}
|
|
1126
1247
|
dataStorage.put(g, 'symbol', symbol)
|
|
1127
1248
|
dataStorage.put(g, 'ref', symbol)
|
|
1128
|
-
convertToGroup(g)
|
|
1129
|
-
return
|
|
1249
|
+
g = convertToGroup(g)
|
|
1250
|
+
if (!g) { return }
|
|
1130
1251
|
}
|
|
1131
1252
|
const parentsA = getParents(g.parentNode, 'a')
|
|
1132
1253
|
if (parentsA?.length) {
|
|
@@ -1206,7 +1327,7 @@ const updateCanvas = (w, h) => {
|
|
|
1206
1327
|
height: svgCanvas.contentH * zoom,
|
|
1207
1328
|
x,
|
|
1208
1329
|
y,
|
|
1209
|
-
viewBox:
|
|
1330
|
+
viewBox: `0 0 ${svgCanvas.contentW} ${svgCanvas.contentH}`
|
|
1210
1331
|
})
|
|
1211
1332
|
|
|
1212
1333
|
assignAttributes(bg, {
|
|
@@ -1226,7 +1347,7 @@ const updateCanvas = (w, h) => {
|
|
|
1226
1347
|
|
|
1227
1348
|
svgCanvas.selectorManager.selectorParentGroup.setAttribute(
|
|
1228
1349
|
'transform',
|
|
1229
|
-
|
|
1350
|
+
`translate(${x},${y})`
|
|
1230
1351
|
)
|
|
1231
1352
|
|
|
1232
1353
|
/**
|
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) {
|