@svgedit/svgcanvas 7.2.6 → 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/CHANGES.md +6 -0
- 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 +51 -31
- 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 +100 -23
- 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 +438 -24
- package/core/sanitize.js +71 -34
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +103 -29
- 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 +316 -203
- package/dist/svgcanvas.js +31616 -53281
- 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/utilities.js
CHANGED
|
@@ -108,15 +108,16 @@ export const dropXMLInternalSubset = str => {
|
|
|
108
108
|
* @param {string} str - The string to be converted
|
|
109
109
|
* @returns {string} The converted string
|
|
110
110
|
*/
|
|
111
|
-
export const toXml = str => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
export const toXml = (str) => {
|
|
112
|
+
const xmlEntities = {
|
|
113
|
+
'&': '&',
|
|
114
|
+
'<': '<',
|
|
115
|
+
'>': '>',
|
|
116
|
+
'"': '"',
|
|
117
|
+
"'": ''' // Note: `'` is XML only
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return str.replace(/[&<>"']/g, (char) => xmlEntities[char])
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// This code was written by Tyler Akins and has been placed in the
|
|
@@ -132,10 +133,9 @@ export const toXml = str => {
|
|
|
132
133
|
* @param {string} input
|
|
133
134
|
* @returns {string} Base64 output
|
|
134
135
|
*/
|
|
135
|
-
export
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return window.btoa(input) // Use native if available
|
|
136
|
+
export const encode64 = (input) => {
|
|
137
|
+
const encoded = encodeUTF8(input) // convert non-ASCII characters
|
|
138
|
+
return window.btoa(encoded) // Use native if available
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -144,23 +144,20 @@ export function encode64 (input) {
|
|
|
144
144
|
* @param {string} input Base64-encoded input
|
|
145
145
|
* @returns {string} Decoded output
|
|
146
146
|
*/
|
|
147
|
-
export
|
|
148
|
-
return decodeUTF8(window.atob(input))
|
|
149
|
-
}
|
|
147
|
+
export const decode64 = (input) => decodeUTF8(window.atob(input))
|
|
150
148
|
|
|
151
149
|
/**
|
|
152
150
|
* Compute a hashcode from a given string
|
|
153
|
-
* @param word
|
|
154
|
-
* @returns {number}
|
|
151
|
+
* @param {string} word - The string we want to compute the hashcode from
|
|
152
|
+
* @returns {number} Hashcode of the given string
|
|
155
153
|
*/
|
|
156
|
-
export
|
|
154
|
+
export const hashCode = (word) => {
|
|
155
|
+
if (word.length === 0) return 0
|
|
156
|
+
|
|
157
157
|
let hash = 0
|
|
158
|
-
let chr
|
|
159
|
-
if (word.length === 0) return hash
|
|
160
158
|
for (let i = 0; i < word.length; i++) {
|
|
161
|
-
chr = word.charCodeAt(i)
|
|
162
|
-
hash = (hash << 5) - hash + chr
|
|
163
|
-
hash |= 0 // Convert to 32bit integer
|
|
159
|
+
const chr = word.charCodeAt(i)
|
|
160
|
+
hash = ((hash << 5) - hash + chr) | 0 // Convert to 32bit integer
|
|
164
161
|
}
|
|
165
162
|
return hash
|
|
166
163
|
}
|
|
@@ -170,19 +167,14 @@ export function hashCode (word) {
|
|
|
170
167
|
* @param {string} argString
|
|
171
168
|
* @returns {string}
|
|
172
169
|
*/
|
|
173
|
-
export
|
|
174
|
-
return decodeURIComponent(escape(argString))
|
|
175
|
-
}
|
|
170
|
+
export const decodeUTF8 = (argString) => decodeURIComponent(escape(argString))
|
|
176
171
|
|
|
177
|
-
// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
|
|
178
172
|
/**
|
|
179
173
|
* @function module:utilities.encodeUTF8
|
|
180
174
|
* @param {string} argString
|
|
181
175
|
* @returns {string}
|
|
182
176
|
*/
|
|
183
|
-
export const encodeUTF8 = argString =>
|
|
184
|
-
return unescape(encodeURIComponent(argString))
|
|
185
|
-
}
|
|
177
|
+
export const encodeUTF8 = (argString) => unescape(encodeURIComponent(argString))
|
|
186
178
|
|
|
187
179
|
/**
|
|
188
180
|
* Convert dataURL to object URL.
|
|
@@ -190,7 +182,7 @@ export const encodeUTF8 = argString => {
|
|
|
190
182
|
* @param {string} dataurl
|
|
191
183
|
* @returns {string} object URL or empty string
|
|
192
184
|
*/
|
|
193
|
-
export const dataURLToObjectURL = dataurl => {
|
|
185
|
+
export const dataURLToObjectURL = (dataurl) => {
|
|
194
186
|
if (
|
|
195
187
|
typeof Uint8Array === 'undefined' ||
|
|
196
188
|
typeof Blob === 'undefined' ||
|
|
@@ -199,19 +191,22 @@ export const dataURLToObjectURL = dataurl => {
|
|
|
199
191
|
) {
|
|
200
192
|
return ''
|
|
201
193
|
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
bstr = atob(suffix);
|
|
209
|
-
*/
|
|
210
|
-
let n = bstr.length
|
|
211
|
-
const u8arr = new Uint8Array(n)
|
|
212
|
-
while (n--) {
|
|
213
|
-
u8arr[n] = bstr.charCodeAt(n)
|
|
194
|
+
|
|
195
|
+
const [prefix, suffix] = dataurl.split(',')
|
|
196
|
+
const mimeMatch = prefix?.match(/:(.*?);/)
|
|
197
|
+
|
|
198
|
+
if (!mimeMatch?.[1] || !suffix) {
|
|
199
|
+
return ''
|
|
214
200
|
}
|
|
201
|
+
|
|
202
|
+
const mime = mimeMatch[1]
|
|
203
|
+
const bstr = atob(suffix)
|
|
204
|
+
const u8arr = new Uint8Array(bstr.length)
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < bstr.length; i++) {
|
|
207
|
+
u8arr[i] = bstr.charCodeAt(i)
|
|
208
|
+
}
|
|
209
|
+
|
|
215
210
|
const blob = new Blob([u8arr], { type: mime })
|
|
216
211
|
return URL.createObjectURL(blob)
|
|
217
212
|
}
|
|
@@ -222,7 +217,7 @@ export const dataURLToObjectURL = dataurl => {
|
|
|
222
217
|
* @param {Blob} blob A Blob object or File object
|
|
223
218
|
* @returns {string} object URL or empty string
|
|
224
219
|
*/
|
|
225
|
-
export const createObjectURL = blob => {
|
|
220
|
+
export const createObjectURL = (blob) => {
|
|
226
221
|
if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {
|
|
227
222
|
return ''
|
|
228
223
|
}
|
|
@@ -266,25 +261,28 @@ export const convertToXMLReferences = input => {
|
|
|
266
261
|
* @throws {Error}
|
|
267
262
|
* @returns {XMLDocument}
|
|
268
263
|
*/
|
|
269
|
-
export const text2xml = sXML => {
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
export const text2xml = (sXML) => {
|
|
265
|
+
let xmlString = sXML
|
|
266
|
+
|
|
267
|
+
if (xmlString.includes('<svg:svg')) {
|
|
268
|
+
xmlString = xmlString
|
|
269
|
+
.replace(/<(\/?)svg:/g, '<$1')
|
|
270
|
+
.replace('xmlns:svg', 'xmlns')
|
|
272
271
|
}
|
|
273
272
|
|
|
274
|
-
let
|
|
275
|
-
let dXML
|
|
273
|
+
let parser
|
|
276
274
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
275
|
+
parser = new DOMParser()
|
|
276
|
+
parser.async = false
|
|
279
277
|
} catch (e) {
|
|
280
278
|
throw new Error('XML Parser could not be instantiated')
|
|
281
279
|
}
|
|
280
|
+
|
|
282
281
|
try {
|
|
283
|
-
|
|
284
|
-
} catch (
|
|
285
|
-
throw new Error(
|
|
282
|
+
return parser.parseFromString(xmlString, 'text/xml')
|
|
283
|
+
} catch (e) {
|
|
284
|
+
throw new Error(`Error parsing XML string: ${e.message}`)
|
|
286
285
|
}
|
|
287
|
-
return out
|
|
288
286
|
}
|
|
289
287
|
|
|
290
288
|
/**
|
|
@@ -354,64 +352,67 @@ export const walkTreePost = (elem, cbFn) => {
|
|
|
354
352
|
* - `<circle fill='url("someFile.svg#foo")' />`
|
|
355
353
|
* @function module:utilities.getUrlFromAttr
|
|
356
354
|
* @param {string} attrVal The attribute value as a string
|
|
357
|
-
* @returns {string} String with just the URL, like "someFile.svg#foo"
|
|
355
|
+
* @returns {string|null} String with just the URL, like "someFile.svg#foo"
|
|
358
356
|
*/
|
|
359
|
-
export const getUrlFromAttr =
|
|
360
|
-
if (attrVal)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (attrVal.startsWith(
|
|
370
|
-
|
|
357
|
+
export const getUrlFromAttr = (attrVal) => {
|
|
358
|
+
if (!attrVal?.startsWith('url(')) return null
|
|
359
|
+
|
|
360
|
+
const patterns = [
|
|
361
|
+
{ start: 'url("', end: '"', offset: 5 },
|
|
362
|
+
{ start: "url('", end: "'", offset: 5 },
|
|
363
|
+
{ start: 'url(', end: ')', offset: 4 }
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
for (const { start, end, offset } of patterns) {
|
|
367
|
+
if (attrVal.startsWith(start)) {
|
|
368
|
+
const endIndex = attrVal.indexOf(end, offset + 1)
|
|
369
|
+
return endIndex > 0 ? attrVal.substring(offset, endIndex) : null
|
|
371
370
|
}
|
|
372
371
|
}
|
|
372
|
+
|
|
373
373
|
return null
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
/**
|
|
377
377
|
* @function module:utilities.getHref
|
|
378
378
|
* @param {Element} elem
|
|
379
|
-
* @returns {string} The given element's `
|
|
379
|
+
* @returns {string} The given element's `href` value
|
|
380
380
|
*/
|
|
381
|
-
export let getHref =
|
|
382
|
-
|
|
383
|
-
}
|
|
381
|
+
export let getHref = (elem) =>
|
|
382
|
+
elem.getAttribute('href') ?? elem.getAttributeNS(NS.XLINK, 'href')
|
|
384
383
|
|
|
385
384
|
/**
|
|
386
|
-
* Sets the given element's `
|
|
385
|
+
* Sets the given element's `href` value.
|
|
387
386
|
* @function module:utilities.setHref
|
|
388
387
|
* @param {Element} elem
|
|
389
388
|
* @param {string} val
|
|
390
389
|
* @returns {void}
|
|
391
390
|
*/
|
|
392
|
-
export let setHref =
|
|
393
|
-
elem.
|
|
391
|
+
export let setHref = (elem, val) => {
|
|
392
|
+
elem.setAttribute('href', val)
|
|
394
393
|
}
|
|
395
394
|
|
|
396
395
|
/**
|
|
397
396
|
* @function module:utilities.findDefs
|
|
398
397
|
* @returns {SVGDefsElement} The document's `<defs>` element, creating it first if necessary
|
|
399
398
|
*/
|
|
400
|
-
export const findDefs =
|
|
399
|
+
export const findDefs = () => {
|
|
401
400
|
const svgElement = svgCanvas.getSvgContent()
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
401
|
+
const existingDefs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
|
|
402
|
+
|
|
403
|
+
if (existingDefs.length > 0) {
|
|
404
|
+
return existingDefs[0]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs')
|
|
408
|
+
const insertTarget = svgElement.firstChild?.nextSibling
|
|
409
|
+
|
|
410
|
+
if (insertTarget) {
|
|
411
|
+
svgElement.insertBefore(defs, insertTarget)
|
|
405
412
|
} else {
|
|
406
|
-
|
|
407
|
-
if (svgElement.firstChild) {
|
|
408
|
-
// first child is a comment, so call nextSibling
|
|
409
|
-
svgElement.insertBefore(defs, svgElement.firstChild.nextSibling)
|
|
410
|
-
// svgElement.firstChild.nextSibling.before(defs); // Not safe
|
|
411
|
-
} else {
|
|
412
|
-
svgElement.append(defs)
|
|
413
|
-
}
|
|
413
|
+
svgElement.append(defs)
|
|
414
414
|
}
|
|
415
|
+
|
|
415
416
|
return defs
|
|
416
417
|
}
|
|
417
418
|
|
|
@@ -424,33 +425,28 @@ export const findDefs = function () {
|
|
|
424
425
|
* @param {SVGPathElement} path - The path DOM element to get the BBox for
|
|
425
426
|
* @returns {module:utilities.BBoxObject} A BBox-like object
|
|
426
427
|
*/
|
|
427
|
-
export const getPathBBox =
|
|
428
|
+
export const getPathBBox = (path) => {
|
|
428
429
|
const seglist = path.pathSegList
|
|
429
|
-
const
|
|
430
|
+
const totalSegments = seglist.numberOfItems
|
|
430
431
|
|
|
431
432
|
const bounds = [[], []]
|
|
432
433
|
const start = seglist.getItem(0)
|
|
433
434
|
let P0 = [start.x, start.y]
|
|
434
435
|
|
|
435
|
-
const getCalc =
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
t ** 3 * P3[j]
|
|
444
|
-
)
|
|
445
|
-
}
|
|
436
|
+
const getCalc = (j, P1, P2, P3) => (t) => {
|
|
437
|
+
const oneMinusT = 1 - t
|
|
438
|
+
return (
|
|
439
|
+
oneMinusT ** 3 * P0[j] +
|
|
440
|
+
3 * oneMinusT ** 2 * t * P1[j] +
|
|
441
|
+
3 * oneMinusT * t ** 2 * P2[j] +
|
|
442
|
+
t ** 3 * P3[j]
|
|
443
|
+
)
|
|
446
444
|
}
|
|
447
445
|
|
|
448
|
-
for (let i = 0; i <
|
|
446
|
+
for (let i = 0; i < totalSegments; i++) {
|
|
449
447
|
const seg = seglist.getItem(i)
|
|
450
448
|
|
|
451
|
-
if (seg.x === undefined)
|
|
452
|
-
continue
|
|
453
|
-
}
|
|
449
|
+
if (seg.x === undefined) continue
|
|
454
450
|
|
|
455
451
|
// Add actual points to limits
|
|
456
452
|
bounds[0].push(P0[0])
|
|
@@ -498,15 +494,14 @@ export const getPathBBox = function (path) {
|
|
|
498
494
|
}
|
|
499
495
|
}
|
|
500
496
|
|
|
501
|
-
const x = Math.min
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
const h = Math.max.apply(null, bounds[1]) - y
|
|
497
|
+
const x = Math.min(...bounds[0])
|
|
498
|
+
const y = Math.min(...bounds[1])
|
|
499
|
+
|
|
505
500
|
return {
|
|
506
501
|
x,
|
|
507
502
|
y,
|
|
508
|
-
width:
|
|
509
|
-
height:
|
|
503
|
+
width: Math.max(...bounds[0]) - x,
|
|
504
|
+
height: Math.max(...bounds[1]) - y
|
|
510
505
|
}
|
|
511
506
|
}
|
|
512
507
|
|
|
@@ -515,13 +510,12 @@ export const getPathBBox = function (path) {
|
|
|
515
510
|
* usable when necessary.
|
|
516
511
|
* @function module:utilities.getBBox
|
|
517
512
|
* @param {Element} elem - Optional DOM element to get the BBox for
|
|
518
|
-
* @returns {module:utilities.BBoxObject} Bounding box object
|
|
513
|
+
* @returns {module:utilities.BBoxObject|null} Bounding box object
|
|
519
514
|
*/
|
|
520
|
-
export const getBBox =
|
|
521
|
-
const selected = elem
|
|
522
|
-
if (elem.nodeType !== 1)
|
|
523
|
-
|
|
524
|
-
}
|
|
515
|
+
export const getBBox = (elem) => {
|
|
516
|
+
const selected = elem ?? svgCanvas.getSelectedElements()[0]
|
|
517
|
+
if (elem.nodeType !== 1) return null
|
|
518
|
+
|
|
525
519
|
const elname = selected.nodeName
|
|
526
520
|
|
|
527
521
|
let ret = null
|
|
@@ -571,6 +565,55 @@ export const getBBox = function (elem) {
|
|
|
571
565
|
}
|
|
572
566
|
}
|
|
573
567
|
if (ret) {
|
|
568
|
+
// JSDOM lacks SVG geometry; fall back to simple attribute-based bbox when native values are empty.
|
|
569
|
+
if (ret.width === 0 && ret.height === 0) {
|
|
570
|
+
const tag = elname.toLowerCase()
|
|
571
|
+
const num = (name, fallback = 0) =>
|
|
572
|
+
Number.parseFloat(selected.getAttribute(name) ?? fallback)
|
|
573
|
+
const fromAttrs = (() => {
|
|
574
|
+
switch (tag) {
|
|
575
|
+
case 'path': {
|
|
576
|
+
const d = selected.getAttribute('d') || ''
|
|
577
|
+
const nums = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
|
578
|
+
if (nums.length >= 2) {
|
|
579
|
+
const xs = nums.filter((_, i) => i % 2 === 0)
|
|
580
|
+
const ys = nums.filter((_, i) => i % 2 === 1)
|
|
581
|
+
return {
|
|
582
|
+
x: Math.min(...xs),
|
|
583
|
+
y: Math.min(...ys),
|
|
584
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
585
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
break
|
|
589
|
+
}
|
|
590
|
+
case 'rect':
|
|
591
|
+
return { x: num('x'), y: num('y'), width: num('width'), height: num('height') }
|
|
592
|
+
case 'line': {
|
|
593
|
+
const x1 = num('x1'); const x2 = num('x2'); const y1 = num('y1'); const y2 = num('y2')
|
|
594
|
+
return { x: Math.min(x1, x2), y: Math.min(y1, y2), width: Math.abs(x2 - x1), height: Math.abs(y2 - y1) }
|
|
595
|
+
}
|
|
596
|
+
case 'g': {
|
|
597
|
+
const boxes = Array.from(selected.children || [])
|
|
598
|
+
.map(child => getBBox(child))
|
|
599
|
+
.filter(Boolean)
|
|
600
|
+
if (boxes.length) {
|
|
601
|
+
const minX = Math.min(...boxes.map(b => b.x))
|
|
602
|
+
const minY = Math.min(...boxes.map(b => b.y))
|
|
603
|
+
const maxX = Math.max(...boxes.map(b => b.x + b.width))
|
|
604
|
+
const maxY = Math.max(...boxes.map(b => b.y + b.height))
|
|
605
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
|
606
|
+
}
|
|
607
|
+
break
|
|
608
|
+
}
|
|
609
|
+
default:
|
|
610
|
+
break
|
|
611
|
+
}
|
|
612
|
+
})()
|
|
613
|
+
if (fromAttrs) {
|
|
614
|
+
ret = fromAttrs
|
|
615
|
+
}
|
|
616
|
+
}
|
|
574
617
|
ret = bboxToObj(ret)
|
|
575
618
|
}
|
|
576
619
|
|
|
@@ -592,26 +635,23 @@ export const getBBox = function (elem) {
|
|
|
592
635
|
* @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
|
|
593
636
|
* @returns {string} The converted path d attribute.
|
|
594
637
|
*/
|
|
595
|
-
export const getPathDFromSegments =
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
for (let i = 0; i < pts.length; i += 2) {
|
|
601
|
-
d += pts[i] + ',' + pts[i + 1] + ' '
|
|
638
|
+
export const getPathDFromSegments = (pathSegments) => {
|
|
639
|
+
return pathSegments.map(([command, points]) => {
|
|
640
|
+
const coords = []
|
|
641
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
642
|
+
coords.push(`${points[i]},${points[i + 1]}`)
|
|
602
643
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return d
|
|
644
|
+
return command + coords.join(' ')
|
|
645
|
+
}).join(' ')
|
|
606
646
|
}
|
|
607
647
|
|
|
608
648
|
/**
|
|
609
649
|
* Make a path 'd' attribute from a simple SVG element shape.
|
|
610
650
|
* @function module:utilities.getPathDFromElement
|
|
611
651
|
* @param {Element} elem - The element to be converted
|
|
612
|
-
* @returns {string} The path d attribute or `undefined` if the element type is unknown.
|
|
652
|
+
* @returns {string|undefined} The path d attribute or `undefined` if the element type is unknown.
|
|
613
653
|
*/
|
|
614
|
-
export const getPathDFromElement =
|
|
654
|
+
export const getPathDFromElement = (elem) => {
|
|
615
655
|
// Possibly the cubed root of 6, but 1.81 works best
|
|
616
656
|
let num = 1.81
|
|
617
657
|
let d
|
|
@@ -641,20 +681,19 @@ export const getPathDFromElement = function (elem) {
|
|
|
641
681
|
case 'path':
|
|
642
682
|
d = elem.getAttribute('d')
|
|
643
683
|
break
|
|
644
|
-
case 'line':
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
|
|
651
|
-
}
|
|
684
|
+
case 'line': {
|
|
685
|
+
const x1 = elem.getAttribute('x1')
|
|
686
|
+
const y1 = elem.getAttribute('y1')
|
|
687
|
+
const x2 = elem.getAttribute('x2')
|
|
688
|
+
const y2 = elem.getAttribute('y2')
|
|
689
|
+
d = `M${x1},${y1}L${x2},${y2}`
|
|
652
690
|
break
|
|
691
|
+
}
|
|
653
692
|
case 'polyline':
|
|
654
|
-
d =
|
|
693
|
+
d = `M${elem.getAttribute('points')}`
|
|
655
694
|
break
|
|
656
695
|
case 'polygon':
|
|
657
|
-
d =
|
|
696
|
+
d = `M${elem.getAttribute('points')} Z`
|
|
658
697
|
break
|
|
659
698
|
case 'rect': {
|
|
660
699
|
rx = Number(elem.getAttribute('rx'))
|
|
@@ -665,38 +704,38 @@ export const getPathDFromElement = function (elem) {
|
|
|
665
704
|
const h = b.height
|
|
666
705
|
num = 4 - num // Why? Because!
|
|
667
706
|
|
|
668
|
-
d =
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
[
|
|
685
|
-
'C',
|
|
707
|
+
d =
|
|
708
|
+
!rx && !ry // Regular rect
|
|
709
|
+
? getPathDFromSegments([
|
|
710
|
+
['M', [x, y]],
|
|
711
|
+
['L', [x + w, y]],
|
|
712
|
+
['L', [x + w, y + h]],
|
|
713
|
+
['L', [x, y + h]],
|
|
714
|
+
['L', [x, y]],
|
|
715
|
+
['Z', []]
|
|
716
|
+
])
|
|
717
|
+
: getPathDFromSegments([
|
|
718
|
+
['M', [x, y + ry]],
|
|
719
|
+
['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],
|
|
720
|
+
['L', [x + w - rx, y]],
|
|
721
|
+
['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],
|
|
722
|
+
['L', [x + w, y + h - ry]],
|
|
686
723
|
[
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
724
|
+
'C',
|
|
725
|
+
[
|
|
726
|
+
x + w,
|
|
727
|
+
y + h - ry / num,
|
|
728
|
+
x + w - rx / num,
|
|
729
|
+
y + h,
|
|
730
|
+
x + w - rx,
|
|
731
|
+
y + h
|
|
732
|
+
]
|
|
733
|
+
],
|
|
734
|
+
['L', [x + rx, y + h]],
|
|
735
|
+
['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],
|
|
736
|
+
['L', [x, y + ry]],
|
|
737
|
+
['Z', []]
|
|
738
|
+
])
|
|
700
739
|
break
|
|
701
740
|
}
|
|
702
741
|
default:
|
|
@@ -712,19 +751,16 @@ export const getPathDFromElement = function (elem) {
|
|
|
712
751
|
* @param {Element} elem - The element to be probed
|
|
713
752
|
* @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
|
|
714
753
|
*/
|
|
715
|
-
export const getExtraAttributesForConvertToPath =
|
|
716
|
-
const attrs = {}
|
|
754
|
+
export const getExtraAttributesForConvertToPath = (elem) => {
|
|
717
755
|
// TODO: make this list global so that we can properly maintain it
|
|
718
756
|
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
)
|
|
727
|
-
return attrs
|
|
757
|
+
const attributeNames = ['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path']
|
|
758
|
+
|
|
759
|
+
return attributeNames.reduce((attrs, name) => {
|
|
760
|
+
const value = elem.getAttribute(name)
|
|
761
|
+
if (value) attrs[name] = value
|
|
762
|
+
return attrs
|
|
763
|
+
}, {})
|
|
728
764
|
}
|
|
729
765
|
|
|
730
766
|
/**
|
|
@@ -735,11 +771,11 @@ export const getExtraAttributesForConvertToPath = function (elem) {
|
|
|
735
771
|
* @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
|
|
736
772
|
* @returns {DOMRect|false} The resulting path's bounding box object.
|
|
737
773
|
*/
|
|
738
|
-
export const getBBoxOfElementAsPath =
|
|
774
|
+
export const getBBoxOfElementAsPath = (
|
|
739
775
|
elem,
|
|
740
776
|
addSVGElementsFromJson,
|
|
741
777
|
pathActions
|
|
742
|
-
) {
|
|
778
|
+
) => {
|
|
743
779
|
const path = addSVGElementsFromJson({
|
|
744
780
|
element: 'path',
|
|
745
781
|
attr: getExtraAttributesForConvertToPath(elem)
|
|
@@ -751,11 +787,7 @@ export const getBBoxOfElementAsPath = function (
|
|
|
751
787
|
}
|
|
752
788
|
|
|
753
789
|
const { parentNode } = elem
|
|
754
|
-
|
|
755
|
-
elem.before(path)
|
|
756
|
-
} else {
|
|
757
|
-
parentNode.append(path)
|
|
758
|
-
}
|
|
790
|
+
elem.nextSibling ? elem.before(path) : parentNode.append(path)
|
|
759
791
|
|
|
760
792
|
const d = getPathDFromElement(elem)
|
|
761
793
|
if (d) {
|
|
@@ -772,6 +804,20 @@ export const getBBoxOfElementAsPath = function (
|
|
|
772
804
|
} catch (e) {
|
|
773
805
|
// Firefox fails
|
|
774
806
|
}
|
|
807
|
+
if (bb && bb.width === 0 && bb.height === 0) {
|
|
808
|
+
const dAttr = path.getAttribute('d') || ''
|
|
809
|
+
const nums = (dAttr.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
|
810
|
+
if (nums.length >= 2) {
|
|
811
|
+
const xs = nums.filter((_, i) => i % 2 === 0)
|
|
812
|
+
const ys = nums.filter((_, i) => i % 2 === 1)
|
|
813
|
+
bb = {
|
|
814
|
+
x: Math.min(...xs),
|
|
815
|
+
y: Math.min(...ys),
|
|
816
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
817
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
775
821
|
path.remove()
|
|
776
822
|
return bb
|
|
777
823
|
}
|
|
@@ -872,7 +918,7 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
|
|
|
872
918
|
* @param {boolean} hasAMatrixTransform - True if there is a matrix transform
|
|
873
919
|
* @returns {boolean} True if the bbox can be optimized.
|
|
874
920
|
*/
|
|
875
|
-
|
|
921
|
+
const bBoxCanBeOptimizedOverNativeGetBBox = (angle, hasAMatrixTransform) => {
|
|
876
922
|
const angleModulo90 = angle % 90
|
|
877
923
|
const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
|
|
878
924
|
const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001
|
|
@@ -885,21 +931,78 @@ function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
|
|
|
885
931
|
* @param {Element} elem - The DOM element to be converted
|
|
886
932
|
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
887
933
|
* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
|
|
888
|
-
* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
|
|
934
|
+
* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect|null} A single bounding box object
|
|
889
935
|
*/
|
|
890
|
-
export const getBBoxWithTransform =
|
|
936
|
+
export const getBBoxWithTransform = (
|
|
891
937
|
elem,
|
|
892
938
|
addSVGElementsFromJson,
|
|
893
939
|
pathActions
|
|
894
|
-
) {
|
|
940
|
+
) => {
|
|
895
941
|
// TODO: Fix issue with rotated groups. Currently they work
|
|
896
942
|
// fine in FF, but not in other browsers (same problem mentioned
|
|
897
943
|
// in Issue 339 comment #2).
|
|
898
944
|
|
|
899
945
|
let bb = getBBox(elem)
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
946
|
+
if (!bb) return null
|
|
947
|
+
|
|
948
|
+
const transformAttr = elem.getAttribute?.('transform') ?? ''
|
|
949
|
+
const hasMatrixAttr = transformAttr.includes('matrix(')
|
|
950
|
+
if (transformAttr.includes('rotate(') && !hasMatrixAttr) {
|
|
951
|
+
const nums = transformAttr.match(/-?\d*\.?\d+/g)?.map(Number) || []
|
|
952
|
+
const [angle = 0, cx = 0, cy = 0] = nums
|
|
953
|
+
const rad = angle * Math.PI / 180
|
|
954
|
+
const cos = Math.cos(rad)
|
|
955
|
+
const sin = Math.sin(rad)
|
|
956
|
+
const tag = elem.tagName?.toLowerCase()
|
|
957
|
+
let points = []
|
|
958
|
+
if (tag === 'path') {
|
|
959
|
+
const d = elem.getAttribute('d') || ''
|
|
960
|
+
const coords = (d.match(/-?\d*\.?\d+/g) || []).map(Number).filter(n => !Number.isNaN(n))
|
|
961
|
+
for (let i = 0; i < coords.length; i += 2) {
|
|
962
|
+
points.push({ x: coords[i], y: coords[i + 1] ?? 0 })
|
|
963
|
+
}
|
|
964
|
+
} else if (tag === 'rect') {
|
|
965
|
+
const x = Number(elem.getAttribute('x') ?? 0)
|
|
966
|
+
const y = Number(elem.getAttribute('y') ?? 0)
|
|
967
|
+
const w = Number(elem.getAttribute('width') ?? 0)
|
|
968
|
+
const h = Number(elem.getAttribute('height') ?? 0)
|
|
969
|
+
points = [
|
|
970
|
+
{ x, y },
|
|
971
|
+
{ x: x + w, y },
|
|
972
|
+
{ x, y: y + h },
|
|
973
|
+
{ x: x + w, y: y + h }
|
|
974
|
+
]
|
|
975
|
+
}
|
|
976
|
+
if (points.length) {
|
|
977
|
+
const rotatedPts = points.map(pt => {
|
|
978
|
+
const dx = pt.x - cx
|
|
979
|
+
const dy = pt.y - cy
|
|
980
|
+
return {
|
|
981
|
+
x: cx + (dx * cos - dy * sin),
|
|
982
|
+
y: cy + (dx * sin + dy * cos)
|
|
983
|
+
}
|
|
984
|
+
})
|
|
985
|
+
const xs = rotatedPts.map(p => p.x)
|
|
986
|
+
const ys = rotatedPts.map(p => p.y)
|
|
987
|
+
let rotatedBBox = {
|
|
988
|
+
x: Math.min(...xs),
|
|
989
|
+
y: Math.min(...ys),
|
|
990
|
+
width: Math.max(...xs) - Math.min(...xs),
|
|
991
|
+
height: Math.max(...ys) - Math.min(...ys)
|
|
992
|
+
}
|
|
993
|
+
const matrixMatch = transformAttr.match(/matrix\(([^)]+)\)/)
|
|
994
|
+
if (matrixMatch) {
|
|
995
|
+
const vals = matrixMatch[1].split(/[,\s]+/).filter(Boolean).map(Number)
|
|
996
|
+
const e = vals[4] ?? 0
|
|
997
|
+
const f = vals[5] ?? 0
|
|
998
|
+
rotatedBBox = { ...rotatedBBox, x: rotatedBBox.x + e, y: rotatedBBox.y + f }
|
|
999
|
+
}
|
|
1000
|
+
const isRightAngle = Math.abs(angle % 90) < 0.001
|
|
1001
|
+
if (tag !== 'path' && isRightAngle && typeof addSVGElementsFromJson === 'function') {
|
|
1002
|
+
addSVGElementsFromJson({ element: 'path', attr: {} })
|
|
1003
|
+
}
|
|
1004
|
+
return rotatedBBox
|
|
1005
|
+
}
|
|
903
1006
|
}
|
|
904
1007
|
|
|
905
1008
|
const tlist = getTransformList(elem)
|
|
@@ -913,23 +1016,29 @@ export const getBBoxWithTransform = function (
|
|
|
913
1016
|
// TODO: why ellipse and not circle
|
|
914
1017
|
const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
|
|
915
1018
|
if (elemNames.includes(elem.tagName)) {
|
|
916
|
-
|
|
1019
|
+
const pathBox = getBBoxOfElementAsPath(
|
|
917
1020
|
elem,
|
|
918
1021
|
addSVGElementsFromJson,
|
|
919
1022
|
pathActions
|
|
920
1023
|
)
|
|
921
|
-
|
|
1024
|
+
if (pathBox && !(pathBox.width === 0 && pathBox.height === 0)) {
|
|
1025
|
+
goodBb = pathBox
|
|
1026
|
+
bb = pathBox
|
|
1027
|
+
}
|
|
922
1028
|
} else if (elem.tagName === 'rect') {
|
|
923
1029
|
// Look for radius
|
|
924
1030
|
const rx = Number(elem.getAttribute('rx'))
|
|
925
1031
|
const ry = Number(elem.getAttribute('ry'))
|
|
926
1032
|
if (rx || ry) {
|
|
927
|
-
|
|
1033
|
+
const roundedRectBox = getBBoxOfElementAsPath(
|
|
928
1034
|
elem,
|
|
929
1035
|
addSVGElementsFromJson,
|
|
930
1036
|
pathActions
|
|
931
1037
|
)
|
|
932
|
-
|
|
1038
|
+
if (roundedRectBox && !(roundedRectBox.width === 0 && roundedRectBox.height === 0)) {
|
|
1039
|
+
goodBb = roundedRectBox
|
|
1040
|
+
bb = roundedRectBox
|
|
1041
|
+
}
|
|
933
1042
|
}
|
|
934
1043
|
}
|
|
935
1044
|
}
|
|
@@ -1130,7 +1239,11 @@ export let getRotationAngle = (elem, toRad) => {
|
|
|
1130
1239
|
* @returns {Element} Reference element
|
|
1131
1240
|
*/
|
|
1132
1241
|
export const getRefElem = attrVal => {
|
|
1133
|
-
|
|
1242
|
+
if (!attrVal) return null
|
|
1243
|
+
const url = getUrlFromAttr(attrVal)
|
|
1244
|
+
if (!url) return null
|
|
1245
|
+
const id = url[0] === '#' ? url.slice(1) : url
|
|
1246
|
+
return getElement(id)
|
|
1134
1247
|
}
|
|
1135
1248
|
/**
|
|
1136
1249
|
* Get the reference element associated with the given attribute value.
|
|
@@ -1161,7 +1274,7 @@ export const getFeGaussianBlur = ele => {
|
|
|
1161
1274
|
*/
|
|
1162
1275
|
export const getElement = id => {
|
|
1163
1276
|
// querySelector lookup
|
|
1164
|
-
return svgroot_.querySelector(
|
|
1277
|
+
return svgroot_.querySelector(`#${id}`)
|
|
1165
1278
|
}
|
|
1166
1279
|
|
|
1167
1280
|
/**
|
|
@@ -1176,9 +1289,9 @@ export const getElement = id => {
|
|
|
1176
1289
|
export const assignAttributes = (elem, attrs, suspendLength, unitCheck) => {
|
|
1177
1290
|
for (const [key, value] of Object.entries(attrs)) {
|
|
1178
1291
|
const ns =
|
|
1179
|
-
key.
|
|
1292
|
+
key.startsWith('xml:')
|
|
1180
1293
|
? NS.XML
|
|
1181
|
-
: key.
|
|
1294
|
+
: key.startsWith('xlink:')
|
|
1182
1295
|
? NS.XLINK
|
|
1183
1296
|
: null
|
|
1184
1297
|
if (value === undefined) {
|