@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/blur-event.js +156 -0
- package/clear.js +43 -0
- package/coords.js +298 -0
- package/copy-elem.js +45 -0
- package/dataStorage.js +28 -0
- package/dist/svgcanvas.js +515 -0
- package/dist/svgcanvas.js.map +1 -0
- package/draw.js +1064 -0
- package/elem-get-set.js +1077 -0
- package/event.js +1388 -0
- package/history.js +619 -0
- package/historyrecording.js +161 -0
- package/json.js +110 -0
- package/layer.js +228 -0
- package/math.js +221 -0
- package/namespaces.js +40 -0
- package/package.json +54 -0
- package/paint.js +88 -0
- package/paste-elem.js +127 -0
- package/path-actions.js +1237 -0
- package/path-method.js +1012 -0
- package/path.js +781 -0
- package/recalculate.js +794 -0
- package/rollup.config.js +40 -0
- package/sanitize.js +252 -0
- package/select.js +543 -0
- package/selected-elem.js +1297 -0
- package/selection.js +482 -0
- package/svg-exec.js +1289 -0
- package/svgcanvas.js +1347 -0
- package/svgroot.js +36 -0
- package/text-actions.js +530 -0
- package/touch.js +51 -0
- package/undo.js +279 -0
- package/utilities.js +1214 -0
package/utilities.js
ADDED
|
@@ -0,0 +1,1214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Miscellaneous utilities.
|
|
3
|
+
* @module utilities
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NS } from './namespaces.js'
|
|
10
|
+
import { setUnitAttr, getTypeMap } from '../../src/common/units.js'
|
|
11
|
+
import {
|
|
12
|
+
hasMatrixTransform, transformListToTransform, transformBox
|
|
13
|
+
} from './math.js'
|
|
14
|
+
import { getClosest, mergeDeep } from '../../src/common/util.js'
|
|
15
|
+
|
|
16
|
+
// Much faster than running getBBox() every time
|
|
17
|
+
const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use,clipPath'
|
|
18
|
+
const visElemsArr = visElems.split(',')
|
|
19
|
+
// const hidElems = 'defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
|
|
20
|
+
|
|
21
|
+
let svgCanvas = null
|
|
22
|
+
let svgroot_ = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Object with the following keys/values.
|
|
26
|
+
* @typedef {PlainObject} module:utilities.SVGElementJSON
|
|
27
|
+
* @property {string} element - Tag name of the SVG element to create
|
|
28
|
+
* @property {PlainObject<string, string>} attr - Has key-value attributes to assign to the new element.
|
|
29
|
+
* An `id` should be set so that {@link module:utilities.EditorContext#addSVGElementsFromJson} can later re-identify the element for modification or replacement.
|
|
30
|
+
* @property {boolean} [curStyles=false] - Indicates whether current style attributes should be applied first
|
|
31
|
+
* @property {module:utilities.SVGElementJSON[]} [children] - Data objects to be added recursively as children
|
|
32
|
+
* @property {string} [namespace="http://www.w3.org/2000/svg"] - Indicate a (non-SVG) namespace
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An object that creates SVG elements for the canvas.
|
|
37
|
+
*
|
|
38
|
+
* @interface module:utilities.EditorContext
|
|
39
|
+
* @property {module:path.pathActions} pathActions
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @function module:utilities.EditorContext#getSvgContent
|
|
43
|
+
* @returns {SVGSVGElement}
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Create a new SVG element based on the given object keys/values and add it
|
|
47
|
+
* to the current layer.
|
|
48
|
+
* The element will be run through `cleanupElement` before being returned.
|
|
49
|
+
* @function module:utilities.EditorContext#addSVGElementsFromJson
|
|
50
|
+
* @param {module:utilities.SVGElementJSON} data
|
|
51
|
+
* @returns {Element} The new element
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* @function module:utilities.EditorContext#getSelectedElements
|
|
55
|
+
* @returns {Element[]} the array with selected DOM elements
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* @function module:utilities.EditorContext#getDOMDocument
|
|
59
|
+
* @returns {HTMLDocument}
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* @function module:utilities.EditorContext#getDOMContainer
|
|
63
|
+
* @returns {HTMLElement}
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* @function module:utilities.EditorContext#getSvgRoot
|
|
67
|
+
* @returns {SVGSVGElement}
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* @function module:utilities.EditorContext#getBaseUnit
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* @function module:utilities.EditorContext#getSnappingStep
|
|
75
|
+
* @returns {Float|string}
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @function module:utilities.init
|
|
80
|
+
* @param {module:utilities.EditorContext} canvas
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
export const init = (canvas) => {
|
|
84
|
+
svgCanvas = canvas
|
|
85
|
+
svgroot_ = canvas.getSvgRoot()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Used to prevent the [Billion laughs attack]{@link https://en.wikipedia.org/wiki/Billion_laughs_attack}.
|
|
90
|
+
* @function module:utilities.dropXMLInternalSubset
|
|
91
|
+
* @param {string} str String to be processed
|
|
92
|
+
* @returns {string} The string with entity declarations in the internal subset removed
|
|
93
|
+
* @todo This might be needed in other places `parseFromString` is used even without LGTM flagging
|
|
94
|
+
*/
|
|
95
|
+
export const dropXMLInternalSubset = (str) => {
|
|
96
|
+
return str.replace(/(<!DOCTYPE\s+\w*\s*\[).*(\?]>)/, '$1$2')
|
|
97
|
+
// return str.replace(/(?<doctypeOpen><!DOCTYPE\s+\w*\s*\[).*(?<doctypeClose>\?\]>)/, '$<doctypeOpen>$<doctypeClose>');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Converts characters in a string to XML-friendly entities.
|
|
102
|
+
* @function module:utilities.toXml
|
|
103
|
+
* @example `&` becomes `&`
|
|
104
|
+
* @param {string} str - The string to be converted
|
|
105
|
+
* @returns {string} The converted string
|
|
106
|
+
*/
|
|
107
|
+
export const toXml = (str) => {
|
|
108
|
+
// ' is ok in XML, but not HTML
|
|
109
|
+
// > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
|
|
110
|
+
return str
|
|
111
|
+
.replace(/&/g, '&')
|
|
112
|
+
.replace(/</g, '<')
|
|
113
|
+
.replace(/>/g, '>')
|
|
114
|
+
.replace(/"/g, '"')
|
|
115
|
+
.replace(/'/g, ''') // Note: `'` is XML only
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// This code was written by Tyler Akins and has been placed in the
|
|
119
|
+
// public domain. It would be nice if you left this header intact.
|
|
120
|
+
// Base64 code from Tyler Akins -- http://rumkin.com
|
|
121
|
+
|
|
122
|
+
// schiller: Removed string concatenation in favour of Array.join() optimization,
|
|
123
|
+
// also precalculate the size of the array needed.
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Converts a string to base64.
|
|
127
|
+
* @function module:utilities.encode64
|
|
128
|
+
* @param {string} input
|
|
129
|
+
* @returns {string} Base64 output
|
|
130
|
+
*/
|
|
131
|
+
export function encode64 (input) {
|
|
132
|
+
// base64 strings are 4/3 larger than the original string
|
|
133
|
+
input = encodeUTF8(input) // convert non-ASCII characters
|
|
134
|
+
return window.btoa(input) // Use native if available
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Converts a string from base64.
|
|
139
|
+
* @function module:utilities.decode64
|
|
140
|
+
* @param {string} input Base64-encoded input
|
|
141
|
+
* @returns {string} Decoded output
|
|
142
|
+
*/
|
|
143
|
+
export function decode64 (input) {
|
|
144
|
+
return decodeUTF8(window.atob(input))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Compute a hashcode from a given string
|
|
149
|
+
* @param word : the string, we want to compute the hashcode
|
|
150
|
+
* @returns {number}: Hascode of the given string
|
|
151
|
+
*/
|
|
152
|
+
export function hashCode (word) {
|
|
153
|
+
let hash = 0
|
|
154
|
+
let chr
|
|
155
|
+
if (word.length === 0) return hash
|
|
156
|
+
for (let i = 0; i < word.length; i++) {
|
|
157
|
+
chr = word.charCodeAt(i)
|
|
158
|
+
hash = ((hash << 5) - hash) + chr
|
|
159
|
+
hash |= 0 // Convert to 32bit integer
|
|
160
|
+
}
|
|
161
|
+
return hash
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @function module:utilities.decodeUTF8
|
|
166
|
+
* @param {string} argString
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function decodeUTF8 (argString) {
|
|
170
|
+
return decodeURIComponent(escape(argString))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
|
|
174
|
+
/**
|
|
175
|
+
* @function module:utilities.encodeUTF8
|
|
176
|
+
* @param {string} argString
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
export const encodeUTF8 = (argString) => {
|
|
180
|
+
return unescape(encodeURIComponent(argString))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Convert dataURL to object URL.
|
|
185
|
+
* @function module:utilities.dataURLToObjectURL
|
|
186
|
+
* @param {string} dataurl
|
|
187
|
+
* @returns {string} object URL or empty string
|
|
188
|
+
*/
|
|
189
|
+
export const dataURLToObjectURL = (dataurl) => {
|
|
190
|
+
if (typeof Uint8Array === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) {
|
|
191
|
+
return ''
|
|
192
|
+
}
|
|
193
|
+
const arr = dataurl.split(',')
|
|
194
|
+
const mime = arr[0].match(/:(.*?);/)[1]
|
|
195
|
+
const bstr = atob(arr[1])
|
|
196
|
+
/*
|
|
197
|
+
const [prefix, suffix] = dataurl.split(','),
|
|
198
|
+
{groups: {mime}} = prefix.match(/:(?<mime>.*?);/),
|
|
199
|
+
bstr = atob(suffix);
|
|
200
|
+
*/
|
|
201
|
+
let n = bstr.length
|
|
202
|
+
const u8arr = new Uint8Array(n)
|
|
203
|
+
while (n--) {
|
|
204
|
+
u8arr[n] = bstr.charCodeAt(n)
|
|
205
|
+
}
|
|
206
|
+
const blob = new Blob([u8arr], { type: mime })
|
|
207
|
+
return URL.createObjectURL(blob)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get object URL for a blob object.
|
|
212
|
+
* @function module:utilities.createObjectURL
|
|
213
|
+
* @param {Blob} blob A Blob object or File object
|
|
214
|
+
* @returns {string} object URL or empty string
|
|
215
|
+
*/
|
|
216
|
+
export const createObjectURL = (blob) => {
|
|
217
|
+
if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {
|
|
218
|
+
return ''
|
|
219
|
+
}
|
|
220
|
+
return URL.createObjectURL(blob)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @property {string} blankPageObjectURL
|
|
225
|
+
*/
|
|
226
|
+
export const blankPageObjectURL = (() => {
|
|
227
|
+
if (typeof Blob === 'undefined') {
|
|
228
|
+
return ''
|
|
229
|
+
}
|
|
230
|
+
const blob = new Blob(['<html><head><title>SVG-edit</title></head><body> </body></html>'], { type: 'text/html' })
|
|
231
|
+
return createObjectURL(blob)
|
|
232
|
+
})()
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Converts a string to use XML references (for non-ASCII).
|
|
236
|
+
* @function module:utilities.convertToXMLReferences
|
|
237
|
+
* @param {string} input
|
|
238
|
+
* @returns {string} Decimal numeric character references
|
|
239
|
+
*/
|
|
240
|
+
export const convertToXMLReferences = (input) => {
|
|
241
|
+
let output = '';
|
|
242
|
+
[...input].forEach((ch) => {
|
|
243
|
+
const c = ch.charCodeAt()
|
|
244
|
+
output += (c <= 127) ? ch : `&#${c};`
|
|
245
|
+
})
|
|
246
|
+
return output
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Cross-browser compatible method of converting a string to an XML tree.
|
|
251
|
+
* Found this function [here]{@link http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f}.
|
|
252
|
+
* @function module:utilities.text2xml
|
|
253
|
+
* @param {string} sXML
|
|
254
|
+
* @throws {Error}
|
|
255
|
+
* @returns {XMLDocument}
|
|
256
|
+
*/
|
|
257
|
+
export const text2xml = (sXML) => {
|
|
258
|
+
if (sXML.includes('<svg:svg')) {
|
|
259
|
+
sXML = sXML.replace(/<(\/?)svg:/g, '<$1').replace('xmlns:svg', 'xmlns')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let out; let dXML
|
|
263
|
+
try {
|
|
264
|
+
dXML = new DOMParser()
|
|
265
|
+
dXML.async = false
|
|
266
|
+
} catch (e) {
|
|
267
|
+
throw new Error('XML Parser could not be instantiated')
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
out = dXML.parseFromString(sXML, 'text/xml')
|
|
271
|
+
} catch (e2) { throw new Error('Error parsing XML string') }
|
|
272
|
+
return out
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @typedef {PlainObject} module:utilities.BBoxObject (like `DOMRect`)
|
|
277
|
+
* @property {Float} x
|
|
278
|
+
* @property {Float} y
|
|
279
|
+
* @property {Float} width
|
|
280
|
+
* @property {Float} height
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Converts a `SVGRect` into an object.
|
|
285
|
+
* @function module:utilities.bboxToObj
|
|
286
|
+
* @param {SVGRect} bbox - a SVGRect
|
|
287
|
+
* @returns {module:utilities.BBoxObject} An object with properties names x, y, width, height.
|
|
288
|
+
*/
|
|
289
|
+
export const bboxToObj = ({ x, y, width, height }) => {
|
|
290
|
+
return { x, y, width, height }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @callback module:utilities.TreeWalker
|
|
295
|
+
* @param {Element} elem - DOM element being traversed
|
|
296
|
+
* @returns {void}
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Walks the tree and executes the callback on each element in a top-down fashion.
|
|
301
|
+
* @function module:utilities.walkTree
|
|
302
|
+
* @param {Element} elem - DOM element to traverse
|
|
303
|
+
* @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
|
|
304
|
+
* @returns {void}
|
|
305
|
+
*/
|
|
306
|
+
export const walkTree = (elem, cbFn) => {
|
|
307
|
+
if (elem?.nodeType === 1) {
|
|
308
|
+
cbFn(elem)
|
|
309
|
+
let i = elem.childNodes.length
|
|
310
|
+
while (i--) {
|
|
311
|
+
walkTree(elem.childNodes.item(i), cbFn)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Walks the tree and executes the callback on each element in a depth-first fashion.
|
|
318
|
+
* @function module:utilities.walkTreePost
|
|
319
|
+
* @todo Shouldn't this be calling walkTreePost?
|
|
320
|
+
* @param {Element} elem - DOM element to traverse
|
|
321
|
+
* @param {module:utilities.TreeWalker} cbFn - Callback function to run on each element
|
|
322
|
+
* @returns {void}
|
|
323
|
+
*/
|
|
324
|
+
export const walkTreePost = (elem, cbFn) => {
|
|
325
|
+
if (elem?.nodeType === 1) {
|
|
326
|
+
let i = elem.childNodes.length
|
|
327
|
+
while (i--) {
|
|
328
|
+
walkTree(elem.childNodes.item(i), cbFn)
|
|
329
|
+
}
|
|
330
|
+
cbFn(elem)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Extracts the URL from the `url(...)` syntax of some attributes.
|
|
336
|
+
* Three variants:
|
|
337
|
+
* - `<circle fill="url(someFile.svg#foo)" />`
|
|
338
|
+
* - `<circle fill="url('someFile.svg#foo')" />`
|
|
339
|
+
* - `<circle fill='url("someFile.svg#foo")' />`
|
|
340
|
+
* @function module:utilities.getUrlFromAttr
|
|
341
|
+
* @param {string} attrVal The attribute value as a string
|
|
342
|
+
* @returns {string} String with just the URL, like "someFile.svg#foo"
|
|
343
|
+
*/
|
|
344
|
+
export const getUrlFromAttr = function (attrVal) {
|
|
345
|
+
if (attrVal) {
|
|
346
|
+
// url('#somegrad')
|
|
347
|
+
if (attrVal.startsWith('url("')) {
|
|
348
|
+
return attrVal.substring(5, attrVal.indexOf('"', 6))
|
|
349
|
+
}
|
|
350
|
+
// url('#somegrad')
|
|
351
|
+
if (attrVal.startsWith("url('")) {
|
|
352
|
+
return attrVal.substring(5, attrVal.indexOf("'", 6))
|
|
353
|
+
}
|
|
354
|
+
if (attrVal.startsWith('url(')) {
|
|
355
|
+
return attrVal.substring(4, attrVal.indexOf(')'))
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @function module:utilities.getHref
|
|
363
|
+
* @param {Element} elem
|
|
364
|
+
* @returns {string} The given element's `xlink:href` value
|
|
365
|
+
*/
|
|
366
|
+
export let getHref = function (elem) {
|
|
367
|
+
return elem.getAttributeNS(NS.XLINK, 'href')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Sets the given element's `xlink:href` value.
|
|
372
|
+
* @function module:utilities.setHref
|
|
373
|
+
* @param {Element} elem
|
|
374
|
+
* @param {string} val
|
|
375
|
+
* @returns {void}
|
|
376
|
+
*/
|
|
377
|
+
export let setHref = function (elem, val) {
|
|
378
|
+
elem.setAttributeNS(NS.XLINK, 'xlink:href', val)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @function module:utilities.findDefs
|
|
383
|
+
* @returns {SVGDefsElement} The document's `<defs>` element, creating it first if necessary
|
|
384
|
+
*/
|
|
385
|
+
export const findDefs = function () {
|
|
386
|
+
const svgElement = svgCanvas.getSvgContent()
|
|
387
|
+
let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs')
|
|
388
|
+
if (defs.length > 0) {
|
|
389
|
+
defs = defs[0]
|
|
390
|
+
} else {
|
|
391
|
+
defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs')
|
|
392
|
+
if (svgElement.firstChild) {
|
|
393
|
+
// first child is a comment, so call nextSibling
|
|
394
|
+
svgElement.insertBefore(defs, svgElement.firstChild.nextSibling)
|
|
395
|
+
// svgElement.firstChild.nextSibling.before(defs); // Not safe
|
|
396
|
+
} else {
|
|
397
|
+
svgElement.append(defs)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return defs
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// TODO(codedread): Consider moving the next to functions to bbox.js
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Get correct BBox for a path in Webkit.
|
|
407
|
+
* Converted from code found [here]{@link http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html}.
|
|
408
|
+
* @function module:utilities.getPathBBox
|
|
409
|
+
* @param {SVGPathElement} path - The path DOM element to get the BBox for
|
|
410
|
+
* @returns {module:utilities.BBoxObject} A BBox-like object
|
|
411
|
+
*/
|
|
412
|
+
export const getPathBBox = function (path) {
|
|
413
|
+
const seglist = path.pathSegList
|
|
414
|
+
const tot = seglist.numberOfItems
|
|
415
|
+
|
|
416
|
+
const bounds = [[], []]
|
|
417
|
+
const start = seglist.getItem(0)
|
|
418
|
+
let P0 = [start.x, start.y]
|
|
419
|
+
|
|
420
|
+
const getCalc = function (j, P1, P2, P3) {
|
|
421
|
+
return function (t) {
|
|
422
|
+
return 1 - t ** 3 * P0[j] +
|
|
423
|
+
3 * 1 - t ** 2 * t * P1[j] +
|
|
424
|
+
3 * (1 - t) * t ** 2 * P2[j] +
|
|
425
|
+
t ** 3 * P3[j]
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < tot; i++) {
|
|
430
|
+
const seg = seglist.getItem(i)
|
|
431
|
+
|
|
432
|
+
if (seg.x === undefined) { continue }
|
|
433
|
+
|
|
434
|
+
// Add actual points to limits
|
|
435
|
+
bounds[0].push(P0[0])
|
|
436
|
+
bounds[1].push(P0[1])
|
|
437
|
+
|
|
438
|
+
if (seg.x1) {
|
|
439
|
+
const P1 = [seg.x1, seg.y1]
|
|
440
|
+
const P2 = [seg.x2, seg.y2]
|
|
441
|
+
const P3 = [seg.x, seg.y]
|
|
442
|
+
|
|
443
|
+
for (let j = 0; j < 2; j++) {
|
|
444
|
+
const calc = getCalc(j, P1, P2, P3)
|
|
445
|
+
|
|
446
|
+
const b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j]
|
|
447
|
+
const a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j]
|
|
448
|
+
const c = 3 * P1[j] - 3 * P0[j]
|
|
449
|
+
|
|
450
|
+
if (a === 0) {
|
|
451
|
+
if (b === 0) { continue }
|
|
452
|
+
const t = -c / b
|
|
453
|
+
if (t > 0 && t < 1) {
|
|
454
|
+
bounds[j].push(calc(t))
|
|
455
|
+
}
|
|
456
|
+
continue
|
|
457
|
+
}
|
|
458
|
+
const b2ac = b ** 2 - 4 * c * a
|
|
459
|
+
if (b2ac < 0) { continue }
|
|
460
|
+
const t1 = (-b + Math.sqrt(b2ac)) / (2 * a)
|
|
461
|
+
if (t1 > 0 && t1 < 1) { bounds[j].push(calc(t1)) }
|
|
462
|
+
const t2 = (-b - Math.sqrt(b2ac)) / (2 * a)
|
|
463
|
+
if (t2 > 0 && t2 < 1) { bounds[j].push(calc(t2)) }
|
|
464
|
+
}
|
|
465
|
+
P0 = P3
|
|
466
|
+
} else {
|
|
467
|
+
bounds[0].push(seg.x)
|
|
468
|
+
bounds[1].push(seg.y)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const x = Math.min.apply(null, bounds[0])
|
|
473
|
+
const w = Math.max.apply(null, bounds[0]) - x
|
|
474
|
+
const y = Math.min.apply(null, bounds[1])
|
|
475
|
+
const h = Math.max.apply(null, bounds[1]) - y
|
|
476
|
+
return {
|
|
477
|
+
x,
|
|
478
|
+
y,
|
|
479
|
+
width: w,
|
|
480
|
+
height: h
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Get the given/selected element's bounding box object, convert it to be more
|
|
486
|
+
* usable when necessary.
|
|
487
|
+
* @function module:utilities.getBBox
|
|
488
|
+
* @param {Element} elem - Optional DOM element to get the BBox for
|
|
489
|
+
* @returns {module:utilities.BBoxObject} Bounding box object
|
|
490
|
+
*/
|
|
491
|
+
export const getBBox = function (elem) {
|
|
492
|
+
const selected = elem || svgCanvas.getSelectedElements()[0]
|
|
493
|
+
if (elem.nodeType !== 1) { return null }
|
|
494
|
+
const elname = selected.nodeName
|
|
495
|
+
|
|
496
|
+
let ret = null
|
|
497
|
+
switch (elname) {
|
|
498
|
+
case 'text':
|
|
499
|
+
if (selected.textContent === '') {
|
|
500
|
+
selected.textContent = 'a' // Some character needed for the selector to use.
|
|
501
|
+
ret = selected.getBBox()
|
|
502
|
+
selected.textContent = ''
|
|
503
|
+
} else if (selected.getBBox) {
|
|
504
|
+
ret = selected.getBBox()
|
|
505
|
+
}
|
|
506
|
+
break
|
|
507
|
+
case 'path':
|
|
508
|
+
case 'g':
|
|
509
|
+
case 'a':
|
|
510
|
+
if (selected.getBBox) {
|
|
511
|
+
ret = selected.getBBox()
|
|
512
|
+
}
|
|
513
|
+
break
|
|
514
|
+
default:
|
|
515
|
+
|
|
516
|
+
if (elname === 'use') {
|
|
517
|
+
ret = selected.getBBox() // , true);
|
|
518
|
+
} else if (visElemsArr.includes(elname)) {
|
|
519
|
+
if (selected) {
|
|
520
|
+
try {
|
|
521
|
+
ret = selected.getBBox()
|
|
522
|
+
} catch (err) {
|
|
523
|
+
// tspan (and textPath apparently) have no `getBBox` in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=937268
|
|
524
|
+
// Re: Chrome returning bbox for containing text element, see: https://bugs.chromium.org/p/chromium/issues/detail?id=349835
|
|
525
|
+
const extent = selected.getExtentOfChar(0) // pos+dimensions of the first glyph
|
|
526
|
+
const width = selected.getComputedTextLength() // width of the tspan
|
|
527
|
+
ret = {
|
|
528
|
+
x: extent.x,
|
|
529
|
+
y: extent.y,
|
|
530
|
+
width,
|
|
531
|
+
height: extent.height
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
// Check if element is child of a foreignObject
|
|
536
|
+
const fo = getClosest(selected.parentNode, 'foreignObject')
|
|
537
|
+
if (fo.length && fo[0].getBBox) {
|
|
538
|
+
ret = fo[0].getBBox()
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (ret) {
|
|
544
|
+
ret = bboxToObj(ret)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// get the bounding box from the DOM (which is in that element's coordinate system)
|
|
548
|
+
return ret
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @typedef {GenericArray} module:utilities.PathSegmentArray
|
|
553
|
+
* @property {Integer} length 2
|
|
554
|
+
* @property {"M"|"L"|"C"|"Z"} 0
|
|
555
|
+
* @property {Float[]} 1
|
|
556
|
+
*/
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Create a path 'd' attribute from path segments.
|
|
560
|
+
* Each segment is an array of the form: `[singleChar, [x,y, x,y, ...]]`
|
|
561
|
+
* @function module:utilities.getPathDFromSegments
|
|
562
|
+
* @param {module:utilities.PathSegmentArray[]} pathSegments - An array of path segments to be converted
|
|
563
|
+
* @returns {string} The converted path d attribute.
|
|
564
|
+
*/
|
|
565
|
+
export const getPathDFromSegments = function (pathSegments) {
|
|
566
|
+
let d = ''
|
|
567
|
+
|
|
568
|
+
pathSegments.forEach(function ([singleChar, pts], _j) {
|
|
569
|
+
d += singleChar
|
|
570
|
+
for (let i = 0; i < pts.length; i += 2) {
|
|
571
|
+
d += (pts[i] + ',' + pts[i + 1]) + ' '
|
|
572
|
+
}
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
return d
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Make a path 'd' attribute from a simple SVG element shape.
|
|
580
|
+
* @function module:utilities.getPathDFromElement
|
|
581
|
+
* @param {Element} elem - The element to be converted
|
|
582
|
+
* @returns {string} The path d attribute or `undefined` if the element type is unknown.
|
|
583
|
+
*/
|
|
584
|
+
export const getPathDFromElement = function (elem) {
|
|
585
|
+
// Possibly the cubed root of 6, but 1.81 works best
|
|
586
|
+
let num = 1.81
|
|
587
|
+
let d; let rx; let ry
|
|
588
|
+
switch (elem.tagName) {
|
|
589
|
+
case 'ellipse':
|
|
590
|
+
case 'circle': {
|
|
591
|
+
rx = Number(elem.getAttribute('rx'))
|
|
592
|
+
ry = Number(elem.getAttribute('ry'))
|
|
593
|
+
const cx = Number(elem.getAttribute('cx'))
|
|
594
|
+
const cy = Number(elem.getAttribute('cy'))
|
|
595
|
+
if (elem.tagName === 'circle' && elem.hasAttribute('r')) {
|
|
596
|
+
ry = Number(elem.getAttribute('r'))
|
|
597
|
+
rx = ry
|
|
598
|
+
}
|
|
599
|
+
d = getPathDFromSegments([
|
|
600
|
+
['M', [(cx - rx), (cy)]],
|
|
601
|
+
['C', [(cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)]],
|
|
602
|
+
['C', [(cx + rx / num), (cy - ry), (cx + rx), (cy - ry / num), (cx + rx), (cy)]],
|
|
603
|
+
['C', [(cx + rx), (cy + ry / num), (cx + rx / num), (cy + ry), (cx), (cy + ry)]],
|
|
604
|
+
['C', [(cx - rx / num), (cy + ry), (cx - rx), (cy + ry / num), (cx - rx), (cy)]],
|
|
605
|
+
['Z', []]
|
|
606
|
+
])
|
|
607
|
+
break
|
|
608
|
+
} case 'path':
|
|
609
|
+
d = elem.getAttribute('d')
|
|
610
|
+
break
|
|
611
|
+
case 'line': {
|
|
612
|
+
const x1 = elem.getAttribute('x1')
|
|
613
|
+
const y1 = elem.getAttribute('y1')
|
|
614
|
+
const x2 = elem.getAttribute('x2')
|
|
615
|
+
const y2 = elem.getAttribute('y2')
|
|
616
|
+
d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2
|
|
617
|
+
}
|
|
618
|
+
break
|
|
619
|
+
case 'polyline':
|
|
620
|
+
d = 'M' + elem.getAttribute('points')
|
|
621
|
+
break
|
|
622
|
+
case 'polygon':
|
|
623
|
+
d = 'M' + elem.getAttribute('points') + ' Z'
|
|
624
|
+
break
|
|
625
|
+
case 'rect': {
|
|
626
|
+
rx = Number(elem.getAttribute('rx'))
|
|
627
|
+
ry = Number(elem.getAttribute('ry'))
|
|
628
|
+
const b = elem.getBBox()
|
|
629
|
+
const { x, y } = b
|
|
630
|
+
const w = b.width
|
|
631
|
+
const h = b.height
|
|
632
|
+
num = 4 - num // Why? Because!
|
|
633
|
+
|
|
634
|
+
d = (!rx && !ry)
|
|
635
|
+
// Regular rect
|
|
636
|
+
? getPathDFromSegments([
|
|
637
|
+
['M', [x, y]],
|
|
638
|
+
['L', [x + w, y]],
|
|
639
|
+
['L', [x + w, y + h]],
|
|
640
|
+
['L', [x, y + h]],
|
|
641
|
+
['L', [x, y]],
|
|
642
|
+
['Z', []]
|
|
643
|
+
])
|
|
644
|
+
: getPathDFromSegments([
|
|
645
|
+
['M', [x, y + ry]],
|
|
646
|
+
['C', [x, y + ry / num, x + rx / num, y, x + rx, y]],
|
|
647
|
+
['L', [x + w - rx, y]],
|
|
648
|
+
['C', [x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry]],
|
|
649
|
+
['L', [x + w, y + h - ry]],
|
|
650
|
+
['C', [x + w, y + h - ry / num, x + w - rx / num, y + h, x + w - rx, y + h]],
|
|
651
|
+
['L', [x + rx, y + h]],
|
|
652
|
+
['C', [x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry]],
|
|
653
|
+
['L', [x, y + ry]],
|
|
654
|
+
['Z', []]
|
|
655
|
+
])
|
|
656
|
+
break
|
|
657
|
+
} default:
|
|
658
|
+
break
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return d
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Get a set of attributes from an element that is useful for convertToPath.
|
|
666
|
+
* @function module:utilities.getExtraAttributesForConvertToPath
|
|
667
|
+
* @param {Element} elem - The element to be probed
|
|
668
|
+
* @returns {PlainObject<"marker-start"|"marker-end"|"marker-mid"|"filter"|"clip-path", string>} An object with attributes.
|
|
669
|
+
*/
|
|
670
|
+
export const getExtraAttributesForConvertToPath = function (elem) {
|
|
671
|
+
const attrs = {};
|
|
672
|
+
// TODO: make this list global so that we can properly maintain it
|
|
673
|
+
// TODO: what about @transform, @clip-rule, @fill-rule, etc?
|
|
674
|
+
['marker-start', 'marker-end', 'marker-mid', 'filter', 'clip-path'].forEach(function (item) {
|
|
675
|
+
const a = elem.getAttribute(item)
|
|
676
|
+
if (a) {
|
|
677
|
+
attrs[item] = a
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
return attrs
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Get the BBox of an element-as-path.
|
|
685
|
+
* @function module:utilities.getBBoxOfElementAsPath
|
|
686
|
+
* @param {Element} elem - The DOM element to be probed
|
|
687
|
+
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
688
|
+
* @param {module:path.pathActions} pathActions - If a transform exists, `pathActions.resetOrientation()` is used. See: canvas.pathActions.
|
|
689
|
+
* @returns {DOMRect|false} The resulting path's bounding box object.
|
|
690
|
+
*/
|
|
691
|
+
export const getBBoxOfElementAsPath = function (elem, addSVGElementsFromJson, pathActions) {
|
|
692
|
+
const path = addSVGElementsFromJson({
|
|
693
|
+
element: 'path',
|
|
694
|
+
attr: getExtraAttributesForConvertToPath(elem)
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
const eltrans = elem.getAttribute('transform')
|
|
698
|
+
if (eltrans) {
|
|
699
|
+
path.setAttribute('transform', eltrans)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const { parentNode } = elem
|
|
703
|
+
if (elem.nextSibling) {
|
|
704
|
+
elem.before(path)
|
|
705
|
+
} else {
|
|
706
|
+
parentNode.append(path)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const d = getPathDFromElement(elem)
|
|
710
|
+
if (d) {
|
|
711
|
+
path.setAttribute('d', d)
|
|
712
|
+
} else {
|
|
713
|
+
path.remove()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Get the correct BBox of the new path, then discard it
|
|
717
|
+
pathActions.resetOrientation(path)
|
|
718
|
+
let bb = false
|
|
719
|
+
try {
|
|
720
|
+
bb = path.getBBox()
|
|
721
|
+
} catch (e) {
|
|
722
|
+
// Firefox fails
|
|
723
|
+
}
|
|
724
|
+
path.remove()
|
|
725
|
+
return bb
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Convert selected element to a path.
|
|
730
|
+
* @function module:utilities.convertToPath
|
|
731
|
+
* @param {Element} elem - The DOM element to be converted
|
|
732
|
+
* @param {module:utilities.SVGElementJSON} attrs - Apply attributes to new path. see canvas.convertToPath
|
|
733
|
+
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
734
|
+
* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
|
|
735
|
+
* @param {module:draw.DrawCanvasInit#clearSelection|module:path.EditorContext#clearSelection} clearSelection - see [canvas.clearSelection]{@link module:svgcanvas.SvgCanvas#clearSelection}
|
|
736
|
+
* @param {module:path.EditorContext#addToSelection} addToSelection - see [canvas.addToSelection]{@link module:svgcanvas.SvgCanvas#addToSelection}
|
|
737
|
+
* @param {module:history} hstry - see history module
|
|
738
|
+
* @param {module:path.EditorContext#addCommandToHistory|module:draw.DrawCanvasInit#addCommandToHistory} addCommandToHistory - see [canvas.addCommandToHistory]{@link module:svgcanvas~addCommandToHistory}
|
|
739
|
+
* @returns {SVGPathElement|null} The converted path element or null if the DOM element was not recognized.
|
|
740
|
+
*/
|
|
741
|
+
export const convertToPath = (elem, attrs, svgCanvas) => {
|
|
742
|
+
const batchCmd = new svgCanvas.history.BatchCommand('Convert element to Path')
|
|
743
|
+
|
|
744
|
+
// Any attribute on the element not covered by the passed-in attributes
|
|
745
|
+
attrs = mergeDeep(attrs, getExtraAttributesForConvertToPath(elem))
|
|
746
|
+
|
|
747
|
+
const path = svgCanvas.addSVGElementsFromJson({
|
|
748
|
+
element: 'path',
|
|
749
|
+
attr: attrs
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
const eltrans = elem.getAttribute('transform')
|
|
753
|
+
if (eltrans) {
|
|
754
|
+
path.setAttribute('transform', eltrans)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const { id } = elem
|
|
758
|
+
const { parentNode } = elem
|
|
759
|
+
if (elem.nextSibling) {
|
|
760
|
+
elem.before(path)
|
|
761
|
+
} else {
|
|
762
|
+
parentNode.append(path)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const d = getPathDFromElement(elem)
|
|
766
|
+
if (d) {
|
|
767
|
+
path.setAttribute('d', d)
|
|
768
|
+
|
|
769
|
+
// Replace the current element with the converted one
|
|
770
|
+
|
|
771
|
+
// Reorient if it has a matrix
|
|
772
|
+
if (eltrans) {
|
|
773
|
+
const tlist = path.transform.baseVal
|
|
774
|
+
if (hasMatrixTransform(tlist)) {
|
|
775
|
+
svgCanvas.pathActions.resetOrientation(path)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const { nextSibling } = elem
|
|
780
|
+
batchCmd.addSubCommand(new svgCanvas.history.RemoveElementCommand(elem, nextSibling, elem.parentNode))
|
|
781
|
+
svgCanvas.clearSelection()
|
|
782
|
+
elem.remove() // We need to remove this element otherwise the nextSibling of 'path' won't be null and an exception will be thrown after subsequent undo and redos.
|
|
783
|
+
|
|
784
|
+
batchCmd.addSubCommand(new svgCanvas.history.InsertElementCommand(path))
|
|
785
|
+
path.setAttribute('id', id)
|
|
786
|
+
path.removeAttribute('visibility')
|
|
787
|
+
svgCanvas.addToSelection([path], true)
|
|
788
|
+
|
|
789
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
790
|
+
|
|
791
|
+
return path
|
|
792
|
+
}
|
|
793
|
+
// the elem.tagName was not recognized, so no "d" attribute. Remove it, so we've haven't changed anything.
|
|
794
|
+
path.remove()
|
|
795
|
+
return null
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Can the bbox be optimized over the native getBBox? The optimized bbox is the same as the native getBBox when
|
|
800
|
+
* the rotation angle is a multiple of 90 degrees and there are no complex transforms.
|
|
801
|
+
* Getting an optimized bbox can be dramatically slower, so we want to make sure it's worth it.
|
|
802
|
+
*
|
|
803
|
+
* The best example for this is a circle rotate 45 degrees. The circle doesn't get wider or taller when rotated
|
|
804
|
+
* about it's center.
|
|
805
|
+
*
|
|
806
|
+
* The standard, unoptimized technique gets the native bbox of the circle, rotates the box 45 degrees, uses
|
|
807
|
+
* that width and height, and applies any transforms to get the final bbox. This means the calculated bbox
|
|
808
|
+
* is much wider than the original circle. If the angle had been 0, 90, 180, etc. both techniques render the
|
|
809
|
+
* same bbox.
|
|
810
|
+
*
|
|
811
|
+
* The optimization is not needed if the rotation is a multiple 90 degrees. The default technique is to call
|
|
812
|
+
* getBBox then apply the angle and any transforms.
|
|
813
|
+
*
|
|
814
|
+
* @param {Float} angle - The rotation angle in degrees
|
|
815
|
+
* @param {boolean} hasAMatrixTransform - True if there is a matrix transform
|
|
816
|
+
* @returns {boolean} True if the bbox can be optimized.
|
|
817
|
+
*/
|
|
818
|
+
function bBoxCanBeOptimizedOverNativeGetBBox (angle, hasAMatrixTransform) {
|
|
819
|
+
const angleModulo90 = angle % 90
|
|
820
|
+
const closeTo90 = angleModulo90 < -89.99 || angleModulo90 > 89.99
|
|
821
|
+
const closeTo0 = angleModulo90 > -0.001 && angleModulo90 < 0.001
|
|
822
|
+
return hasAMatrixTransform || !(closeTo0 || closeTo90)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Get bounding box that includes any transforms.
|
|
827
|
+
* @function module:utilities.getBBoxWithTransform
|
|
828
|
+
* @param {Element} elem - The DOM element to be converted
|
|
829
|
+
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
830
|
+
* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
|
|
831
|
+
* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
|
|
832
|
+
*/
|
|
833
|
+
export const getBBoxWithTransform = function (elem, addSVGElementsFromJson, pathActions) {
|
|
834
|
+
// TODO: Fix issue with rotated groups. Currently they work
|
|
835
|
+
// fine in FF, but not in other browsers (same problem mentioned
|
|
836
|
+
// in Issue 339 comment #2).
|
|
837
|
+
|
|
838
|
+
let bb = getBBox(elem)
|
|
839
|
+
|
|
840
|
+
if (!bb) {
|
|
841
|
+
return null
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const tlist = elem.transform.baseVal
|
|
845
|
+
const angle = getRotationAngleFromTransformList(tlist)
|
|
846
|
+
const hasMatrixXForm = hasMatrixTransform(tlist)
|
|
847
|
+
|
|
848
|
+
if (angle || hasMatrixXForm) {
|
|
849
|
+
let goodBb = false
|
|
850
|
+
if (bBoxCanBeOptimizedOverNativeGetBBox(angle, hasMatrixXForm)) {
|
|
851
|
+
// Get the BBox from the raw path for these elements
|
|
852
|
+
// TODO: why ellipse and not circle
|
|
853
|
+
const elemNames = ['ellipse', 'path', 'line', 'polyline', 'polygon']
|
|
854
|
+
if (elemNames.includes(elem.tagName)) {
|
|
855
|
+
goodBb = getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions)
|
|
856
|
+
bb = goodBb
|
|
857
|
+
} else if (elem.tagName === 'rect') {
|
|
858
|
+
// Look for radius
|
|
859
|
+
const rx = Number(elem.getAttribute('rx'))
|
|
860
|
+
const ry = Number(elem.getAttribute('ry'))
|
|
861
|
+
if (rx || ry) {
|
|
862
|
+
goodBb = getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions)
|
|
863
|
+
bb = goodBb
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (!goodBb) {
|
|
869
|
+
const { matrix } = transformListToTransform(tlist)
|
|
870
|
+
bb = transformBox(bb.x, bb.y, bb.width, bb.height, matrix).aabox
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return bb
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* @param {Element} elem
|
|
878
|
+
* @returns {Float}
|
|
879
|
+
* @todo This is problematic with large stroke-width and, for example, a single
|
|
880
|
+
* horizontal line. The calculated BBox extends way beyond left and right sides.
|
|
881
|
+
*/
|
|
882
|
+
const getStrokeOffsetForBBox = (elem) => {
|
|
883
|
+
const sw = elem.getAttribute('stroke-width')
|
|
884
|
+
return (!isNaN(sw) && elem.getAttribute('stroke') !== 'none') ? sw / 2 : 0
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* @typedef {PlainObject} BBox
|
|
889
|
+
* @property {Integer} x The x value
|
|
890
|
+
* @property {Integer} y The y value
|
|
891
|
+
* @property {Float} width
|
|
892
|
+
* @property {Float} height
|
|
893
|
+
*/
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Get the bounding box for one or more stroked and/or transformed elements.
|
|
897
|
+
* @function module:utilities.getStrokedBBox
|
|
898
|
+
* @param {Element[]} elems - Array with DOM elements to check
|
|
899
|
+
* @param {module:utilities.EditorContext#addSVGElementsFromJson} addSVGElementsFromJson - Function to add the path element to the current layer. See canvas.addSVGElementsFromJson
|
|
900
|
+
* @param {module:path.pathActions} pathActions - If a transform exists, pathActions.resetOrientation() is used. See: canvas.pathActions.
|
|
901
|
+
* @returns {module:utilities.BBoxObject|module:math.TransformedBox|DOMRect} A single bounding box object
|
|
902
|
+
*/
|
|
903
|
+
export const getStrokedBBox = (elems, addSVGElementsFromJson, pathActions) => {
|
|
904
|
+
if (!elems || !elems.length) { return false }
|
|
905
|
+
|
|
906
|
+
let fullBb
|
|
907
|
+
elems.forEach((elem) => {
|
|
908
|
+
if (fullBb) { return }
|
|
909
|
+
if (!elem.parentNode) { return }
|
|
910
|
+
fullBb = getBBoxWithTransform(elem, addSVGElementsFromJson, pathActions)
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
// This shouldn't ever happen...
|
|
914
|
+
if (!fullBb) { return null }
|
|
915
|
+
|
|
916
|
+
// fullBb doesn't include the stoke, so this does no good!
|
|
917
|
+
// if (elems.length == 1) return fullBb;
|
|
918
|
+
|
|
919
|
+
let maxX = fullBb.x + fullBb.width
|
|
920
|
+
let maxY = fullBb.y + fullBb.height
|
|
921
|
+
let minX = fullBb.x
|
|
922
|
+
let minY = fullBb.y
|
|
923
|
+
|
|
924
|
+
// If only one elem, don't call the potentially slow getBBoxWithTransform method again.
|
|
925
|
+
if (elems.length === 1) {
|
|
926
|
+
const offset = getStrokeOffsetForBBox(elems[0])
|
|
927
|
+
minX -= offset
|
|
928
|
+
minY -= offset
|
|
929
|
+
maxX += offset
|
|
930
|
+
maxY += offset
|
|
931
|
+
} else {
|
|
932
|
+
elems.forEach((elem) => {
|
|
933
|
+
const curBb = getBBoxWithTransform(elem, addSVGElementsFromJson, pathActions)
|
|
934
|
+
if (curBb) {
|
|
935
|
+
const offset = getStrokeOffsetForBBox(elem)
|
|
936
|
+
minX = Math.min(minX, curBb.x - offset)
|
|
937
|
+
minY = Math.min(minY, curBb.y - offset)
|
|
938
|
+
// TODO: The old code had this test for max, but not min. I suspect this test should be for both min and max
|
|
939
|
+
if (elem.nodeType === 1) {
|
|
940
|
+
maxX = Math.max(maxX, curBb.x + curBb.width + offset)
|
|
941
|
+
maxY = Math.max(maxY, curBb.y + curBb.height + offset)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
})
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
fullBb.x = minX
|
|
948
|
+
fullBb.y = minY
|
|
949
|
+
fullBb.width = maxX - minX
|
|
950
|
+
fullBb.height = maxY - minY
|
|
951
|
+
return fullBb
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Get all elements that have a BBox (excludes `<defs>`, `<title>`, etc).
|
|
956
|
+
* Note that 0-opacity, off-screen etc elements are still considered "visible"
|
|
957
|
+
* for this function.
|
|
958
|
+
* @function module:utilities.getVisibleElements
|
|
959
|
+
* @param {Element} parentElement - The parent DOM element to search within
|
|
960
|
+
* @returns {Element[]} All "visible" elements.
|
|
961
|
+
*/
|
|
962
|
+
export const getVisibleElements = (parentElement) => {
|
|
963
|
+
if (!parentElement) {
|
|
964
|
+
const svgContent = svgCanvas.getSvgContent()
|
|
965
|
+
parentElement = svgContent.children[0] // Prevent layers from being included
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const contentElems = []
|
|
969
|
+
const children = parentElement.children
|
|
970
|
+
// eslint-disable-next-line array-callback-return
|
|
971
|
+
Array.from(children, (elem) => {
|
|
972
|
+
if (elem.getBBox) {
|
|
973
|
+
contentElems.push(elem)
|
|
974
|
+
}
|
|
975
|
+
})
|
|
976
|
+
return contentElems.reverse()
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Get the bounding box for one or more stroked and/or transformed elements.
|
|
981
|
+
* @function module:utilities.getStrokedBBoxDefaultVisible
|
|
982
|
+
* @param {Element[]} elems - Array with DOM elements to check
|
|
983
|
+
* @returns {module:utilities.BBoxObject} A single bounding box object
|
|
984
|
+
*/
|
|
985
|
+
export const getStrokedBBoxDefaultVisible = (elems) => {
|
|
986
|
+
if (!elems) { elems = getVisibleElements() }
|
|
987
|
+
return getStrokedBBox(
|
|
988
|
+
elems,
|
|
989
|
+
svgCanvas.addSVGElementsFromJson,
|
|
990
|
+
svgCanvas.pathActions
|
|
991
|
+
)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Get the rotation angle of the given transform list.
|
|
996
|
+
* @function module:utilities.getRotationAngleFromTransformList
|
|
997
|
+
* @param {SVGTransformList} tlist - List of transforms
|
|
998
|
+
* @param {boolean} toRad - When true returns the value in radians rather than degrees
|
|
999
|
+
* @returns {Float} The angle in degrees or radians
|
|
1000
|
+
*/
|
|
1001
|
+
export const getRotationAngleFromTransformList = (tlist, toRad) => {
|
|
1002
|
+
if (!tlist) { return 0 } // <svg> element have no tlist
|
|
1003
|
+
for (let i = 0; i < tlist.numberOfItems; ++i) {
|
|
1004
|
+
const xform = tlist.getItem(i)
|
|
1005
|
+
if (xform.type === 4) {
|
|
1006
|
+
return toRad ? xform.angle * Math.PI / 180.0 : xform.angle
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return 0.0
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Get the rotation angle of the given/selected DOM element.
|
|
1014
|
+
* @function module:utilities.getRotationAngle
|
|
1015
|
+
* @param {Element} [elem] - DOM element to get the angle for. Default to first of selected elements.
|
|
1016
|
+
* @param {boolean} [toRad=false] - When true returns the value in radians rather than degrees
|
|
1017
|
+
* @returns {Float} The angle in degrees or radians
|
|
1018
|
+
*/
|
|
1019
|
+
export let getRotationAngle = (elem, toRad) => {
|
|
1020
|
+
const selected = elem || svgCanvas.getSelectedElements()[0]
|
|
1021
|
+
// find the rotation transform (if any) and set it
|
|
1022
|
+
const tlist = selected.transform?.baseVal
|
|
1023
|
+
return getRotationAngleFromTransformList(tlist, toRad)
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Get the reference element associated with the given attribute value.
|
|
1028
|
+
* @function module:utilities.getRefElem
|
|
1029
|
+
* @param {string} attrVal - The attribute value as a string
|
|
1030
|
+
* @returns {Element} Reference element
|
|
1031
|
+
*/
|
|
1032
|
+
export const getRefElem = (attrVal) => {
|
|
1033
|
+
return getElement(getUrlFromAttr(attrVal).substr(1))
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Get the reference element associated with the given attribute value.
|
|
1037
|
+
* @function module:utilities.getFeGaussianBlur
|
|
1038
|
+
* @param {any} Element
|
|
1039
|
+
* @returns {any} Reference element
|
|
1040
|
+
*/
|
|
1041
|
+
export const getFeGaussianBlur = (ele) => {
|
|
1042
|
+
if (ele?.firstChild?.tagName === 'feGaussianBlur') {
|
|
1043
|
+
return ele.firstChild
|
|
1044
|
+
} else {
|
|
1045
|
+
const childrens = ele.children
|
|
1046
|
+
// eslint-disable-next-line no-unused-vars
|
|
1047
|
+
for (const [_, value] of Object.entries(childrens)) {
|
|
1048
|
+
if (value.tagName === 'feGaussianBlur') {
|
|
1049
|
+
return value
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return null
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Get a DOM element by ID within the SVG root element.
|
|
1058
|
+
* @function module:utilities.getElement
|
|
1059
|
+
* @param {string} id - String with the element's new ID
|
|
1060
|
+
* @returns {?Element}
|
|
1061
|
+
*/
|
|
1062
|
+
export const getElement = (id) => {
|
|
1063
|
+
// querySelector lookup
|
|
1064
|
+
return svgroot_.querySelector('#' + id)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Assigns multiple attributes to an element.
|
|
1069
|
+
* @function module:utilities.assignAttributes
|
|
1070
|
+
* @param {Element} elem - DOM element to apply new attribute values to
|
|
1071
|
+
* @param {PlainObject<string, string>} attrs - Object with attribute keys/values
|
|
1072
|
+
* @param {Integer} [suspendLength] - Milliseconds to suspend redraw
|
|
1073
|
+
* @param {boolean} [unitCheck=false] - Boolean to indicate the need to use units.setUnitAttr
|
|
1074
|
+
* @returns {void}
|
|
1075
|
+
*/
|
|
1076
|
+
export const assignAttributes = (elem, attrs, suspendLength, unitCheck) => {
|
|
1077
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
1078
|
+
const ns = (key.substr(0, 4) === 'xml:'
|
|
1079
|
+
? NS.XML
|
|
1080
|
+
: key.substr(0, 6) === 'xlink:' ? NS.XLINK : null)
|
|
1081
|
+
if (value === undefined) {
|
|
1082
|
+
if (ns) {
|
|
1083
|
+
elem.removeAttributeNS(ns, key)
|
|
1084
|
+
} else {
|
|
1085
|
+
elem.removeAttribute(key)
|
|
1086
|
+
}
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
if (ns) {
|
|
1090
|
+
elem.setAttributeNS(ns, key, value)
|
|
1091
|
+
} else if (!unitCheck) {
|
|
1092
|
+
elem.setAttribute(key, value)
|
|
1093
|
+
} else {
|
|
1094
|
+
setUnitAttr(elem, key, value)
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Remove unneeded (default) attributes, making resulting SVG smaller.
|
|
1101
|
+
* @function module:utilities.cleanupElement
|
|
1102
|
+
* @param {Element} element - DOM element to clean up
|
|
1103
|
+
* @returns {void}
|
|
1104
|
+
*/
|
|
1105
|
+
export const cleanupElement = (element) => {
|
|
1106
|
+
const defaults = {
|
|
1107
|
+
'fill-opacity': 1,
|
|
1108
|
+
'stop-opacity': 1,
|
|
1109
|
+
opacity: 1,
|
|
1110
|
+
stroke: 'none',
|
|
1111
|
+
'stroke-dasharray': 'none',
|
|
1112
|
+
'stroke-linejoin': 'miter',
|
|
1113
|
+
'stroke-linecap': 'butt',
|
|
1114
|
+
'stroke-opacity': 1,
|
|
1115
|
+
'stroke-width': 1,
|
|
1116
|
+
rx: 0,
|
|
1117
|
+
ry: 0
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (element.nodeName === 'ellipse') {
|
|
1121
|
+
// Ellipse elements require rx and ry attributes
|
|
1122
|
+
delete defaults.rx
|
|
1123
|
+
delete defaults.ry
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
Object.entries(defaults).forEach(([attr, val]) => {
|
|
1127
|
+
if (element.getAttribute(attr) === String(val)) {
|
|
1128
|
+
element.removeAttribute(attr)
|
|
1129
|
+
}
|
|
1130
|
+
})
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Round value to for snapping.
|
|
1135
|
+
* @function module:utilities.snapToGrid
|
|
1136
|
+
* @param {Float} value
|
|
1137
|
+
* @returns {Integer}
|
|
1138
|
+
*/
|
|
1139
|
+
export const snapToGrid = (value) => {
|
|
1140
|
+
const unit = svgCanvas.getBaseUnit()
|
|
1141
|
+
let stepSize = svgCanvas.getSnappingStep()
|
|
1142
|
+
if (unit !== 'px') {
|
|
1143
|
+
stepSize *= getTypeMap()[unit]
|
|
1144
|
+
}
|
|
1145
|
+
value = Math.round(value / stepSize) * stepSize
|
|
1146
|
+
return value
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Prevents default browser click behaviour on the given element.
|
|
1151
|
+
* @function module:utilities.preventClickDefault
|
|
1152
|
+
* @param {Element} img - The DOM element to prevent the click on
|
|
1153
|
+
* @returns {void}
|
|
1154
|
+
*/
|
|
1155
|
+
export const preventClickDefault = (img) => {
|
|
1156
|
+
$click(img, (e) => {
|
|
1157
|
+
e.preventDefault()
|
|
1158
|
+
})
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* @callback module:utilities.GetNextID
|
|
1163
|
+
* @returns {string} The ID
|
|
1164
|
+
*/
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Whether a value is `null` or `undefined`.
|
|
1168
|
+
* @param {any} val
|
|
1169
|
+
* @returns {boolean}
|
|
1170
|
+
*/
|
|
1171
|
+
export const isNullish = (val) => {
|
|
1172
|
+
return val === null || val === undefined
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Overwrite methods for unit testing.
|
|
1177
|
+
* @function module:utilities.mock
|
|
1178
|
+
* @param {PlainObject} mockMethods
|
|
1179
|
+
* @param {module:utilities.getHref} mockMethods.getHref
|
|
1180
|
+
* @param {module:utilities.setHref} mockMethods.setHref
|
|
1181
|
+
* @param {module:utilities.getRotationAngle} mockMethods.getRotationAngle
|
|
1182
|
+
* @returns {void}
|
|
1183
|
+
*/
|
|
1184
|
+
export const mock = ({
|
|
1185
|
+
getHref: getHrefUser, setHref: setHrefUser, getRotationAngle: getRotationAngleUser
|
|
1186
|
+
}) => {
|
|
1187
|
+
getHref = getHrefUser
|
|
1188
|
+
setHref = setHrefUser
|
|
1189
|
+
getRotationAngle = getRotationAngleUser
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export const stringToHTML = (str) => {
|
|
1193
|
+
const parser = new DOMParser()
|
|
1194
|
+
const doc = parser.parseFromString(str, 'text/html')
|
|
1195
|
+
return doc.body.firstChild
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export const insertChildAtIndex = (parent, child, index = 0) => {
|
|
1199
|
+
const doc = stringToHTML(child)
|
|
1200
|
+
if (index >= parent.children.length) {
|
|
1201
|
+
parent.appendChild(doc)
|
|
1202
|
+
} else {
|
|
1203
|
+
parent.insertBefore(doc, parent.children[index])
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// shortcuts to common DOM functions
|
|
1208
|
+
export const $id = (id) => document.getElementById(id)
|
|
1209
|
+
export const $qq = (sel) => document.querySelector(sel)
|
|
1210
|
+
export const $qa = (sel) => [...document.querySelectorAll(sel)]
|
|
1211
|
+
export const $click = (element, handler) => {
|
|
1212
|
+
element.addEventListener('click', handler)
|
|
1213
|
+
element.addEventListener('touchend', handler)
|
|
1214
|
+
}
|