@svgedit/svgcanvas 7.1.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/recalculate.js ADDED
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Recalculate.
3
+ * @module recalculate
4
+ * @license MIT
5
+ */
6
+
7
+ import { NS } from './namespaces.js'
8
+ import { convertToNum } from '../../src/common/units.js'
9
+ import { getRotationAngle, getHref, getBBox, getRefElem } from './utilities.js'
10
+ import { BatchCommand, ChangeElementCommand } from './history.js'
11
+ import { remapElement } from './coords.js'
12
+ import {
13
+ isIdentity, matrixMultiply, transformPoint, transformListToTransform,
14
+ hasMatrixTransform
15
+ } from './math.js'
16
+ import {
17
+ mergeDeep
18
+ } from '../../src/common/util.js'
19
+
20
+ let svgCanvas
21
+
22
+ /**
23
+ * @interface module:recalculate.EditorContext
24
+ */
25
+ /**
26
+ * @function module:recalculate.EditorContext#getSvgRoot
27
+ * @returns {SVGSVGElement} The root DOM element
28
+ */
29
+ /**
30
+ * @function module:recalculate.EditorContext#getStartTransform
31
+ * @returns {string}
32
+ */
33
+ /**
34
+ * @function module:recalculate.EditorContext#setStartTransform
35
+ * @param {string} transform
36
+ * @returns {void}
37
+ */
38
+
39
+ /**
40
+ * @function module:recalculate.init
41
+ * @param {module:recalculate.EditorContext} editorContext
42
+ * @returns {void}
43
+ */
44
+ export const init = (canvas) => {
45
+ svgCanvas = canvas
46
+ }
47
+
48
+ /**
49
+ * Updates a `<clipPath>`s values based on the given translation of an element.
50
+ * @function module:recalculate.updateClipPath
51
+ * @param {string} attr - The clip-path attribute value with the clipPath's ID
52
+ * @param {Float} tx - The translation's x value
53
+ * @param {Float} ty - The translation's y value
54
+ * @returns {void}
55
+ */
56
+ export const updateClipPath = (attr, tx, ty) => {
57
+ const path = getRefElem(attr).firstChild
58
+ const cpXform = path.transform.baseVal
59
+ const newxlate = svgCanvas.getSvgRoot().createSVGTransform()
60
+ newxlate.setTranslate(tx, ty)
61
+
62
+ cpXform.appendItem(newxlate)
63
+
64
+ // Update clipPath's dimensions
65
+ recalculateDimensions(path)
66
+ }
67
+
68
+ /**
69
+ * Decides the course of action based on the element's transform list.
70
+ * @function module:recalculate.recalculateDimensions
71
+ * @param {Element} selected - The DOM element to recalculate
72
+ * @returns {Command} Undo command object with the resulting change
73
+ */
74
+ export const recalculateDimensions = (selected) => {
75
+ if (!selected) return null
76
+ const svgroot = svgCanvas.getSvgRoot()
77
+ const dataStorage = svgCanvas.getDataStorage()
78
+ const tlist = selected.transform?.baseVal
79
+ // remove any unnecessary transforms
80
+ if (tlist?.numberOfItems > 0) {
81
+ let k = tlist.numberOfItems
82
+ const noi = k
83
+ while (k--) {
84
+ const xform = tlist.getItem(k)
85
+ if (xform.type === 0) {
86
+ tlist.removeItem(k)
87
+ // remove identity matrices
88
+ } else if (xform.type === 1) {
89
+ if (isIdentity(xform.matrix)) {
90
+ if (noi === 1) {
91
+ // Overcome Chrome bug (though only when noi is 1) with
92
+ // `removeItem` preventing `removeAttribute` from
93
+ // subsequently working
94
+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=843901
95
+ selected.removeAttribute('transform')
96
+ return null
97
+ }
98
+ tlist.removeItem(k)
99
+ }
100
+ // remove zero-degree rotations
101
+ } else if (xform.type === 4 && xform.angle === 0) {
102
+ tlist.removeItem(k)
103
+ }
104
+ }
105
+ // End here if all it has is a rotation
106
+ if (tlist.numberOfItems === 1 &&
107
+ getRotationAngle(selected)) { return null }
108
+ }
109
+
110
+ // if this element had no transforms, we are done
111
+ if (!tlist || tlist.numberOfItems === 0) {
112
+ // Chrome apparently had a bug that requires clearing the attribute first.
113
+ selected.setAttribute('transform', '')
114
+ // However, this still next line currently doesn't work at all in Chrome
115
+ selected.removeAttribute('transform')
116
+ // selected.transform.baseVal.clear(); // Didn't help for Chrome bug
117
+ return null
118
+ }
119
+
120
+ // TODO: Make this work for more than 2
121
+ if (tlist) {
122
+ let mxs = []
123
+ let k = tlist.numberOfItems
124
+ while (k--) {
125
+ const xform = tlist.getItem(k)
126
+ if (xform.type === 1) {
127
+ mxs.push([xform.matrix, k])
128
+ } else if (mxs.length) {
129
+ mxs = []
130
+ }
131
+ }
132
+ if (mxs.length === 2) {
133
+ const mNew = svgroot.createSVGTransformFromMatrix(matrixMultiply(mxs[1][0], mxs[0][0]))
134
+ tlist.removeItem(mxs[0][1])
135
+ tlist.removeItem(mxs[1][1])
136
+ tlist.insertItemBefore(mNew, mxs[1][1])
137
+ }
138
+
139
+ // combine matrix + translate
140
+ k = tlist.numberOfItems
141
+ if (k >= 2 && tlist.getItem(k - 2).type === 1 && tlist.getItem(k - 1).type === 2) {
142
+ const mt = svgroot.createSVGTransform()
143
+
144
+ const m = matrixMultiply(
145
+ tlist.getItem(k - 2).matrix,
146
+ tlist.getItem(k - 1).matrix
147
+ )
148
+ mt.setMatrix(m)
149
+ tlist.removeItem(k - 2)
150
+ tlist.removeItem(k - 2)
151
+ tlist.appendItem(mt)
152
+ }
153
+ }
154
+
155
+ // If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned).
156
+ switch (selected.tagName) {
157
+ // Ignore these elements, as they can absorb the [M]
158
+ case 'line':
159
+ case 'polyline':
160
+ case 'polygon':
161
+ case 'path':
162
+ break
163
+ default:
164
+ if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) ||
165
+ (tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4)) {
166
+ return null
167
+ }
168
+ }
169
+ // Grouped SVG element
170
+ const gsvg = (dataStorage.has(selected, 'gsvg')) ? dataStorage.get(selected, 'gsvg') : undefined
171
+ // we know we have some transforms, so set up return variable
172
+ const batchCmd = new BatchCommand('Transform')
173
+
174
+ // store initial values that will be affected by reducing the transform list
175
+ let changes = {}
176
+ let initial = null
177
+ let attrs = []
178
+ switch (selected.tagName) {
179
+ case 'line':
180
+ attrs = ['x1', 'y1', 'x2', 'y2']
181
+ break
182
+ case 'circle':
183
+ attrs = ['cx', 'cy', 'r']
184
+ break
185
+ case 'ellipse':
186
+ attrs = ['cx', 'cy', 'rx', 'ry']
187
+ break
188
+ case 'foreignObject':
189
+ case 'rect':
190
+ case 'image':
191
+ attrs = ['width', 'height', 'x', 'y']
192
+ break
193
+ case 'use':
194
+ case 'text':
195
+ case 'tspan':
196
+ attrs = ['x', 'y']
197
+ break
198
+ case 'polygon':
199
+ case 'polyline': {
200
+ initial = {}
201
+ initial.points = selected.getAttribute('points')
202
+ const list = selected.points
203
+ const len = list.numberOfItems
204
+ changes.points = new Array(len)
205
+ for (let i = 0; i < len; ++i) {
206
+ const pt = list.getItem(i)
207
+ changes.points[i] = { x: pt.x, y: pt.y }
208
+ }
209
+ break
210
+ } case 'path':
211
+ initial = {}
212
+ initial.d = selected.getAttribute('d')
213
+ changes.d = selected.getAttribute('d')
214
+ break
215
+ } // switch on element type to get initial values
216
+
217
+ if (attrs.length) {
218
+ attrs.forEach((attr) => {
219
+ changes[attr] = convertToNum(attr, selected.getAttribute(attr))
220
+ })
221
+ } else if (gsvg) {
222
+ // GSVG exception
223
+ changes = {
224
+ x: Number(gsvg.getAttribute('x')) || 0,
225
+ y: Number(gsvg.getAttribute('y')) || 0
226
+ }
227
+ }
228
+
229
+ // if we haven't created an initial array in polygon/polyline/path, then
230
+ // make a copy of initial values and include the transform
231
+ if (!initial) {
232
+ initial = mergeDeep({}, changes)
233
+ for (const [attr, val] of Object.entries(initial)) {
234
+ initial[attr] = convertToNum(attr, val)
235
+ }
236
+ }
237
+ // save the start transform value too
238
+ initial.transform = svgCanvas.getStartTransform() || ''
239
+
240
+ let oldcenter; let newcenter
241
+
242
+ // if it's a regular group, we have special processing to flatten transforms
243
+ if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {
244
+ const box = getBBox(selected)
245
+
246
+ oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
247
+ newcenter = transformPoint(
248
+ box.x + box.width / 2,
249
+ box.y + box.height / 2,
250
+ transformListToTransform(tlist).matrix
251
+ )
252
+ // let m = svgroot.createSVGMatrix();
253
+
254
+ // temporarily strip off the rotate and save the old center
255
+ const gangle = getRotationAngle(selected)
256
+ if (gangle) {
257
+ const a = gangle * Math.PI / 180
258
+ const s = Math.abs(a) > (1.0e-10) ? Math.sin(a) / (1 - Math.cos(a)) : 2 / a
259
+ for (let i = 0; i < tlist.numberOfItems; ++i) {
260
+ const xform = tlist.getItem(i)
261
+ if (xform.type === 4) {
262
+ // extract old center through mystical arts
263
+ const rm = xform.matrix
264
+ oldcenter.y = (s * rm.e + rm.f) / 2
265
+ oldcenter.x = (rm.e - s * rm.f) / 2
266
+ tlist.removeItem(i)
267
+ break
268
+ }
269
+ }
270
+ }
271
+
272
+ const N = tlist.numberOfItems
273
+ let tx = 0; let ty = 0; let operation = 0
274
+
275
+ let firstM
276
+ if (N) {
277
+ firstM = tlist.getItem(0).matrix
278
+ }
279
+
280
+ let oldStartTransform
281
+ // first, if it was a scale then the second-last transform will be it
282
+ if (N >= 3 && tlist.getItem(N - 2).type === 3 &&
283
+ tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {
284
+ operation = 3 // scale
285
+
286
+ // if the children are unrotated, pass the scale down directly
287
+ // otherwise pass the equivalent matrix() down directly
288
+ const tm = tlist.getItem(N - 3).matrix
289
+ const sm = tlist.getItem(N - 2).matrix
290
+ const tmn = tlist.getItem(N - 1).matrix
291
+
292
+ const children = selected.childNodes
293
+ let c = children.length
294
+ while (c--) {
295
+ const child = children.item(c)
296
+ tx = 0
297
+ ty = 0
298
+ if (child.nodeType === 1) {
299
+ const childTlist = child.transform.baseVal
300
+
301
+ // some children might not have a transform (<metadata>, <defs>, etc)
302
+ if (!childTlist) { continue }
303
+
304
+ const m = transformListToTransform(childTlist).matrix
305
+
306
+ // Convert a matrix to a scale if applicable
307
+ // if (hasMatrixTransform(childTlist) && childTlist.numberOfItems == 1) {
308
+ // if (m.b==0 && m.c==0 && m.e==0 && m.f==0) {
309
+ // childTlist.removeItem(0);
310
+ // const translateOrigin = svgroot.createSVGTransform(),
311
+ // scale = svgroot.createSVGTransform(),
312
+ // translateBack = svgroot.createSVGTransform();
313
+ // translateOrigin.setTranslate(0, 0);
314
+ // scale.setScale(m.a, m.d);
315
+ // translateBack.setTranslate(0, 0);
316
+ // childTlist.appendItem(translateBack);
317
+ // childTlist.appendItem(scale);
318
+ // childTlist.appendItem(translateOrigin);
319
+ // }
320
+ // }
321
+
322
+ const angle = getRotationAngle(child)
323
+ oldStartTransform = svgCanvas.getStartTransform()
324
+ // const childxforms = [];
325
+ svgCanvas.setStartTransform(child.getAttribute('transform'))
326
+ if (angle || hasMatrixTransform(childTlist)) {
327
+ const e2t = svgroot.createSVGTransform()
328
+ e2t.setMatrix(matrixMultiply(tm, sm, tmn, m))
329
+ childTlist.clear()
330
+ childTlist.appendItem(e2t)
331
+ // childxforms.push(e2t);
332
+ // if not rotated or skewed, push the [T][S][-T] down to the child
333
+ } else {
334
+ // update the transform list with translate,scale,translate
335
+
336
+ // slide the [T][S][-T] from the front to the back
337
+ // [T][S][-T][M] = [M][T2][S2][-T2]
338
+
339
+ // (only bringing [-T] to the right of [M])
340
+ // [T][S][-T][M] = [T][S][M][-T2]
341
+ // [-T2] = [M_inv][-T][M]
342
+ const t2n = matrixMultiply(m.inverse(), tmn, m)
343
+ // [T2] is always negative translation of [-T2]
344
+ const t2 = svgroot.createSVGMatrix()
345
+ t2.e = -t2n.e
346
+ t2.f = -t2n.f
347
+
348
+ // [T][S][-T][M] = [M][T2][S2][-T2]
349
+ // [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv]
350
+ const s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse())
351
+
352
+ const translateOrigin = svgroot.createSVGTransform()
353
+ const scale = svgroot.createSVGTransform()
354
+ const translateBack = svgroot.createSVGTransform()
355
+ translateOrigin.setTranslate(t2n.e, t2n.f)
356
+ scale.setScale(s2.a, s2.d)
357
+ translateBack.setTranslate(t2.e, t2.f)
358
+ childTlist.appendItem(translateBack)
359
+ childTlist.appendItem(scale)
360
+ childTlist.appendItem(translateOrigin)
361
+ } // not rotated
362
+ batchCmd.addSubCommand(recalculateDimensions(child))
363
+ svgCanvas.setStartTransform(oldStartTransform)
364
+ } // element
365
+ } // for each child
366
+ // Remove these transforms from group
367
+ tlist.removeItem(N - 1)
368
+ tlist.removeItem(N - 2)
369
+ tlist.removeItem(N - 3)
370
+ } else if (N >= 3 && tlist.getItem(N - 1).type === 1) {
371
+ operation = 3 // scale
372
+ const m = transformListToTransform(tlist).matrix
373
+ const e2t = svgroot.createSVGTransform()
374
+ e2t.setMatrix(m)
375
+ tlist.clear()
376
+ tlist.appendItem(e2t)
377
+ // next, check if the first transform was a translate
378
+ // if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
379
+ // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
380
+ } else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
381
+ tlist.getItem(0).type === 2) {
382
+ operation = 2 // translate
383
+ const T_M = transformListToTransform(tlist).matrix
384
+ tlist.removeItem(0)
385
+ const mInv = transformListToTransform(tlist).matrix.inverse()
386
+ const M2 = matrixMultiply(mInv, T_M)
387
+
388
+ tx = M2.e
389
+ ty = M2.f
390
+
391
+ if (tx !== 0 || ty !== 0) {
392
+ // we pass the translates down to the individual children
393
+ const children = selected.childNodes
394
+ let c = children.length
395
+
396
+ const clipPathsDone = []
397
+ while (c--) {
398
+ const child = children.item(c)
399
+ if (child.nodeType === 1) {
400
+ // Check if child has clip-path
401
+ if (child.getAttribute('clip-path')) {
402
+ // tx, ty
403
+ const attr = child.getAttribute('clip-path')
404
+ if (!clipPathsDone.includes(attr)) {
405
+ updateClipPath(attr, tx, ty)
406
+ clipPathsDone.push(attr)
407
+ }
408
+ }
409
+
410
+ oldStartTransform = svgCanvas.getStartTransform()
411
+ svgCanvas.setStartTransform(child.getAttribute('transform'))
412
+
413
+ const childTlist = child.transform?.baseVal
414
+ // some children might not have a transform (<metadata>, <defs>, etc)
415
+ if (childTlist) {
416
+ const newxlate = svgroot.createSVGTransform()
417
+ newxlate.setTranslate(tx, ty)
418
+ if (childTlist.numberOfItems) {
419
+ childTlist.insertItemBefore(newxlate, 0)
420
+ } else {
421
+ childTlist.appendItem(newxlate)
422
+ }
423
+ batchCmd.addSubCommand(recalculateDimensions(child))
424
+ // If any <use> have this group as a parent and are
425
+ // referencing this child, then impose a reverse translate on it
426
+ // so that when it won't get double-translated
427
+ const uses = selected.getElementsByTagNameNS(NS.SVG, 'use')
428
+ const href = '#' + child.id
429
+ let u = uses.length
430
+ while (u--) {
431
+ const useElem = uses.item(u)
432
+ if (href === getHref(useElem)) {
433
+ const usexlate = svgroot.createSVGTransform()
434
+ usexlate.setTranslate(-tx, -ty)
435
+ useElem.transform.baseVal.insertItemBefore(usexlate, 0)
436
+ batchCmd.addSubCommand(recalculateDimensions(useElem))
437
+ }
438
+ }
439
+ svgCanvas.setStartTransform(oldStartTransform)
440
+ }
441
+ }
442
+ }
443
+ svgCanvas.setStartTransform(oldStartTransform)
444
+ }
445
+ // else, a matrix imposition from a parent group
446
+ // keep pushing it down to the children
447
+ } else if (N === 1 && tlist.getItem(0).type === 1 && !gangle) {
448
+ operation = 1
449
+ const m = tlist.getItem(0).matrix
450
+ const children = selected.childNodes
451
+ let c = children.length
452
+ while (c--) {
453
+ const child = children.item(c)
454
+ if (child.nodeType === 1) {
455
+ oldStartTransform = svgCanvas.getStartTransform()
456
+ svgCanvas.setStartTransform(child.getAttribute('transform'))
457
+ const childTlist = child.transform?.baseVal
458
+
459
+ if (!childTlist) { continue }
460
+
461
+ const em = matrixMultiply(m, transformListToTransform(childTlist).matrix)
462
+ const e2m = svgroot.createSVGTransform()
463
+ e2m.setMatrix(em)
464
+ childTlist.clear()
465
+ childTlist.appendItem(e2m, 0)
466
+
467
+ batchCmd.addSubCommand(recalculateDimensions(child))
468
+ svgCanvas.setStartTransform(oldStartTransform)
469
+
470
+ // Convert stroke
471
+ // TODO: Find out if this should actually happen somewhere else
472
+ const sw = child.getAttribute('stroke-width')
473
+ if (child.getAttribute('stroke') !== 'none' && !isNaN(sw)) {
474
+ const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2
475
+ child.setAttribute('stroke-width', sw * avg)
476
+ }
477
+ }
478
+ }
479
+ tlist.clear()
480
+ // else it was just a rotate
481
+ } else {
482
+ if (gangle) {
483
+ const newRot = svgroot.createSVGTransform()
484
+ newRot.setRotate(gangle, newcenter.x, newcenter.y)
485
+ if (tlist.numberOfItems) {
486
+ tlist.insertItemBefore(newRot, 0)
487
+ } else {
488
+ tlist.appendItem(newRot)
489
+ }
490
+ }
491
+ if (tlist.numberOfItems === 0) {
492
+ selected.removeAttribute('transform')
493
+ }
494
+ return null
495
+ }
496
+
497
+ // if it was a translate, put back the rotate at the new center
498
+ if (operation === 2) {
499
+ if (gangle) {
500
+ newcenter = {
501
+ x: oldcenter.x + firstM.e,
502
+ y: oldcenter.y + firstM.f
503
+ }
504
+
505
+ const newRot = svgroot.createSVGTransform()
506
+ newRot.setRotate(gangle, newcenter.x, newcenter.y)
507
+ if (tlist.numberOfItems) {
508
+ tlist.insertItemBefore(newRot, 0)
509
+ } else {
510
+ tlist.appendItem(newRot)
511
+ }
512
+ }
513
+ // if it was a resize
514
+ } else if (operation === 3) {
515
+ const m = transformListToTransform(tlist).matrix
516
+ const roldt = svgroot.createSVGTransform()
517
+ roldt.setRotate(gangle, oldcenter.x, oldcenter.y)
518
+ const rold = roldt.matrix
519
+ const rnew = svgroot.createSVGTransform()
520
+ rnew.setRotate(gangle, newcenter.x, newcenter.y)
521
+ const rnewInv = rnew.matrix.inverse()
522
+ const mInv = m.inverse()
523
+ const extrat = matrixMultiply(mInv, rnewInv, rold, m)
524
+
525
+ tx = extrat.e
526
+ ty = extrat.f
527
+
528
+ if (tx !== 0 || ty !== 0) {
529
+ // now push this transform down to the children
530
+ // we pass the translates down to the individual children
531
+ const children = selected.childNodes
532
+ let c = children.length
533
+ while (c--) {
534
+ const child = children.item(c)
535
+ if (child.nodeType === 1) {
536
+ oldStartTransform = svgCanvas.getStartTransform()
537
+ svgCanvas.setStartTransform(child.getAttribute('transform'))
538
+ const childTlist = child.transform?.baseVal
539
+ const newxlate = svgroot.createSVGTransform()
540
+ newxlate.setTranslate(tx, ty)
541
+ if (childTlist.numberOfItems) {
542
+ childTlist.insertItemBefore(newxlate, 0)
543
+ } else {
544
+ childTlist.appendItem(newxlate)
545
+ }
546
+
547
+ batchCmd.addSubCommand(recalculateDimensions(child))
548
+ svgCanvas.setStartTransform(oldStartTransform)
549
+ }
550
+ }
551
+ }
552
+
553
+ if (gangle) {
554
+ if (tlist.numberOfItems) {
555
+ tlist.insertItemBefore(rnew, 0)
556
+ } else {
557
+ tlist.appendItem(rnew)
558
+ }
559
+ }
560
+ }
561
+ // else, it's a non-group
562
+ } else {
563
+ // TODO: box might be null for some elements (<metadata> etc), need to handle this
564
+ const box = getBBox(selected)
565
+
566
+ // Paths (and possbly other shapes) will have no BBox while still in <defs>,
567
+ // but we still may need to recalculate them (see issue 595).
568
+ // TODO: Figure out how to get BBox from these elements in case they
569
+ // have a rotation transform
570
+
571
+ if (!box && selected.tagName !== 'path') return null
572
+
573
+ let m // = svgroot.createSVGMatrix();
574
+ // temporarily strip off the rotate and save the old center
575
+ const angle = getRotationAngle(selected)
576
+ if (angle) {
577
+ oldcenter = { x: box.x + box.width / 2, y: box.y + box.height / 2 }
578
+ newcenter = transformPoint(
579
+ box.x + box.width / 2,
580
+ box.y + box.height / 2,
581
+ transformListToTransform(tlist).matrix
582
+ )
583
+
584
+ const a = angle * Math.PI / 180
585
+ const s = (Math.abs(a) > (1.0e-10))
586
+ ? Math.sin(a) / (1 - Math.cos(a))
587
+ // TODO: This blows up if the angle is exactly 0!
588
+ : 2 / a
589
+
590
+ for (let i = 0; i < tlist.numberOfItems; ++i) {
591
+ const xform = tlist.getItem(i)
592
+ if (xform.type === 4) {
593
+ // extract old center through mystical arts
594
+ const rm = xform.matrix
595
+ oldcenter.y = (s * rm.e + rm.f) / 2
596
+ oldcenter.x = (rm.e - s * rm.f) / 2
597
+ tlist.removeItem(i)
598
+ break
599
+ }
600
+ }
601
+ }
602
+
603
+ // 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition
604
+ let operation = 0
605
+ const N = tlist.numberOfItems
606
+
607
+ // Check if it has a gradient with userSpaceOnUse, in which case
608
+ // adjust it by recalculating the matrix transform.
609
+
610
+ const fill = selected.getAttribute('fill')
611
+ if (fill?.startsWith('url(')) {
612
+ const paint = getRefElem(fill)
613
+ if (paint) {
614
+ let type = 'pattern'
615
+ if (paint?.tagName !== type) type = 'gradient'
616
+ const attrVal = paint.getAttribute(type + 'Units')
617
+ if (attrVal === 'userSpaceOnUse') {
618
+ // Update the userSpaceOnUse element
619
+ m = transformListToTransform(tlist).matrix
620
+ const gtlist = paint.transform.baseVal
621
+ const gmatrix = transformListToTransform(gtlist).matrix
622
+ m = matrixMultiply(m, gmatrix)
623
+ const mStr = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')'
624
+ paint.setAttribute(type + 'Transform', mStr)
625
+ }
626
+ }
627
+ }
628
+
629
+ // first, if it was a scale of a non-skewed element, then the second-last
630
+ // transform will be the [S]
631
+ // if we had [M][T][S][T] we want to extract the matrix equivalent of
632
+ // [T][S][T] and push it down to the element
633
+ if (N >= 3 && tlist.getItem(N - 2).type === 3 &&
634
+ tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {
635
+ // Removed this so a <use> with a given [T][S][T] would convert to a matrix.
636
+ // Is that bad?
637
+ // && selected.nodeName != 'use'
638
+ operation = 3 // scale
639
+ m = transformListToTransform(tlist, N - 3, N - 1).matrix
640
+ tlist.removeItem(N - 1)
641
+ tlist.removeItem(N - 2)
642
+ tlist.removeItem(N - 3)
643
+ // if we had [T][S][-T][M], then this was a skewed element being resized
644
+ // Thus, we simply combine it all into one matrix
645
+ } else if (N === 4 && tlist.getItem(N - 1).type === 1) {
646
+ operation = 3 // scale
647
+ m = transformListToTransform(tlist).matrix
648
+ const e2t = svgroot.createSVGTransform()
649
+ e2t.setMatrix(m)
650
+ tlist.clear()
651
+ tlist.appendItem(e2t)
652
+ // reset the matrix so that the element is not re-mapped
653
+ m = svgroot.createSVGMatrix()
654
+ // if we had [R][T][S][-T][M], then this was a rotated matrix-element
655
+ // if we had [T1][M] we want to transform this into [M][T2]
656
+ // therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
657
+ // down to the element
658
+ } else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
659
+ tlist.getItem(0).type === 2) {
660
+ operation = 2 // translate
661
+ const oldxlate = tlist.getItem(0).matrix
662
+ const meq = transformListToTransform(tlist, 1).matrix
663
+ const meqInv = meq.inverse()
664
+ m = matrixMultiply(meqInv, oldxlate, meq)
665
+ tlist.removeItem(0)
666
+ // else if this child now has a matrix imposition (from a parent group)
667
+ // we might be able to simplify
668
+ } else if (N === 1 && tlist.getItem(0).type === 1 && !angle) {
669
+ // Remap all point-based elements
670
+ m = transformListToTransform(tlist).matrix
671
+ switch (selected.tagName) {
672
+ case 'line':
673
+ changes = {
674
+ x1: selected.getAttribute('x1'),
675
+ y1: selected.getAttribute('y1'),
676
+ x2: selected.getAttribute('x2'),
677
+ y2: selected.getAttribute('y2')
678
+ }
679
+ // Fallthrough
680
+ case 'polyline':
681
+ case 'polygon':
682
+ changes.points = selected.getAttribute('points')
683
+ if (changes.points) {
684
+ const list = selected.points
685
+ const len = list.numberOfItems
686
+ changes.points = new Array(len)
687
+ for (let i = 0; i < len; ++i) {
688
+ const pt = list.getItem(i)
689
+ changes.points[i] = { x: pt.x, y: pt.y }
690
+ }
691
+ }
692
+ // Fallthrough
693
+ case 'path':
694
+ changes.d = selected.getAttribute('d')
695
+ operation = 1
696
+ tlist.clear()
697
+ break
698
+ default:
699
+ break
700
+ }
701
+ // if it was a rotation, put the rotate back and return without a command
702
+ // (this function has zero work to do for a rotate())
703
+ } else {
704
+ // operation = 4; // rotation
705
+ if (angle) {
706
+ const newRot = svgroot.createSVGTransform()
707
+ newRot.setRotate(angle, newcenter.x, newcenter.y)
708
+
709
+ if (tlist.numberOfItems) {
710
+ tlist.insertItemBefore(newRot, 0)
711
+ } else {
712
+ tlist.appendItem(newRot)
713
+ }
714
+ }
715
+ if (tlist.numberOfItems === 0) {
716
+ selected.removeAttribute('transform')
717
+ }
718
+ return null
719
+ }
720
+
721
+ // if it was a translate or resize, we need to remap the element and absorb the xform
722
+ if (operation === 1 || operation === 2 || operation === 3) {
723
+ remapElement(selected, changes, m)
724
+ } // if we are remapping
725
+
726
+ // if it was a translate, put back the rotate at the new center
727
+ if (operation === 2) {
728
+ if (angle) {
729
+ if (!hasMatrixTransform(tlist)) {
730
+ newcenter = {
731
+ x: oldcenter.x + m.e,
732
+ y: oldcenter.y + m.f
733
+ }
734
+ }
735
+ const newRot = svgroot.createSVGTransform()
736
+ newRot.setRotate(angle, newcenter.x, newcenter.y)
737
+ if (tlist.numberOfItems) {
738
+ tlist.insertItemBefore(newRot, 0)
739
+ } else {
740
+ tlist.appendItem(newRot)
741
+ }
742
+ }
743
+ // We have special processing for tspans: Tspans are not transformable
744
+ // but they can have x,y coordinates (sigh). Thus, if this was a translate,
745
+ // on a text element, also translate any tspan children.
746
+ if (selected.tagName === 'text') {
747
+ const children = selected.childNodes
748
+ let c = children.length
749
+ while (c--) {
750
+ const child = children.item(c)
751
+ if (child.tagName === 'tspan') {
752
+ const tspanChanges = {
753
+ x: Number(child.getAttribute('x')) || 0,
754
+ y: Number(child.getAttribute('y')) || 0
755
+ }
756
+ remapElement(child, tspanChanges, m)
757
+ }
758
+ }
759
+ }
760
+ // [Rold][M][T][S][-T] became [Rold][M]
761
+ // we want it to be [Rnew][M][Tr] where Tr is the
762
+ // translation required to re-center it
763
+ // Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]
764
+ } else if (operation === 3 && angle) {
765
+ const { matrix } = transformListToTransform(tlist)
766
+ const roldt = svgroot.createSVGTransform()
767
+ roldt.setRotate(angle, oldcenter.x, oldcenter.y)
768
+ const rold = roldt.matrix
769
+ const rnew = svgroot.createSVGTransform()
770
+ rnew.setRotate(angle, newcenter.x, newcenter.y)
771
+ const rnewInv = rnew.matrix.inverse()
772
+ const mInv = matrix.inverse()
773
+ const extrat = matrixMultiply(mInv, rnewInv, rold, matrix)
774
+
775
+ remapElement(selected, changes, extrat)
776
+ if (angle) {
777
+ if (tlist.numberOfItems) {
778
+ tlist.insertItemBefore(rnew, 0)
779
+ } else {
780
+ tlist.appendItem(rnew)
781
+ }
782
+ }
783
+ }
784
+ } // a non-group
785
+
786
+ // if the transform list has been emptied, remove it
787
+ if (tlist.numberOfItems === 0) {
788
+ selected.removeAttribute('transform')
789
+ }
790
+
791
+ batchCmd.addSubCommand(new ChangeElementCommand(selected, initial))
792
+
793
+ return batchCmd
794
+ }