@svgedit/svgcanvas 7.2.7 → 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/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 +50 -30
- 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 +46 -14
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- 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 +282 -170
- package/dist/svgcanvas.js +31590 -53383
- 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,22 +352,24 @@ 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
|
|
|
@@ -378,10 +378,8 @@ export const getUrlFromAttr = function (attrVal) {
|
|
|
378
378
|
* @param {Element} elem
|
|
379
379
|
* @returns {string} The given element's `href` value
|
|
380
380
|
*/
|
|
381
|
-
export let getHref =
|
|
382
|
-
|
|
383
|
-
return elem.getAttribute('href') || elem.getAttributeNS(NS.XLINK, 'href')
|
|
384
|
-
}
|
|
381
|
+
export let getHref = (elem) =>
|
|
382
|
+
elem.getAttribute('href') ?? elem.getAttributeNS(NS.XLINK, 'href')
|
|
385
383
|
|
|
386
384
|
/**
|
|
387
385
|
* Sets the given element's `href` value.
|
|
@@ -390,7 +388,7 @@ export let getHref = function (elem) {
|
|
|
390
388
|
* @param {string} val
|
|
391
389
|
* @returns {void}
|
|
392
390
|
*/
|
|
393
|
-
export let setHref =
|
|
391
|
+
export let setHref = (elem, val) => {
|
|
394
392
|
elem.setAttribute('href', val)
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -398,21 +396,23 @@ export let setHref = function (elem, val) {
|
|
|
398
396
|
* @function module:utilities.findDefs
|
|
399
397
|
* @returns {SVGDefsElement} The document's `<defs>` element, creating it first if necessary
|
|
400
398
|
*/
|
|
401
|
-
export const findDefs =
|
|
399
|
+
export const findDefs = () => {
|
|
402
400
|
const svgElement = svgCanvas.getSvgContent()
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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)
|
|
406
412
|
} else {
|
|
407
|
-
|
|
408
|
-
if (svgElement.firstChild) {
|
|
409
|
-
// first child is a comment, so call nextSibling
|
|
410
|
-
svgElement.insertBefore(defs, svgElement.firstChild.nextSibling)
|
|
411
|
-
// svgElement.firstChild.nextSibling.before(defs); // Not safe
|
|
412
|
-
} else {
|
|
413
|
-
svgElement.append(defs)
|
|
414
|
-
}
|
|
413
|
+
svgElement.append(defs)
|
|
415
414
|
}
|
|
415
|
+
|
|
416
416
|
return defs
|
|
417
417
|
}
|
|
418
418
|
|
|
@@ -425,33 +425,28 @@ export const findDefs = function () {
|
|
|
425
425
|
* @param {SVGPathElement} path - The path DOM element to get the BBox for
|
|
426
426
|
* @returns {module:utilities.BBoxObject} A BBox-like object
|
|
427
427
|
*/
|
|
428
|
-
export const getPathBBox =
|
|
428
|
+
export const getPathBBox = (path) => {
|
|
429
429
|
const seglist = path.pathSegList
|
|
430
|
-
const
|
|
430
|
+
const totalSegments = seglist.numberOfItems
|
|
431
431
|
|
|
432
432
|
const bounds = [[], []]
|
|
433
433
|
const start = seglist.getItem(0)
|
|
434
434
|
let P0 = [start.x, start.y]
|
|
435
435
|
|
|
436
|
-
const getCalc =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
t ** 3 * P3[j]
|
|
445
|
-
)
|
|
446
|
-
}
|
|
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
|
+
)
|
|
447
444
|
}
|
|
448
445
|
|
|
449
|
-
for (let i = 0; i <
|
|
446
|
+
for (let i = 0; i < totalSegments; i++) {
|
|
450
447
|
const seg = seglist.getItem(i)
|
|
451
448
|
|
|
452
|
-
if (seg.x === undefined)
|
|
453
|
-
continue
|
|
454
|
-
}
|
|
449
|
+
if (seg.x === undefined) continue
|
|
455
450
|
|
|
456
451
|
// Add actual points to limits
|
|
457
452
|
bounds[0].push(P0[0])
|
|
@@ -499,15 +494,14 @@ export const getPathBBox = function (path) {
|
|
|
499
494
|
}
|
|
500
495
|
}
|
|
501
496
|
|
|
502
|
-
const x = Math.min
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
const h = Math.max.apply(null, bounds[1]) - y
|
|
497
|
+
const x = Math.min(...bounds[0])
|
|
498
|
+
const y = Math.min(...bounds[1])
|
|
499
|
+
|
|
506
500
|
return {
|
|
507
501
|
x,
|
|
508
502
|
y,
|
|
509
|
-
width:
|
|
510
|
-
height:
|
|
503
|
+
width: Math.max(...bounds[0]) - x,
|
|
504
|
+
height: Math.max(...bounds[1]) - y
|
|
511
505
|
}
|
|
512
506
|
}
|
|
513
507
|
|
|
@@ -516,13 +510,12 @@ export const getPathBBox = function (path) {
|
|
|
516
510
|
* usable when necessary.
|
|
517
511
|
* @function module:utilities.getBBox
|
|
518
512
|
* @param {Element} elem - Optional DOM element to get the BBox for
|
|
519
|
-
* @returns {module:utilities.BBoxObject} Bounding box object
|
|
513
|
+
* @returns {module:utilities.BBoxObject|null} Bounding box object
|
|
520
514
|
*/
|
|
521
|
-
export const getBBox =
|
|
522
|
-
const selected = elem
|
|
523
|
-
if (elem.nodeType !== 1)
|
|
524
|
-
|
|
525
|
-
}
|
|
515
|
+
export const getBBox = (elem) => {
|
|
516
|
+
const selected = elem ?? svgCanvas.getSelectedElements()[0]
|
|
517
|
+
if (elem.nodeType !== 1) return null
|
|
518
|
+
|
|
526
519
|
const elname = selected.nodeName
|
|
527
520
|
|
|
528
521
|
let ret = null
|
|
@@ -572,6 +565,55 @@ export const getBBox = function (elem) {
|
|
|
572
565
|
}
|
|
573
566
|
}
|
|
574
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
|
+
}
|
|
575
617
|
ret = bboxToObj(ret)
|
|
576
618
|
}
|
|
577
619
|
|
|
@@ -593,26 +635,23 @@ export const getBBox = function (elem) {
|
|
|
593
635
|
* @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
|
|
594
636
|
* @returns {string} The converted path d attribute.
|
|
595
637
|
*/
|
|
596
|
-
export const getPathDFromSegments =
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
for (let i = 0; i < pts.length; i += 2) {
|
|
602
|
-
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]}`)
|
|
603
643
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return d
|
|
644
|
+
return command + coords.join(' ')
|
|
645
|
+
}).join(' ')
|
|
607
646
|
}
|
|
608
647
|
|
|
609
648
|
/**
|
|
610
649
|
* Make a path 'd' attribute from a simple SVG element shape.
|
|
611
650
|
* @function module:utilities.getPathDFromElement
|
|
612
651
|
* @param {Element} elem - The element to be converted
|
|
613
|
-
* @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.
|
|
614
653
|
*/
|
|
615
|
-
export const getPathDFromElement =
|
|
654
|
+
export const getPathDFromElement = (elem) => {
|
|
616
655
|
// Possibly the cubed root of 6, but 1.81 works best
|
|
617
656
|
let num = 1.81
|
|
618
657
|
let d
|
|
@@ -642,20 +681,19 @@ export const getPathDFromElement = function (elem) {
|
|
|
642
681
|
case 'path':
|
|
643
682
|
d = elem.getAttribute('d')
|
|
644
683
|
break
|
|
645
|
-
case 'line':
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
|
|
652
|
-
}
|
|
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}`
|
|
653
690
|
break
|
|
691
|
+
}
|
|
654
692
|
case 'polyline':
|
|
655
|
-
d =
|
|
693
|
+
d = `M${elem.getAttribute('points')}`
|
|
656
694
|
break
|
|
657
695
|
case 'polygon':
|
|
658
|
-
d =
|
|
696
|
+
d = `M${elem.getAttribute('points')} Z`
|
|
659
697
|
break
|
|
660
698
|
case 'rect': {
|
|
661
699
|
rx = Number(elem.getAttribute('rx'))
|
|
@@ -713,19 +751,16 @@ export const getPathDFromElement = function (elem) {
|
|
|
713
751
|
* @param {Element} elem - The element to be probed
|
|
714
752
|
* @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
|
|
715
753
|
*/
|
|
716
|
-
export const getExtraAttributesForConvertToPath =
|
|
717
|
-
const attrs = {}
|
|
754
|
+
export const getExtraAttributesForConvertToPath = (elem) => {
|
|
718
755
|
// TODO: make this list global so that we can properly maintain it
|
|
719
756
|
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
728
|
-
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
|
+
}, {})
|
|
729
764
|
}
|
|
730
765
|
|
|
731
766
|
/**
|
|
@@ -736,11 +771,11 @@ export const getExtraAttributesForConvertToPath = function (elem) {
|
|
|
736
771
|
* @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
|
|
737
772
|
* @returns {DOMRect|false} The resulting path's bounding box object.
|
|
738
773
|
*/
|
|
739
|
-
export const getBBoxOfElementAsPath =
|
|
774
|
+
export const getBBoxOfElementAsPath = (
|
|
740
775
|
elem,
|
|
741
776
|
addSVGElementsFromJson,
|
|
742
777
|
pathActions
|
|
743
|
-
) {
|
|
778
|
+
) => {
|
|
744
779
|
const path = addSVGElementsFromJson({
|
|
745
780
|
element: 'path',
|
|
746
781
|
attr: getExtraAttributesForConvertToPath(elem)
|
|
@@ -752,11 +787,7 @@ export const getBBoxOfElementAsPath = function (
|
|
|
752
787
|
}
|
|
753
788
|
|
|
754
789
|
const { parentNode } = elem
|
|
755
|
-
|
|
756
|
-
elem.before(path)
|
|
757
|
-
} else {
|
|
758
|
-
parentNode.append(path)
|
|
759
|
-
}
|
|
790
|
+
elem.nextSibling ? elem.before(path) : parentNode.append(path)
|
|
760
791
|
|
|
761
792
|
const d = getPathDFromElement(elem)
|
|
762
793
|
if (d) {
|
|
@@ -773,6 +804,20 @@ export const getBBoxOfElementAsPath = function (
|
|
|
773
804
|
} catch (e) {
|
|
774
805
|
// Firefox fails
|
|
775
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
|
+
}
|
|
776
821
|
path.remove()
|
|
777
822
|
return bb
|
|
778
823
|
}
|
|
@@ -873,7 +918,7 @@ export const convertToPath = (elem, attrs, svgCanvas) => {
|
|
|
873
918
|
* @param {boolean} hasAMatrixTransform - True if there is a matrix transform
|
|
874
919
|
* @returns {boolean} True if the bbox can be optimized.
|
|
875
920
|
*/
|
|
876
|
-
|
|
921
|
+
const bBoxCanBeOptimizedOverNativeGetBBox = (angle, hasAMatrixTransform) => {
|
|
877
922
|
const angleModulo90 = angle % 90
|
|
878
923
|
const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
|
|
879
924
|
const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001
|
|
@@ -886,21 +931,78 @@ function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
|
|
|
886
931
|
* @param {Element} elem - The DOM element to be converted
|
|
887
932
|
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
888
933
|
* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
|
|
889
|
-
* @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
|
|
890
935
|
*/
|
|
891
|
-
export const getBBoxWithTransform =
|
|
936
|
+
export const getBBoxWithTransform = (
|
|
892
937
|
elem,
|
|
893
938
|
addSVGElementsFromJson,
|
|
894
939
|
pathActions
|
|
895
|
-
) {
|
|
940
|
+
) => {
|
|
896
941
|
// TODO: Fix issue with rotated groups. Currently they work
|
|
897
942
|
// fine in FF, but not in other browsers (same problem mentioned
|
|
898
943
|
// in Issue 339 comment #2).
|
|
899
944
|
|
|
900
945
|
let bb = getBBox(elem)
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
}
|
|
904
1006
|
}
|
|
905
1007
|
|
|
906
1008
|
const tlist = getTransformList(elem)
|
|
@@ -914,23 +1016,29 @@ export const getBBoxWithTransform = function (
|
|
|
914
1016
|
// TODO: why ellipse and not circle
|
|
915
1017
|
const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
|
|
916
1018
|
if (elemNames.includes(elem.tagName)) {
|
|
917
|
-
|
|
1019
|
+
const pathBox = getBBoxOfElementAsPath(
|
|
918
1020
|
elem,
|
|
919
1021
|
addSVGElementsFromJson,
|
|
920
1022
|
pathActions
|
|
921
1023
|
)
|
|
922
|
-
|
|
1024
|
+
if (pathBox && !(pathBox.width === 0 && pathBox.height === 0)) {
|
|
1025
|
+
goodBb = pathBox
|
|
1026
|
+
bb = pathBox
|
|
1027
|
+
}
|
|
923
1028
|
} else if (elem.tagName === 'rect') {
|
|
924
1029
|
// Look for radius
|
|
925
1030
|
const rx = Number(elem.getAttribute('rx'))
|
|
926
1031
|
const ry = Number(elem.getAttribute('ry'))
|
|
927
1032
|
if (rx || ry) {
|
|
928
|
-
|
|
1033
|
+
const roundedRectBox = getBBoxOfElementAsPath(
|
|
929
1034
|
elem,
|
|
930
1035
|
addSVGElementsFromJson,
|
|
931
1036
|
pathActions
|
|
932
1037
|
)
|
|
933
|
-
|
|
1038
|
+
if (roundedRectBox && !(roundedRectBox.width === 0 && roundedRectBox.height === 0)) {
|
|
1039
|
+
goodBb = roundedRectBox
|
|
1040
|
+
bb = roundedRectBox
|
|
1041
|
+
}
|
|
934
1042
|
}
|
|
935
1043
|
}
|
|
936
1044
|
}
|
|
@@ -1131,7 +1239,11 @@ export let getRotationAngle = (elem, toRad) => {
|
|
|
1131
1239
|
* @returns {Element} Reference element
|
|
1132
1240
|
*/
|
|
1133
1241
|
export const getRefElem = attrVal => {
|
|
1134
|
-
|
|
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)
|
|
1135
1247
|
}
|
|
1136
1248
|
/**
|
|
1137
1249
|
* Get the reference element associated with the given attribute value.
|
|
@@ -1162,7 +1274,7 @@ export const getFeGaussianBlur = ele => {
|
|
|
1162
1274
|
*/
|
|
1163
1275
|
export const getElement = id => {
|
|
1164
1276
|
// querySelector lookup
|
|
1165
|
-
return svgroot_.querySelector(
|
|
1277
|
+
return svgroot_.querySelector(`#${id}`)
|
|
1166
1278
|
}
|
|
1167
1279
|
|
|
1168
1280
|
/**
|
|
@@ -1177,9 +1289,9 @@ export const getElement = id => {
|
|
|
1177
1289
|
export const assignAttributes = (elem, attrs, suspendLength, unitCheck) => {
|
|
1178
1290
|
for (const [key, value] of Object.entries(attrs)) {
|
|
1179
1291
|
const ns =
|
|
1180
|
-
key.
|
|
1292
|
+
key.startsWith('xml:')
|
|
1181
1293
|
? NS.XML
|
|
1182
|
-
: key.
|
|
1294
|
+
: key.startsWith('xlink:')
|
|
1183
1295
|
? NS.XLINK
|
|
1184
1296
|
: null
|
|
1185
1297
|
if (value === undefined) {
|