@svgedit/svgcanvas 7.2.3 → 7.2.4
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 +4 -0
- package/core/coords.js +203 -99
- package/core/draw.js +129 -56
- package/core/event.js +1 -0
- package/core/math.js +138 -154
- package/core/recalculate.js +232 -613
- package/core/sanitize.js +3 -3
- package/core/selected-elem.js +1 -1
- package/core/selection.js +1 -1
- package/core/svg-exec.js +163 -140
- package/core/text-actions.js +160 -129
- package/dist/svgcanvas.js +20310 -19109
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +1 -1
- package/svgcanvas.js +1 -1
package/CHANGES.md
CHANGED
package/core/coords.js
CHANGED
|
@@ -5,68 +5,90 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
snapToGrid,
|
|
8
|
+
snapToGrid,
|
|
9
|
+
assignAttributes,
|
|
10
|
+
getBBox,
|
|
11
|
+
getRefElem,
|
|
12
|
+
findDefs
|
|
9
13
|
} from './utilities.js'
|
|
10
14
|
import {
|
|
11
|
-
transformPoint,
|
|
15
|
+
transformPoint,
|
|
16
|
+
transformListToTransform,
|
|
17
|
+
matrixMultiply,
|
|
18
|
+
transformBox,
|
|
19
|
+
getTransformList
|
|
12
20
|
} from './math.js'
|
|
13
|
-
|
|
14
|
-
// this is how we map paths to our preferred relative segment types
|
|
15
|
-
const pathMap = [
|
|
16
|
-
0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a',
|
|
17
|
-
'H', 'h', 'V', 'v', 'S', 's', 'T', 't'
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @interface module:coords.EditorContext
|
|
22
|
-
*/
|
|
23
|
-
/**
|
|
24
|
-
* @function module:coords.EditorContext#getGridSnapping
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* @function module:coords.EditorContext#getSvgRoot
|
|
29
|
-
* @returns {SVGSVGElement}
|
|
30
|
-
*/
|
|
21
|
+
import { convertToNum } from './units.js'
|
|
31
22
|
|
|
32
23
|
let svgCanvas = null
|
|
33
24
|
|
|
34
25
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
37
|
-
* @
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
* Initialize the coords module with the SVG canvas.
|
|
27
|
+
* @function module:coords.init
|
|
28
|
+
* @param {Object} canvas - The SVG canvas object
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
export const init = canvas => {
|
|
40
32
|
svgCanvas = canvas
|
|
41
33
|
}
|
|
42
34
|
|
|
35
|
+
// This is how we map path segment types to their corresponding commands
|
|
36
|
+
const pathMap = [
|
|
37
|
+
0,
|
|
38
|
+
'z',
|
|
39
|
+
'M',
|
|
40
|
+
'm',
|
|
41
|
+
'L',
|
|
42
|
+
'l',
|
|
43
|
+
'C',
|
|
44
|
+
'c',
|
|
45
|
+
'Q',
|
|
46
|
+
'q',
|
|
47
|
+
'A',
|
|
48
|
+
'a',
|
|
49
|
+
'H',
|
|
50
|
+
'h',
|
|
51
|
+
'V',
|
|
52
|
+
'v',
|
|
53
|
+
'S',
|
|
54
|
+
's',
|
|
55
|
+
'T',
|
|
56
|
+
't'
|
|
57
|
+
]
|
|
58
|
+
|
|
43
59
|
/**
|
|
44
60
|
* Applies coordinate changes to an element based on the given matrix.
|
|
45
|
-
* @
|
|
46
|
-
* @
|
|
47
|
-
|
|
61
|
+
* @function module:coords.remapElement
|
|
62
|
+
* @param {Element} selected - The DOM element to remap
|
|
63
|
+
* @param {Object} changes - An object containing attribute changes
|
|
64
|
+
* @param {SVGMatrix} m - The transformation matrix
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
48
67
|
export const remapElement = (selected, changes, m) => {
|
|
49
68
|
const remap = (x, y) => transformPoint(x, y, m)
|
|
50
|
-
const scalew =
|
|
51
|
-
const scaleh =
|
|
52
|
-
const doSnapping =
|
|
69
|
+
const scalew = w => m.a * w
|
|
70
|
+
const scaleh = h => m.d * h
|
|
71
|
+
const doSnapping =
|
|
72
|
+
svgCanvas.getGridSnapping() &&
|
|
73
|
+
selected.parentNode.parentNode.localName === 'svg'
|
|
53
74
|
const finishUp = () => {
|
|
54
75
|
if (doSnapping) {
|
|
55
|
-
Object.entries(changes).forEach(([
|
|
56
|
-
changes[
|
|
76
|
+
Object.entries(changes).forEach(([attr, value]) => {
|
|
77
|
+
changes[attr] = snapToGrid(value)
|
|
57
78
|
})
|
|
58
79
|
}
|
|
59
80
|
assignAttributes(selected, changes, 1000, true)
|
|
60
81
|
}
|
|
61
|
-
const box = getBBox(selected)
|
|
82
|
+
const box = getBBox(selected)
|
|
62
83
|
|
|
63
|
-
|
|
84
|
+
// Handle gradients and patterns
|
|
85
|
+
;['fill', 'stroke'].forEach(type => {
|
|
64
86
|
const attrVal = selected.getAttribute(type)
|
|
65
87
|
if (attrVal?.startsWith('url(') && (m.a < 0 || m.d < 0)) {
|
|
66
88
|
const grad = getRefElem(attrVal)
|
|
67
89
|
const newgrad = grad.cloneNode(true)
|
|
68
90
|
if (m.a < 0) {
|
|
69
|
-
//
|
|
91
|
+
// Flip x
|
|
70
92
|
const x1 = newgrad.getAttribute('x1')
|
|
71
93
|
const x2 = newgrad.getAttribute('x2')
|
|
72
94
|
newgrad.setAttribute('x1', -(x1 - 1))
|
|
@@ -74,7 +96,7 @@ export const remapElement = (selected, changes, m) => {
|
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
if (m.d < 0) {
|
|
77
|
-
//
|
|
99
|
+
// Flip y
|
|
78
100
|
const y1 = newgrad.getAttribute('y1')
|
|
79
101
|
const y2 = newgrad.getAttribute('y2')
|
|
80
102
|
newgrad.setAttribute('y1', -(y1 - 1))
|
|
@@ -87,34 +109,22 @@ export const remapElement = (selected, changes, m) => {
|
|
|
87
109
|
})
|
|
88
110
|
|
|
89
111
|
const elName = selected.tagName
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const existing = transformListToTransform(selected).matrix
|
|
96
|
-
const tNew = matrixMultiply(existing.inverse(), m, existing)
|
|
97
|
-
changes.x = Number.parseFloat(changes.x) + tNew.e
|
|
98
|
-
changes.y = Number.parseFloat(changes.y) + tNew.f
|
|
99
|
-
} else {
|
|
100
|
-
// we just absorb all matrices into the element and don't do any remapping
|
|
101
|
-
const chlist = getTransformList(selected)
|
|
102
|
-
const mt = svgCanvas.getSvgRoot().createSVGTransform()
|
|
103
|
-
mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m))
|
|
104
|
-
chlist.clear()
|
|
105
|
-
chlist.appendItem(mt)
|
|
106
|
-
}
|
|
112
|
+
|
|
113
|
+
// Skip remapping for '<use>' elements
|
|
114
|
+
if (elName === 'use') {
|
|
115
|
+
// Do not remap '<use>' elements; transformations are handled via 'transform' attribute
|
|
116
|
+
return
|
|
107
117
|
}
|
|
108
118
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
119
|
+
// Now we have a set of changes and an applied reduced transform list
|
|
120
|
+
// We apply the changes directly to the DOM
|
|
111
121
|
switch (elName) {
|
|
112
122
|
case 'foreignObject':
|
|
113
123
|
case 'rect':
|
|
114
124
|
case 'image': {
|
|
115
|
-
|
|
125
|
+
// Allow images to be inverted (give them matrix when flipped)
|
|
116
126
|
if (elName === 'image' && (m.a < 0 || m.d < 0)) {
|
|
117
|
-
|
|
127
|
+
// Convert to matrix if flipped
|
|
118
128
|
const chlist = getTransformList(selected)
|
|
119
129
|
const mt = svgCanvas.getSvgRoot().createSVGTransform()
|
|
120
130
|
mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m))
|
|
@@ -131,66 +141,133 @@ export const remapElement = (selected, changes, m) => {
|
|
|
131
141
|
}
|
|
132
142
|
finishUp()
|
|
133
143
|
break
|
|
134
|
-
}
|
|
144
|
+
}
|
|
145
|
+
case 'ellipse': {
|
|
135
146
|
const c = remap(changes.cx, changes.cy)
|
|
136
147
|
changes.cx = c.x
|
|
137
148
|
changes.cy = c.y
|
|
138
|
-
changes.rx = scalew(changes.rx)
|
|
139
|
-
changes.ry = scaleh(changes.ry)
|
|
140
|
-
changes.rx = Math.abs(changes.rx)
|
|
141
|
-
changes.ry = Math.abs(changes.ry)
|
|
149
|
+
changes.rx = Math.abs(scalew(changes.rx))
|
|
150
|
+
changes.ry = Math.abs(scaleh(changes.ry))
|
|
142
151
|
finishUp()
|
|
143
152
|
break
|
|
144
|
-
}
|
|
153
|
+
}
|
|
154
|
+
case 'circle': {
|
|
145
155
|
const c = remap(changes.cx, changes.cy)
|
|
146
156
|
changes.cx = c.x
|
|
147
157
|
changes.cy = c.y
|
|
148
|
-
//
|
|
158
|
+
// Take the minimum of the new dimensions for the new circle radius
|
|
149
159
|
const tbox = transformBox(box.x, box.y, box.width, box.height, m)
|
|
150
|
-
const w = tbox.tr.x - tbox.tl.x
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (changes.r) { changes.r = Math.abs(changes.r) }
|
|
160
|
+
const w = tbox.tr.x - tbox.tl.x
|
|
161
|
+
const h = tbox.bl.y - tbox.tl.y
|
|
162
|
+
changes.r = Math.min(Math.abs(w / 2), Math.abs(h / 2))
|
|
154
163
|
finishUp()
|
|
155
164
|
break
|
|
156
|
-
}
|
|
165
|
+
}
|
|
166
|
+
case 'line': {
|
|
157
167
|
const pt1 = remap(changes.x1, changes.y1)
|
|
158
168
|
const pt2 = remap(changes.x2, changes.y2)
|
|
159
169
|
changes.x1 = pt1.x
|
|
160
170
|
changes.y1 = pt1.y
|
|
161
171
|
changes.x2 = pt2.x
|
|
162
172
|
changes.y2 = pt2.y
|
|
163
|
-
} // Fallthrough
|
|
164
|
-
case 'text':
|
|
165
|
-
case 'tspan':
|
|
166
|
-
case 'use': {
|
|
167
173
|
finishUp()
|
|
168
174
|
break
|
|
169
|
-
}
|
|
175
|
+
}
|
|
176
|
+
case 'text': {
|
|
177
|
+
const pt = remap(changes.x, changes.y)
|
|
178
|
+
changes.x = pt.x
|
|
179
|
+
changes.y = pt.y
|
|
180
|
+
|
|
181
|
+
// Scale font-size
|
|
182
|
+
let fontSize = selected.getAttribute('font-size')
|
|
183
|
+
if (!fontSize) {
|
|
184
|
+
// If not directly set, try computed style
|
|
185
|
+
fontSize = window.getComputedStyle(selected).fontSize
|
|
186
|
+
}
|
|
187
|
+
const fontSizeNum = parseFloat(fontSize)
|
|
188
|
+
if (!isNaN(fontSizeNum)) {
|
|
189
|
+
// Assume uniform scaling and use m.a
|
|
190
|
+
changes['font-size'] = fontSizeNum * Math.abs(m.a)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
finishUp()
|
|
194
|
+
|
|
195
|
+
// Handle child 'tspan' elements
|
|
196
|
+
const childNodes = selected.childNodes
|
|
197
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
198
|
+
const child = childNodes[i]
|
|
199
|
+
if (child.nodeType === 1 && child.tagName === 'tspan') {
|
|
200
|
+
const childChanges = {}
|
|
201
|
+
const hasX = child.hasAttribute('x')
|
|
202
|
+
const hasY = child.hasAttribute('y')
|
|
203
|
+
if (hasX) {
|
|
204
|
+
const childX = convertToNum('x', child.getAttribute('x'))
|
|
205
|
+
const childPtX = remap(childX, changes.y).x
|
|
206
|
+
childChanges.x = childPtX
|
|
207
|
+
}
|
|
208
|
+
if (hasY) {
|
|
209
|
+
const childY = convertToNum('y', child.getAttribute('y'))
|
|
210
|
+
const childPtY = remap(changes.x, childY).y
|
|
211
|
+
childChanges.y = childPtY
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let tspanFS = child.getAttribute('font-size')
|
|
215
|
+
if (!tspanFS) {
|
|
216
|
+
tspanFS = window.getComputedStyle(child).fontSize
|
|
217
|
+
}
|
|
218
|
+
const tspanFSNum = parseFloat(tspanFS)
|
|
219
|
+
if (!isNaN(tspanFSNum)) {
|
|
220
|
+
childChanges['font-size'] = tspanFSNum * Math.abs(m.a)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (hasX || hasY || childChanges['font-size']) {
|
|
224
|
+
assignAttributes(child, childChanges, 1000, true)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
case 'tspan': {
|
|
231
|
+
const pt = remap(changes.x, changes.y)
|
|
232
|
+
changes.x = pt.x
|
|
233
|
+
changes.y = pt.y
|
|
234
|
+
|
|
235
|
+
// Handle tspan font-size scaling
|
|
236
|
+
let tspanFS = selected.getAttribute('font-size')
|
|
237
|
+
if (!tspanFS) {
|
|
238
|
+
tspanFS = window.getComputedStyle(selected).fontSize
|
|
239
|
+
}
|
|
240
|
+
const tspanFSNum = parseFloat(tspanFS)
|
|
241
|
+
if (!isNaN(tspanFSNum)) {
|
|
242
|
+
changes['font-size'] = tspanFSNum * Math.abs(m.a)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
finishUp()
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
case 'g': {
|
|
170
249
|
const dataStorage = svgCanvas.getDataStorage()
|
|
171
250
|
const gsvg = dataStorage.get(selected, 'gsvg')
|
|
172
251
|
if (gsvg) {
|
|
173
252
|
assignAttributes(gsvg, changes, 1000, true)
|
|
174
253
|
}
|
|
175
254
|
break
|
|
176
|
-
}
|
|
255
|
+
}
|
|
256
|
+
case 'polyline':
|
|
177
257
|
case 'polygon': {
|
|
178
|
-
changes.points.forEach(
|
|
258
|
+
changes.points.forEach(pt => {
|
|
179
259
|
const { x, y } = remap(pt.x, pt.y)
|
|
180
260
|
pt.x = x
|
|
181
261
|
pt.y = y
|
|
182
262
|
})
|
|
183
|
-
|
|
184
|
-
// const len = changes.points.length;
|
|
185
|
-
let pstr = ''
|
|
186
|
-
changes.points.forEach((pt) => {
|
|
187
|
-
pstr += pt.x + ',' + pt.y + ' '
|
|
188
|
-
})
|
|
263
|
+
const pstr = changes.points.map(pt => `${pt.x},${pt.y}`).join(' ')
|
|
189
264
|
selected.setAttribute('points', pstr)
|
|
190
265
|
break
|
|
191
|
-
}
|
|
266
|
+
}
|
|
267
|
+
case 'path': {
|
|
268
|
+
// Handle path segments
|
|
192
269
|
const segList = selected.pathSegList
|
|
193
|
-
|
|
270
|
+
const len = segList.numberOfItems
|
|
194
271
|
changes.d = []
|
|
195
272
|
for (let i = 0; i < len; ++i) {
|
|
196
273
|
const seg = segList.getItem(i)
|
|
@@ -210,7 +287,6 @@ export const remapElement = (selected, changes, m) => {
|
|
|
210
287
|
}
|
|
211
288
|
}
|
|
212
289
|
|
|
213
|
-
len = changes.d.length
|
|
214
290
|
const firstseg = changes.d[0]
|
|
215
291
|
let currentpt
|
|
216
292
|
if (len > 0) {
|
|
@@ -221,11 +297,10 @@ export const remapElement = (selected, changes, m) => {
|
|
|
221
297
|
for (let i = 1; i < len; ++i) {
|
|
222
298
|
const seg = changes.d[i]
|
|
223
299
|
const { type } = seg
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const thisy = (seg.y !== undefined) ? seg.y : currentpt.y // for H commands
|
|
300
|
+
// If absolute or first segment, remap x, y, x1, y1, x2, y2
|
|
301
|
+
if (type % 2 === 0) {
|
|
302
|
+
const thisx = seg.x !== undefined ? seg.x : currentpt.x // For V commands
|
|
303
|
+
const thisy = seg.y !== undefined ? seg.y : currentpt.y // For H commands
|
|
229
304
|
const pt = remap(thisx, thisy)
|
|
230
305
|
const pt1 = remap(seg.x1, seg.y1)
|
|
231
306
|
const pt2 = remap(seg.x2, seg.y2)
|
|
@@ -237,7 +312,8 @@ export const remapElement = (selected, changes, m) => {
|
|
|
237
312
|
seg.y2 = pt2.y
|
|
238
313
|
seg.r1 = scalew(seg.r1)
|
|
239
314
|
seg.r2 = scaleh(seg.r2)
|
|
240
|
-
} else {
|
|
315
|
+
} else {
|
|
316
|
+
// For relative segments, scale x, y, x1, y1, x2, y2
|
|
241
317
|
seg.x = scalew(seg.x)
|
|
242
318
|
seg.y = scaleh(seg.y)
|
|
243
319
|
seg.x1 = scalew(seg.x1)
|
|
@@ -247,10 +323,10 @@ export const remapElement = (selected, changes, m) => {
|
|
|
247
323
|
seg.r1 = scalew(seg.r1)
|
|
248
324
|
seg.r2 = scaleh(seg.r2)
|
|
249
325
|
}
|
|
250
|
-
}
|
|
326
|
+
}
|
|
251
327
|
|
|
252
328
|
let dstr = ''
|
|
253
|
-
changes.d.forEach(
|
|
329
|
+
changes.d.forEach(seg => {
|
|
254
330
|
const { type } = seg
|
|
255
331
|
dstr += pathMap[type]
|
|
256
332
|
switch (type) {
|
|
@@ -272,8 +348,19 @@ export const remapElement = (selected, changes, m) => {
|
|
|
272
348
|
break
|
|
273
349
|
case 7: // relative cubic (c)
|
|
274
350
|
case 6: // absolute cubic (C)
|
|
275
|
-
dstr +=
|
|
276
|
-
seg.
|
|
351
|
+
dstr +=
|
|
352
|
+
seg.x1 +
|
|
353
|
+
',' +
|
|
354
|
+
seg.y1 +
|
|
355
|
+
' ' +
|
|
356
|
+
seg.x2 +
|
|
357
|
+
',' +
|
|
358
|
+
seg.y2 +
|
|
359
|
+
' ' +
|
|
360
|
+
seg.x +
|
|
361
|
+
',' +
|
|
362
|
+
seg.y +
|
|
363
|
+
' '
|
|
277
364
|
break
|
|
278
365
|
case 9: // relative quad (q)
|
|
279
366
|
case 8: // absolute quad (Q)
|
|
@@ -281,18 +368,35 @@ export const remapElement = (selected, changes, m) => {
|
|
|
281
368
|
break
|
|
282
369
|
case 11: // relative elliptical arc (a)
|
|
283
370
|
case 10: // absolute elliptical arc (A)
|
|
284
|
-
dstr +=
|
|
285
|
-
|
|
371
|
+
dstr +=
|
|
372
|
+
seg.r1 +
|
|
373
|
+
',' +
|
|
374
|
+
seg.r2 +
|
|
375
|
+
' ' +
|
|
376
|
+
seg.angle +
|
|
377
|
+
' ' +
|
|
378
|
+
Number(seg.largeArcFlag) +
|
|
379
|
+
' ' +
|
|
380
|
+
Number(seg.sweepFlag) +
|
|
381
|
+
' ' +
|
|
382
|
+
seg.x +
|
|
383
|
+
',' +
|
|
384
|
+
seg.y +
|
|
385
|
+
' '
|
|
286
386
|
break
|
|
287
387
|
case 17: // relative smooth cubic (s)
|
|
288
388
|
case 16: // absolute smooth cubic (S)
|
|
289
389
|
dstr += seg.x2 + ',' + seg.y2 + ' ' + seg.x + ',' + seg.y + ' '
|
|
290
390
|
break
|
|
391
|
+
default:
|
|
392
|
+
break
|
|
291
393
|
}
|
|
292
394
|
})
|
|
293
395
|
|
|
294
|
-
selected.setAttribute('d', dstr)
|
|
396
|
+
selected.setAttribute('d', dstr.trim())
|
|
295
397
|
break
|
|
296
398
|
}
|
|
399
|
+
default:
|
|
400
|
+
break
|
|
297
401
|
}
|
|
298
402
|
}
|