@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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # svgcanvas CHANGES
2
2
 
3
+ ## 7.2.4
4
+ - bug fixes
5
+ - update dependencies
6
+
3
7
  ## 7.2.3
4
8
  - update dependencies
5
9
 
package/core/coords.js CHANGED
@@ -5,68 +5,90 @@
5
5
  */
6
6
 
7
7
  import {
8
- snapToGrid, assignAttributes, getBBox, getRefElem, findDefs
8
+ snapToGrid,
9
+ assignAttributes,
10
+ getBBox,
11
+ getRefElem,
12
+ findDefs
9
13
  } from './utilities.js'
10
14
  import {
11
- transformPoint, transformListToTransform, matrixMultiply, transformBox, getTransformList
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
- * @function module:coords.init
36
- * @param {module:svgcanvas.SvgCanvas#event:pointsAdded} editorContext
37
- * @returns {void}
38
- */
39
- export const init = (canvas) => {
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
- * @name module:coords.remapElement
46
- * @type {module:path.EditorContext#remapElement}
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 = (w) => m.a * w
51
- const scaleh = (h) => m.d * h
52
- const doSnapping = svgCanvas.getGridSnapping() && selected.parentNode.parentNode.localName === 'svg'
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(([o, value]) => {
56
- changes[o] = snapToGrid(value)
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
- ['fill', 'stroke'].forEach((type) => {
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
- // flip x
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
- // flip y
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
- if (elName === 'g' || elName === 'text' || elName === 'tspan' || elName === 'use') {
91
- // if it was a translate, then just update x,y
92
- if (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && (m.e !== 0 || m.f !== 0)) {
93
- // [T][M] = [M][T']
94
- // therefore [T'] = [M_inv][T][M]
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
- // now we have a set of changes and an applied reduced transform list
110
- // we apply the changes directly to the DOM
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
- // Allow images to be inverted (give them matrix when flipped)
125
+ // Allow images to be inverted (give them matrix when flipped)
116
126
  if (elName === 'image' && (m.a < 0 || m.d < 0)) {
117
- // Convert to matrix
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
- } case 'ellipse': {
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
- } case 'circle': {
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
- // take the minimum of the new selected box's dimensions for the new circle radius
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; const h = tbox.bl.y - tbox.tl.y
151
- changes.r = Math.min(w / 2, h / 2)
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
- } case 'line': {
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
- } case 'g': {
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
- } case 'polyline':
255
+ }
256
+ case 'polyline':
177
257
  case 'polygon': {
178
- changes.points.forEach((pt) => {
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
- } case 'path': {
266
+ }
267
+ case 'path': {
268
+ // Handle path segments
192
269
  const segList = selected.pathSegList
193
- let len = segList.numberOfItems
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
- // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2
225
- // if relative, we want to scalew, scaleh
226
- if (type % 2 === 0) { // absolute
227
- const thisx = (seg.x !== undefined) ? seg.x : currentpt.x // for V commands
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 { // relative
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
- } // for each segment
326
+ }
251
327
 
252
328
  let dstr = ''
253
- changes.d.forEach((seg) => {
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 += seg.x1 + ',' + seg.y1 + ' ' + seg.x2 + ',' + seg.y2 + ' ' +
276
- seg.x + ',' + seg.y + ' '
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 += seg.r1 + ',' + seg.r2 + ' ' + seg.angle + ' ' + Number(seg.largeArcFlag) +
285
- ' ' + Number(seg.sweepFlag) + ' ' + seg.x + ',' + seg.y + ' '
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
  }