@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/selected-elem.js
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools for SVG selected element operation.
|
|
3
|
+
* @module selected-elem
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NS } from './namespaces.js'
|
|
10
|
+
import * as hstry from './history.js'
|
|
11
|
+
import * as pathModule from './path.js'
|
|
12
|
+
import {
|
|
13
|
+
getStrokedBBoxDefaultVisible,
|
|
14
|
+
setHref,
|
|
15
|
+
getElement,
|
|
16
|
+
getHref,
|
|
17
|
+
getVisibleElements,
|
|
18
|
+
findDefs,
|
|
19
|
+
getRotationAngle,
|
|
20
|
+
getRefElem,
|
|
21
|
+
getBBox as utilsGetBBox,
|
|
22
|
+
walkTreePost,
|
|
23
|
+
assignAttributes,
|
|
24
|
+
getFeGaussianBlur
|
|
25
|
+
} from './utilities.js'
|
|
26
|
+
import {
|
|
27
|
+
transformPoint,
|
|
28
|
+
matrixMultiply,
|
|
29
|
+
transformListToTransform
|
|
30
|
+
} from './math.js'
|
|
31
|
+
import { recalculateDimensions } from './recalculate.js'
|
|
32
|
+
import { isGecko } from '../../src/common/browser.js'
|
|
33
|
+
import { getParents } from '../../src/common/util.js'
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
MoveElementCommand,
|
|
37
|
+
BatchCommand,
|
|
38
|
+
InsertElementCommand,
|
|
39
|
+
RemoveElementCommand,
|
|
40
|
+
ChangeElementCommand
|
|
41
|
+
} = hstry
|
|
42
|
+
|
|
43
|
+
let svgCanvas = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @function module:selected-elem.init
|
|
47
|
+
* @param {module:selected-elem.elementContext} elementContext
|
|
48
|
+
* @returns {void}
|
|
49
|
+
*/
|
|
50
|
+
export const init = canvas => {
|
|
51
|
+
svgCanvas = canvas
|
|
52
|
+
svgCanvas.copySelectedElements = copySelectedElements
|
|
53
|
+
svgCanvas.groupSelectedElements = groupSelectedElements // Wraps all the selected elements in a group (`g`) element.
|
|
54
|
+
svgCanvas.pushGroupProperties = pushGroupProperty // Pushes all appropriate parent group properties down to its children
|
|
55
|
+
svgCanvas.ungroupSelectedElement = ungroupSelectedElement // Unwraps all the elements in a selected group (`g`) element
|
|
56
|
+
svgCanvas.moveToTopSelectedElement = moveToTopSelectedElem // Repositions the selected element to the bottom in the DOM to appear on top
|
|
57
|
+
svgCanvas.moveToBottomSelectedElement = moveToBottomSelectedElem // Repositions the selected element to the top in the DOM to appear under other elements
|
|
58
|
+
svgCanvas.moveUpDownSelected = moveUpDownSelected // Moves the select element up or down the stack, based on the visibly
|
|
59
|
+
svgCanvas.moveSelectedElements = moveSelectedElements // Moves selected elements on the X/Y axis.
|
|
60
|
+
svgCanvas.cloneSelectedElements = cloneSelectedElements // Create deep DOM copies (clones) of all selected elements and move them slightly
|
|
61
|
+
svgCanvas.alignSelectedElements = alignSelectedElements // Aligns selected elements.
|
|
62
|
+
svgCanvas.updateCanvas = updateCanvas // Updates the editor canvas width/height/position after a zoom has occurred.
|
|
63
|
+
svgCanvas.cycleElement = cycleElement // Select the next/previous element within the current layer.
|
|
64
|
+
svgCanvas.deleteSelectedElements = deleteSelectedElements // Removes all selected elements from the DOM and adds the change to the history
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Repositions the selected element to the bottom in the DOM to appear on top of
|
|
69
|
+
* other elements.
|
|
70
|
+
* @function module:selected-elem.SvgCanvas#moveToTopSelectedElem
|
|
71
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
74
|
+
const moveToTopSelectedElem = () => {
|
|
75
|
+
const [selected] = svgCanvas.getSelectedElements()
|
|
76
|
+
if (selected) {
|
|
77
|
+
const t = selected
|
|
78
|
+
const oldParent = t.parentNode
|
|
79
|
+
const oldNextSibling = t.nextSibling
|
|
80
|
+
t.parentNode.append(t)
|
|
81
|
+
// If the element actually moved position, add the command and fire the changed
|
|
82
|
+
// event handler.
|
|
83
|
+
if (oldNextSibling !== t.nextSibling) {
|
|
84
|
+
svgCanvas.addCommandToHistory(
|
|
85
|
+
new MoveElementCommand(t, oldNextSibling, oldParent, 'top')
|
|
86
|
+
)
|
|
87
|
+
svgCanvas.call('changed', [t])
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Repositions the selected element to the top in the DOM to appear under
|
|
94
|
+
* other elements.
|
|
95
|
+
* @function module:selected-elem.SvgCanvas#moveToBottomSelectedElement
|
|
96
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
const moveToBottomSelectedElem = () => {
|
|
100
|
+
const [selected] = svgCanvas.getSelectedElements()
|
|
101
|
+
if (selected) {
|
|
102
|
+
let t = selected
|
|
103
|
+
const oldParent = t.parentNode
|
|
104
|
+
const oldNextSibling = t.nextSibling
|
|
105
|
+
let { firstChild } = t.parentNode
|
|
106
|
+
if (firstChild.tagName === 'title') {
|
|
107
|
+
firstChild = firstChild.nextSibling
|
|
108
|
+
}
|
|
109
|
+
// This can probably be removed, as the defs should not ever apppear
|
|
110
|
+
// inside a layer group
|
|
111
|
+
if (firstChild.tagName === 'defs') {
|
|
112
|
+
firstChild = firstChild.nextSibling
|
|
113
|
+
}
|
|
114
|
+
t = t.parentNode.insertBefore(t, firstChild)
|
|
115
|
+
// If the element actually moved position, add the command and fire the changed
|
|
116
|
+
// event handler.
|
|
117
|
+
if (oldNextSibling !== t.nextSibling) {
|
|
118
|
+
svgCanvas.addCommandToHistory(
|
|
119
|
+
new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom')
|
|
120
|
+
)
|
|
121
|
+
svgCanvas.call('changed', [t])
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Moves the select element up or down the stack, based on the visibly
|
|
128
|
+
* intersecting elements.
|
|
129
|
+
* @function module:selected-elem.SvgCanvas#moveUpDownSelected
|
|
130
|
+
* @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
|
|
131
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
const moveUpDownSelected = dir => {
|
|
135
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
136
|
+
const selected = selectedElements[0]
|
|
137
|
+
if (!selected) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
svgCanvas.setCurBBoxes([])
|
|
142
|
+
let closest
|
|
143
|
+
let foundCur
|
|
144
|
+
// jQuery sorts this list
|
|
145
|
+
const list = svgCanvas.getIntersectionList(
|
|
146
|
+
getStrokedBBoxDefaultVisible([selected])
|
|
147
|
+
)
|
|
148
|
+
if (dir === 'Down') {
|
|
149
|
+
list.reverse()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Array.prototype.forEach.call(list, el => {
|
|
153
|
+
if (!foundCur) {
|
|
154
|
+
if (el === selected) {
|
|
155
|
+
foundCur = true
|
|
156
|
+
}
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
if (closest === undefined) {
|
|
160
|
+
closest = el
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
})
|
|
164
|
+
if (!closest) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const t = selected
|
|
169
|
+
const oldParent = t.parentNode
|
|
170
|
+
const oldNextSibling = t.nextSibling
|
|
171
|
+
if (dir === 'Down') {
|
|
172
|
+
closest.insertAdjacentElement('beforebegin', t)
|
|
173
|
+
} else {
|
|
174
|
+
closest.insertAdjacentElement('afterend', t)
|
|
175
|
+
}
|
|
176
|
+
// If the element actually moved position, add the command and fire the changed
|
|
177
|
+
// event handler.
|
|
178
|
+
if (oldNextSibling !== t.nextSibling) {
|
|
179
|
+
svgCanvas.addCommandToHistory(
|
|
180
|
+
new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir)
|
|
181
|
+
)
|
|
182
|
+
svgCanvas.call('changed', [t])
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Moves selected elements on the X/Y axis.
|
|
188
|
+
* @function module:selected-elem.SvgCanvas#moveSelectedElements
|
|
189
|
+
* @param {Float} dx - Float with the distance to move on the x-axis
|
|
190
|
+
* @param {Float} dy - Float with the distance to move on the y-axis
|
|
191
|
+
* @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
|
|
192
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
193
|
+
* @returns {BatchCommand|void} Batch command for the move
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
const moveSelectedElements = (dx, dy, undoable = true) => {
|
|
197
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
198
|
+
const zoom = svgCanvas.getZoom()
|
|
199
|
+
// if undoable is not sent, default to true
|
|
200
|
+
// if single values, scale them to the zoom
|
|
201
|
+
if (!Array.isArray(dx)) {
|
|
202
|
+
dx /= zoom
|
|
203
|
+
dy /= zoom
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const batchCmd = new BatchCommand('position')
|
|
207
|
+
selectedElements.forEach((selected, i) => {
|
|
208
|
+
if (selected) {
|
|
209
|
+
const xform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
210
|
+
const tlist = selected.transform?.baseVal
|
|
211
|
+
|
|
212
|
+
// dx and dy could be arrays
|
|
213
|
+
if (Array.isArray(dx)) {
|
|
214
|
+
xform.setTranslate(dx[i], dy[i])
|
|
215
|
+
} else {
|
|
216
|
+
xform.setTranslate(dx, dy)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (tlist.numberOfItems) {
|
|
220
|
+
tlist.insertItemBefore(xform, 0)
|
|
221
|
+
} else {
|
|
222
|
+
tlist.appendItem(xform)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const cmd = recalculateDimensions(selected)
|
|
226
|
+
if (cmd) {
|
|
227
|
+
batchCmd.addSubCommand(cmd)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
svgCanvas
|
|
231
|
+
.gettingSelectorManager()
|
|
232
|
+
.requestSelector(selected)
|
|
233
|
+
.resize()
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
if (!batchCmd.isEmpty()) {
|
|
237
|
+
if (undoable) {
|
|
238
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
239
|
+
}
|
|
240
|
+
svgCanvas.call('changed', selectedElements)
|
|
241
|
+
return batchCmd
|
|
242
|
+
}
|
|
243
|
+
return undefined
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create deep DOM copies (clones) of all selected elements and move them slightly
|
|
248
|
+
* from their originals.
|
|
249
|
+
* @function module:selected-elem.SvgCanvas#cloneSelectedElements
|
|
250
|
+
* @param {Float} x Float with the distance to move on the x-axis
|
|
251
|
+
* @param {Float} y Float with the distance to move on the y-axis
|
|
252
|
+
* @returns {void}
|
|
253
|
+
*/
|
|
254
|
+
const cloneSelectedElements = (x, y) => {
|
|
255
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
256
|
+
const currentGroup = svgCanvas.getCurrentGroup()
|
|
257
|
+
let i
|
|
258
|
+
let elem
|
|
259
|
+
const batchCmd = new BatchCommand('Clone Elements')
|
|
260
|
+
// find all the elements selected (stop at first null)
|
|
261
|
+
const len = selectedElements.length
|
|
262
|
+
|
|
263
|
+
const index = el => {
|
|
264
|
+
if (!el) return -1
|
|
265
|
+
let i = 0
|
|
266
|
+
do {
|
|
267
|
+
i++
|
|
268
|
+
} while (el === el.previousElementSibling)
|
|
269
|
+
return i
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sorts an array numerically and ascending.
|
|
274
|
+
* @param {Element} a
|
|
275
|
+
* @param {Element} b
|
|
276
|
+
* @returns {Integer}
|
|
277
|
+
*/
|
|
278
|
+
const sortfunction = (a, b) => {
|
|
279
|
+
return index(b) - index(a)
|
|
280
|
+
}
|
|
281
|
+
selectedElements.sort(sortfunction)
|
|
282
|
+
for (i = 0; i < len; ++i) {
|
|
283
|
+
elem = selectedElements[i]
|
|
284
|
+
if (!elem) {
|
|
285
|
+
break
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// use slice to quickly get the subset of elements we need
|
|
289
|
+
const copiedElements = selectedElements.slice(0, i)
|
|
290
|
+
svgCanvas.clearSelection(true)
|
|
291
|
+
// note that we loop in the reverse way because of the way elements are added
|
|
292
|
+
// to the selectedElements array (top-first)
|
|
293
|
+
const drawing = svgCanvas.getDrawing()
|
|
294
|
+
i = copiedElements.length
|
|
295
|
+
while (i--) {
|
|
296
|
+
// clone each element and replace it within copiedElements
|
|
297
|
+
elem = copiedElements[i] = drawing.copyElem(copiedElements[i])
|
|
298
|
+
;(currentGroup || drawing.getCurrentLayer()).append(elem)
|
|
299
|
+
batchCmd.addSubCommand(new InsertElementCommand(elem))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!batchCmd.isEmpty()) {
|
|
303
|
+
svgCanvas.addToSelection(copiedElements.reverse()) // Need to reverse for correct selection-adding
|
|
304
|
+
moveSelectedElements(x, y, false)
|
|
305
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Aligns selected elements.
|
|
310
|
+
* @function module:selected-elem.SvgCanvas#alignSelectedElements
|
|
311
|
+
* @param {string} type - String with single character indicating the alignment type
|
|
312
|
+
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
|
|
313
|
+
* @returns {void}
|
|
314
|
+
*/
|
|
315
|
+
const alignSelectedElements = (type, relativeTo) => {
|
|
316
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
317
|
+
const bboxes = [] // angles = [];
|
|
318
|
+
const len = selectedElements.length
|
|
319
|
+
if (!len) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
let minx = Number.MAX_VALUE
|
|
323
|
+
let maxx = Number.MIN_VALUE
|
|
324
|
+
let miny = Number.MAX_VALUE
|
|
325
|
+
let maxy = Number.MIN_VALUE
|
|
326
|
+
|
|
327
|
+
const isHorizontalAlign = (type) => ['l', 'c', 'r', 'left', 'center', 'right'].includes(type)
|
|
328
|
+
const isVerticalAlign = (type) => ['t', 'm', 'b', 'top', 'middle', 'bottom'].includes(type)
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < len; ++i) {
|
|
331
|
+
if (!selectedElements[i]) {
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
const elem = selectedElements[i]
|
|
335
|
+
bboxes[i] = getStrokedBBoxDefaultVisible([elem])
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// distribute horizontal and vertical align is not support smallest and largest
|
|
339
|
+
if (['smallest', 'largest'].includes(relativeTo) && ['dh', 'distrib_horiz', 'dv', 'distrib_verti'].includes(type)) {
|
|
340
|
+
relativeTo = 'selected'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
switch (relativeTo) {
|
|
344
|
+
case 'smallest':
|
|
345
|
+
if (isHorizontalAlign(type) || isVerticalAlign(type)) {
|
|
346
|
+
const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
|
|
347
|
+
const minBbox = sortedBboxes[0]
|
|
348
|
+
minx = minBbox.x
|
|
349
|
+
miny = minBbox.y
|
|
350
|
+
maxx = minBbox.x + minBbox.width
|
|
351
|
+
maxy = minBbox.y + minBbox.height
|
|
352
|
+
}
|
|
353
|
+
break
|
|
354
|
+
case 'largest':
|
|
355
|
+
if (isHorizontalAlign(type) || isVerticalAlign(type)) {
|
|
356
|
+
const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
|
|
357
|
+
const maxBbox = sortedBboxes[bboxes.length - 1]
|
|
358
|
+
minx = maxBbox.x
|
|
359
|
+
miny = maxBbox.y
|
|
360
|
+
maxx = maxBbox.x + maxBbox.width
|
|
361
|
+
maxy = maxBbox.y + maxBbox.height
|
|
362
|
+
}
|
|
363
|
+
break
|
|
364
|
+
case 'page':
|
|
365
|
+
minx = 0
|
|
366
|
+
miny = 0
|
|
367
|
+
maxx = svgCanvas.getContentW()
|
|
368
|
+
maxy = svgCanvas.getContentH()
|
|
369
|
+
break
|
|
370
|
+
default:
|
|
371
|
+
// 'selected'
|
|
372
|
+
minx = Math.min(...bboxes.map(box => box.x))
|
|
373
|
+
miny = Math.min(...bboxes.map(box => box.y))
|
|
374
|
+
maxx = Math.max(...bboxes.map(box => box.x + box.width))
|
|
375
|
+
maxy = Math.max(...bboxes.map(box => box.y + box.height))
|
|
376
|
+
break
|
|
377
|
+
} // adjust min/max
|
|
378
|
+
|
|
379
|
+
let dx = []
|
|
380
|
+
let dy = []
|
|
381
|
+
|
|
382
|
+
if (['dh', 'distrib_horiz'].includes(type)) { // distribute horizontal align
|
|
383
|
+
[dx, dy] = _getDistributeHorizontalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
384
|
+
} else if (['dv', 'distrib_verti'].includes(type)) { // distribute vertical align
|
|
385
|
+
[dx, dy] = _getDistributeVerticalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
386
|
+
} else { // normal align (top, left, right, ...)
|
|
387
|
+
[dx, dy] = _getNormalDistances(type, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
moveSelectedElements(dx, dy)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Aligns selected elements.
|
|
395
|
+
* @function module:selected-elem.SvgCanvas#alignSelectedElements
|
|
396
|
+
* @param {string} type - String with single character indicating the alignment type
|
|
397
|
+
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
|
|
398
|
+
* @returns {void}
|
|
399
|
+
*/
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* get distribution horizontal distances.
|
|
403
|
+
* (internal call only)
|
|
404
|
+
*
|
|
405
|
+
* @param {string} relativeTo
|
|
406
|
+
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
407
|
+
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
408
|
+
* @param {Float} minx - selected area min-x
|
|
409
|
+
* @param {Float} maxx - selected area max-x
|
|
410
|
+
* @param {Float} miny - selected area min-y
|
|
411
|
+
* @param {Float} maxy - selected area max-y
|
|
412
|
+
* @returns {Array.Float[]} x and y distances array
|
|
413
|
+
* @private
|
|
414
|
+
*/
|
|
415
|
+
const _getDistributeHorizontalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
416
|
+
const dx = []
|
|
417
|
+
const dy = []
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < selectedElements.length; i++) {
|
|
420
|
+
dy[i] = 0
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const bboxesSortedClone = bboxes
|
|
424
|
+
.slice()
|
|
425
|
+
.sort((firstBox, secondBox) => {
|
|
426
|
+
const firstMaxX = firstBox.x + firstBox.width
|
|
427
|
+
const secondMaxX = secondBox.x + secondBox.width
|
|
428
|
+
|
|
429
|
+
if (firstMaxX === secondMaxX) { return 0 } else if (firstMaxX > secondMaxX) { return 1 } else { return -1 }
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
if (relativeTo === 'page') {
|
|
433
|
+
bboxesSortedClone.unshift({ x: 0, y: 0, width: 0, height: maxy }) // virtual left box
|
|
434
|
+
bboxesSortedClone.push({ x: maxx, y: 0, width: 0, height: maxy }) // virtual right box
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const totalWidth = maxx - minx
|
|
438
|
+
const totalBoxWidth = bboxesSortedClone.map(b => b.width).reduce((w1, w2) => w1 + w2, 0)
|
|
439
|
+
const space = (totalWidth - totalBoxWidth) / (bboxesSortedClone.length - 1)
|
|
440
|
+
const _dx = []
|
|
441
|
+
|
|
442
|
+
for (let i = 0; i < bboxesSortedClone.length; ++i) {
|
|
443
|
+
_dx[i] = 0
|
|
444
|
+
|
|
445
|
+
if (i === 0) { continue }
|
|
446
|
+
|
|
447
|
+
const orgX = bboxesSortedClone[i].x
|
|
448
|
+
bboxesSortedClone[i].x = bboxesSortedClone[i - 1].x + bboxesSortedClone[i - 1].width + space
|
|
449
|
+
_dx[i] = bboxesSortedClone[i].x - orgX
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
bboxesSortedClone.forEach((boxClone, idx) => {
|
|
453
|
+
const orgIdx = bboxes.findIndex(box => box === boxClone)
|
|
454
|
+
if (orgIdx !== -1) {
|
|
455
|
+
dx[orgIdx] = _dx[idx]
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
return [dx, dy]
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* get distribution vertical distances.
|
|
464
|
+
* (internal call only)
|
|
465
|
+
*
|
|
466
|
+
* @param {string} relativeTo
|
|
467
|
+
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
468
|
+
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
469
|
+
* @param {Float} minx - selected area min-x
|
|
470
|
+
* @param {Float} maxx - selected area max-x
|
|
471
|
+
* @param {Float} miny - selected area min-y
|
|
472
|
+
* @param {Float} maxy - selected area max-y
|
|
473
|
+
* @returns {Array.Float[]}} x and y distances array
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
const _getDistributeVerticalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
477
|
+
const dx = []
|
|
478
|
+
const dy = []
|
|
479
|
+
|
|
480
|
+
for (let i = 0; i < selectedElements.length; i++) {
|
|
481
|
+
dx[i] = 0
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const bboxesSortedClone = bboxes
|
|
485
|
+
.slice()
|
|
486
|
+
.sort((firstBox, secondBox) => {
|
|
487
|
+
const firstMaxY = firstBox.y + firstBox.height
|
|
488
|
+
const secondMaxY = secondBox.y + secondBox.height
|
|
489
|
+
|
|
490
|
+
if (firstMaxY === secondMaxY) { return 0 } else if (firstMaxY > secondMaxY) { return 1 } else { return -1 }
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
if (relativeTo === 'page') {
|
|
494
|
+
bboxesSortedClone.unshift({ x: 0, y: 0, width: maxx, height: 0 }) // virtual top box
|
|
495
|
+
bboxesSortedClone.push({ x: 0, y: maxy, width: maxx, height: 0 }) // virtual bottom box
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const totalHeight = maxy - miny
|
|
499
|
+
const totalBoxHeight = bboxesSortedClone.map(b => b.height).reduce((h1, h2) => h1 + h2, 0)
|
|
500
|
+
const space = (totalHeight - totalBoxHeight) / (bboxesSortedClone.length - 1)
|
|
501
|
+
const _dy = []
|
|
502
|
+
|
|
503
|
+
for (let i = 0; i < bboxesSortedClone.length; ++i) {
|
|
504
|
+
_dy[i] = 0
|
|
505
|
+
|
|
506
|
+
if (i === 0) { continue }
|
|
507
|
+
|
|
508
|
+
const orgY = bboxesSortedClone[i].y
|
|
509
|
+
bboxesSortedClone[i].y = bboxesSortedClone[i - 1].y + bboxesSortedClone[i - 1].height + space
|
|
510
|
+
_dy[i] = bboxesSortedClone[i].y - orgY
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
bboxesSortedClone.forEach((boxClone, idx) => {
|
|
514
|
+
const orgIdx = bboxes.findIndex(box => box === boxClone)
|
|
515
|
+
if (orgIdx !== -1) {
|
|
516
|
+
dy[orgIdx] = _dy[idx]
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
return [dx, dy]
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* get normal align distances.
|
|
525
|
+
* (internal call only)
|
|
526
|
+
*
|
|
527
|
+
* @param {string} type
|
|
528
|
+
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
529
|
+
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
530
|
+
* @param {Float} minx - selected area min-x
|
|
531
|
+
* @param {Float} maxx - selected area max-x
|
|
532
|
+
* @param {Float} miny - selected area min-y
|
|
533
|
+
* @param {Float} maxy - selected area max-y
|
|
534
|
+
* @returns {Array.Float[]} x and y distances array
|
|
535
|
+
* @private
|
|
536
|
+
*/
|
|
537
|
+
const _getNormalDistances = (type, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
538
|
+
const len = selectedElements.length
|
|
539
|
+
const dx = new Array(len)
|
|
540
|
+
const dy = new Array(len)
|
|
541
|
+
|
|
542
|
+
for (let i = 0; i < len; ++i) {
|
|
543
|
+
if (!selectedElements[i]) {
|
|
544
|
+
break
|
|
545
|
+
}
|
|
546
|
+
// const elem = selectedElements[i];
|
|
547
|
+
const bbox = bboxes[i]
|
|
548
|
+
dx[i] = 0
|
|
549
|
+
dy[i] = 0
|
|
550
|
+
|
|
551
|
+
switch (type) {
|
|
552
|
+
case 'l': // left (horizontal)
|
|
553
|
+
case 'left': // left (horizontal)
|
|
554
|
+
dx[i] = minx - bbox.x
|
|
555
|
+
break
|
|
556
|
+
case 'c': // center (horizontal)
|
|
557
|
+
case 'center': // center (horizontal)
|
|
558
|
+
dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2)
|
|
559
|
+
break
|
|
560
|
+
case 'r': // right (horizontal)
|
|
561
|
+
case 'right': // right (horizontal)
|
|
562
|
+
dx[i] = maxx - (bbox.x + bbox.width)
|
|
563
|
+
break
|
|
564
|
+
case 't': // top (vertical)
|
|
565
|
+
case 'top': // top (vertical)
|
|
566
|
+
dy[i] = miny - bbox.y
|
|
567
|
+
break
|
|
568
|
+
case 'm': // middle (vertical)
|
|
569
|
+
case 'middle': // middle (vertical)
|
|
570
|
+
dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2)
|
|
571
|
+
break
|
|
572
|
+
case 'b': // bottom (vertical)
|
|
573
|
+
case 'bottom': // bottom (vertical)
|
|
574
|
+
dy[i] = maxy - (bbox.y + bbox.height)
|
|
575
|
+
break
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return [dx, dy]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Removes all selected elements from the DOM and adds the change to the
|
|
584
|
+
* history stack.
|
|
585
|
+
* @function module:selected-elem.SvgCanvas#deleteSelectedElements
|
|
586
|
+
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
587
|
+
* @returns {void}
|
|
588
|
+
*/
|
|
589
|
+
const deleteSelectedElements = () => {
|
|
590
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
591
|
+
const batchCmd = new BatchCommand('Delete Elements')
|
|
592
|
+
const selectedCopy = [] // selectedElements is being deleted
|
|
593
|
+
|
|
594
|
+
selectedElements.forEach(selected => {
|
|
595
|
+
if (selected) {
|
|
596
|
+
let parent = selected.parentNode
|
|
597
|
+
let t = selected
|
|
598
|
+
// this will unselect the element and remove the selectedOutline
|
|
599
|
+
svgCanvas.gettingSelectorManager().releaseSelector(t)
|
|
600
|
+
// Remove the path if present.
|
|
601
|
+
pathModule.removePath_(t.id)
|
|
602
|
+
// Get the parent if it's a single-child anchor
|
|
603
|
+
if (parent.tagName === 'a' && parent.childNodes.length === 1) {
|
|
604
|
+
t = parent
|
|
605
|
+
parent = parent.parentNode
|
|
606
|
+
}
|
|
607
|
+
const { nextSibling } = t
|
|
608
|
+
t.remove()
|
|
609
|
+
const elem = t
|
|
610
|
+
selectedCopy.push(selected) // for the copy
|
|
611
|
+
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent))
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
svgCanvas.setEmptySelectedElements()
|
|
615
|
+
|
|
616
|
+
if (!batchCmd.isEmpty()) {
|
|
617
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
618
|
+
}
|
|
619
|
+
svgCanvas.call('changed', selectedCopy)
|
|
620
|
+
svgCanvas.clearSelection()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Remembers the current selected elements on the clipboard.
|
|
625
|
+
* @function module:selected-elem.SvgCanvas#copySelectedElements
|
|
626
|
+
* @returns {void}
|
|
627
|
+
*/
|
|
628
|
+
const copySelectedElements = () => {
|
|
629
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
630
|
+
const data = JSON.stringify(
|
|
631
|
+
selectedElements.map(x => svgCanvas.getJsonFromSvgElements(x))
|
|
632
|
+
)
|
|
633
|
+
// Use sessionStorage for the clipboard data.
|
|
634
|
+
sessionStorage.setItem(svgCanvas.getClipboardID(), data)
|
|
635
|
+
svgCanvas.flashStorage()
|
|
636
|
+
|
|
637
|
+
// Context menu might not exist (it is provided by editor.js).
|
|
638
|
+
const canvMenu = document.getElementById('se-cmenu_canvas')
|
|
639
|
+
canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place')
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Wraps all the selected elements in a group (`g`) element.
|
|
644
|
+
* @function module:selected-elem.SvgCanvas#groupSelectedElements
|
|
645
|
+
* @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
|
|
646
|
+
* @param {string} [urlArg]
|
|
647
|
+
* @returns {void}
|
|
648
|
+
*/
|
|
649
|
+
const groupSelectedElements = (type, urlArg) => {
|
|
650
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
651
|
+
if (!type) {
|
|
652
|
+
type = 'g'
|
|
653
|
+
}
|
|
654
|
+
let cmdStr = ''
|
|
655
|
+
let url
|
|
656
|
+
|
|
657
|
+
switch (type) {
|
|
658
|
+
case 'a': {
|
|
659
|
+
cmdStr = 'Make hyperlink'
|
|
660
|
+
url = urlArg || ''
|
|
661
|
+
break
|
|
662
|
+
}
|
|
663
|
+
default: {
|
|
664
|
+
type = 'g'
|
|
665
|
+
cmdStr = 'Group Elements'
|
|
666
|
+
break
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const batchCmd = new BatchCommand(cmdStr)
|
|
671
|
+
|
|
672
|
+
// create and insert the group element
|
|
673
|
+
const g = svgCanvas.addSVGElementsFromJson({
|
|
674
|
+
element: type,
|
|
675
|
+
attr: {
|
|
676
|
+
id: svgCanvas.getNextId()
|
|
677
|
+
}
|
|
678
|
+
})
|
|
679
|
+
if (type === 'a') {
|
|
680
|
+
setHref(g, url)
|
|
681
|
+
}
|
|
682
|
+
batchCmd.addSubCommand(new InsertElementCommand(g))
|
|
683
|
+
|
|
684
|
+
// now move all children into the group
|
|
685
|
+
let i = selectedElements.length
|
|
686
|
+
while (i--) {
|
|
687
|
+
let elem = selectedElements[i]
|
|
688
|
+
if (!elem) {
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (
|
|
693
|
+
elem.parentNode.tagName === 'a' &&
|
|
694
|
+
elem.parentNode.childNodes.length === 1
|
|
695
|
+
) {
|
|
696
|
+
elem = elem.parentNode
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const oldNextSibling = elem.nextSibling
|
|
700
|
+
const oldParent = elem.parentNode
|
|
701
|
+
g.append(elem)
|
|
702
|
+
batchCmd.addSubCommand(
|
|
703
|
+
new MoveElementCommand(elem, oldNextSibling, oldParent)
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
if (!batchCmd.isEmpty()) {
|
|
707
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// update selection
|
|
711
|
+
svgCanvas.selectOnly([g], true)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Pushes all appropriate parent group properties down to its children, then
|
|
716
|
+
* removes them from the group.
|
|
717
|
+
* @function module:selected-elem.SvgCanvas#pushGroupProperty
|
|
718
|
+
* @param {SVGAElement|SVGGElement} g
|
|
719
|
+
* @param {boolean} undoable
|
|
720
|
+
* @returns {BatchCommand|void}
|
|
721
|
+
*/
|
|
722
|
+
const pushGroupProperty = (g, undoable) => {
|
|
723
|
+
const children = g.childNodes
|
|
724
|
+
const len = children.length
|
|
725
|
+
const xform = g.getAttribute('transform')
|
|
726
|
+
|
|
727
|
+
const glist = g.transform.baseVal
|
|
728
|
+
const m = transformListToTransform(glist).matrix
|
|
729
|
+
|
|
730
|
+
const batchCmd = new BatchCommand('Push group properties')
|
|
731
|
+
|
|
732
|
+
// TODO: get all fill/stroke properties from the group that we are about to destroy
|
|
733
|
+
// "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
|
|
734
|
+
// "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
|
|
735
|
+
// "stroke-width"
|
|
736
|
+
// and then for each child, if they do not have the attribute (or the value is 'inherit')
|
|
737
|
+
// then set the child's attribute
|
|
738
|
+
|
|
739
|
+
const gangle = getRotationAngle(g)
|
|
740
|
+
|
|
741
|
+
const gattrs = {
|
|
742
|
+
filter: g.getAttribute('filter'),
|
|
743
|
+
opacity: g.getAttribute('opacity')
|
|
744
|
+
}
|
|
745
|
+
let gfilter
|
|
746
|
+
let gblur
|
|
747
|
+
let changes
|
|
748
|
+
const drawing = svgCanvas.getDrawing()
|
|
749
|
+
|
|
750
|
+
for (let i = 0; i < len; i++) {
|
|
751
|
+
const elem = children[i]
|
|
752
|
+
|
|
753
|
+
if (elem.nodeType !== 1) {
|
|
754
|
+
continue
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (gattrs.opacity !== null && gattrs.opacity !== 1) {
|
|
758
|
+
// const c_opac = elem.getAttribute('opacity') || 1;
|
|
759
|
+
const newOpac =
|
|
760
|
+
Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) /
|
|
761
|
+
100
|
|
762
|
+
svgCanvas.changeSelectedAttribute('opacity', newOpac, [elem])
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (gattrs.filter) {
|
|
766
|
+
let cblur = svgCanvas.getBlur(elem)
|
|
767
|
+
const origCblur = cblur
|
|
768
|
+
if (!gblur) {
|
|
769
|
+
gblur = svgCanvas.getBlur(g)
|
|
770
|
+
}
|
|
771
|
+
if (cblur) {
|
|
772
|
+
// Is this formula correct?
|
|
773
|
+
cblur = Number(gblur) + Number(cblur)
|
|
774
|
+
} else if (cblur === 0) {
|
|
775
|
+
cblur = gblur
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// If child has no current filter, get group's filter or clone it.
|
|
779
|
+
if (!origCblur) {
|
|
780
|
+
// Set group's filter to use first child's ID
|
|
781
|
+
if (!gfilter) {
|
|
782
|
+
gfilter = getRefElem(gattrs.filter)
|
|
783
|
+
} else {
|
|
784
|
+
// Clone the group's filter
|
|
785
|
+
gfilter = drawing.copyElem(gfilter)
|
|
786
|
+
findDefs().append(gfilter)
|
|
787
|
+
|
|
788
|
+
// const filterElem = getRefElem(gfilter);
|
|
789
|
+
const blurElem = getFeGaussianBlur(gfilter)
|
|
790
|
+
// Change this in future for different filters
|
|
791
|
+
const suffix =
|
|
792
|
+
blurElem?.tagName === 'feGaussianBlur' ? 'blur' : 'filter'
|
|
793
|
+
gfilter.id = elem.id + '_' + suffix
|
|
794
|
+
svgCanvas.changeSelectedAttribute(
|
|
795
|
+
'filter',
|
|
796
|
+
'url(#' + gfilter.id + ')',
|
|
797
|
+
[elem]
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
} else {
|
|
801
|
+
gfilter = getRefElem(elem.getAttribute('filter'))
|
|
802
|
+
}
|
|
803
|
+
// const filterElem = getRefElem(gfilter);
|
|
804
|
+
const blurElem = getFeGaussianBlur(gfilter)
|
|
805
|
+
|
|
806
|
+
// Update blur value
|
|
807
|
+
if (cblur) {
|
|
808
|
+
svgCanvas.changeSelectedAttribute('stdDeviation', cblur, [blurElem])
|
|
809
|
+
svgCanvas.setBlurOffsets(gfilter, cblur)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
let chtlist = elem.transform?.baseVal
|
|
814
|
+
|
|
815
|
+
// Don't process gradient transforms
|
|
816
|
+
if (elem.tagName.includes('Gradient')) {
|
|
817
|
+
chtlist = null
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Hopefully not a problem to add this. Necessary for elements like <desc/>
|
|
821
|
+
if (!chtlist) {
|
|
822
|
+
continue
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Apparently <defs> can get get a transformlist, but we don't want it to have one!
|
|
826
|
+
if (elem.tagName === 'defs') {
|
|
827
|
+
continue
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (glist.numberOfItems) {
|
|
831
|
+
// TODO: if the group's transform is just a rotate, we can always transfer the
|
|
832
|
+
// rotate() down to the children (collapsing consecutive rotates and factoring
|
|
833
|
+
// out any translates)
|
|
834
|
+
if (gangle && glist.numberOfItems === 1) {
|
|
835
|
+
// [Rg] [Rc] [Mc]
|
|
836
|
+
// we want [Tr] [Rc2] [Mc] where:
|
|
837
|
+
// - [Rc2] is at the child's current center but has the
|
|
838
|
+
// sum of the group and child's rotation angles
|
|
839
|
+
// - [Tr] is the equivalent translation that this child
|
|
840
|
+
// undergoes if the group wasn't there
|
|
841
|
+
|
|
842
|
+
// [Tr] = [Rg] [Rc] [Rc2_inv]
|
|
843
|
+
|
|
844
|
+
// get group's rotation matrix (Rg)
|
|
845
|
+
const rgm = glist.getItem(0).matrix
|
|
846
|
+
|
|
847
|
+
// get child's rotation matrix (Rc)
|
|
848
|
+
let rcm = svgCanvas.getSvgRoot().createSVGMatrix()
|
|
849
|
+
const cangle = getRotationAngle(elem)
|
|
850
|
+
if (cangle) {
|
|
851
|
+
rcm = chtlist.getItem(0).matrix
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// get child's old center of rotation
|
|
855
|
+
const cbox = utilsGetBBox(elem)
|
|
856
|
+
const ceqm = transformListToTransform(chtlist).matrix
|
|
857
|
+
const coldc = transformPoint(
|
|
858
|
+
cbox.x + cbox.width / 2,
|
|
859
|
+
cbox.y + cbox.height / 2,
|
|
860
|
+
ceqm
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
// sum group and child's angles
|
|
864
|
+
const sangle = gangle + cangle
|
|
865
|
+
|
|
866
|
+
// get child's rotation at the old center (Rc2_inv)
|
|
867
|
+
const r2 = svgCanvas.getSvgRoot().createSVGTransform()
|
|
868
|
+
r2.setRotate(sangle, coldc.x, coldc.y)
|
|
869
|
+
|
|
870
|
+
// calculate equivalent translate
|
|
871
|
+
const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse())
|
|
872
|
+
|
|
873
|
+
// set up tlist
|
|
874
|
+
if (cangle) {
|
|
875
|
+
chtlist.removeItem(0)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (sangle) {
|
|
879
|
+
if (chtlist.numberOfItems) {
|
|
880
|
+
chtlist.insertItemBefore(r2, 0)
|
|
881
|
+
} else {
|
|
882
|
+
chtlist.appendItem(r2)
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (trm.e || trm.f) {
|
|
887
|
+
const tr = svgCanvas.getSvgRoot().createSVGTransform()
|
|
888
|
+
tr.setTranslate(trm.e, trm.f)
|
|
889
|
+
if (chtlist.numberOfItems) {
|
|
890
|
+
chtlist.insertItemBefore(tr, 0)
|
|
891
|
+
} else {
|
|
892
|
+
chtlist.appendItem(tr)
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
// more complicated than just a rotate
|
|
897
|
+
// transfer the group's transform down to each child and then
|
|
898
|
+
// call recalculateDimensions()
|
|
899
|
+
const oldxform = elem.getAttribute('transform')
|
|
900
|
+
changes = {}
|
|
901
|
+
changes.transform = oldxform || ''
|
|
902
|
+
|
|
903
|
+
const newxform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
904
|
+
|
|
905
|
+
// [ gm ] [ chm ] = [ chm ] [ gm' ]
|
|
906
|
+
// [ gm' ] = [ chmInv ] [ gm ] [ chm ]
|
|
907
|
+
const chm = transformListToTransform(chtlist).matrix
|
|
908
|
+
const chmInv = chm.inverse()
|
|
909
|
+
const gm = matrixMultiply(chmInv, m, chm)
|
|
910
|
+
newxform.setMatrix(gm)
|
|
911
|
+
chtlist.appendItem(newxform)
|
|
912
|
+
}
|
|
913
|
+
const cmd = recalculateDimensions(elem)
|
|
914
|
+
if (cmd) {
|
|
915
|
+
batchCmd.addSubCommand(cmd)
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// remove transform and make it undo-able
|
|
921
|
+
if (xform) {
|
|
922
|
+
changes = {}
|
|
923
|
+
changes.transform = xform
|
|
924
|
+
g.setAttribute('transform', '')
|
|
925
|
+
g.removeAttribute('transform')
|
|
926
|
+
batchCmd.addSubCommand(new ChangeElementCommand(g, changes))
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (undoable && !batchCmd.isEmpty()) {
|
|
930
|
+
return batchCmd
|
|
931
|
+
}
|
|
932
|
+
return undefined
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Converts selected/given `<use>` or child SVG element to a group.
|
|
937
|
+
* @function module:selected-elem.SvgCanvas#convertToGroup
|
|
938
|
+
* @param {Element} elem
|
|
939
|
+
* @fires module:selected-elem.SvgCanvas#event:selected
|
|
940
|
+
* @returns {void}
|
|
941
|
+
*/
|
|
942
|
+
const convertToGroup = elem => {
|
|
943
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
944
|
+
if (!elem) {
|
|
945
|
+
elem = selectedElements[0]
|
|
946
|
+
}
|
|
947
|
+
const $elem = elem
|
|
948
|
+
const batchCmd = new BatchCommand()
|
|
949
|
+
let ts
|
|
950
|
+
const dataStorage = svgCanvas.getDataStorage()
|
|
951
|
+
if (dataStorage.has($elem, 'gsvg')) {
|
|
952
|
+
// Use the gsvg as the new group
|
|
953
|
+
const svg = elem.firstChild
|
|
954
|
+
const pt = {
|
|
955
|
+
x: Number(svg.getAttribute('x')),
|
|
956
|
+
y: Number(svg.getAttribute('y'))
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// $(elem.firstChild.firstChild).unwrap();
|
|
960
|
+
const firstChild = elem.firstChild.firstChild
|
|
961
|
+
if (firstChild) {
|
|
962
|
+
firstChild.outerHTML = firstChild.innerHTML
|
|
963
|
+
}
|
|
964
|
+
dataStorage.remove(elem, 'gsvg')
|
|
965
|
+
|
|
966
|
+
const tlist = elem.transform.baseVal
|
|
967
|
+
const xform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
968
|
+
xform.setTranslate(pt.x, pt.y)
|
|
969
|
+
tlist.appendItem(xform)
|
|
970
|
+
recalculateDimensions(elem)
|
|
971
|
+
svgCanvas.call('selected', [elem])
|
|
972
|
+
} else if (dataStorage.has($elem, 'symbol')) {
|
|
973
|
+
elem = dataStorage.get($elem, 'symbol')
|
|
974
|
+
|
|
975
|
+
ts = $elem.getAttribute('transform')
|
|
976
|
+
const pos = {
|
|
977
|
+
x: Number($elem.getAttribute('x')),
|
|
978
|
+
y: Number($elem.getAttribute('y'))
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const vb = elem.getAttribute('viewBox')
|
|
982
|
+
|
|
983
|
+
if (vb) {
|
|
984
|
+
const nums = vb.split(' ')
|
|
985
|
+
pos.x -= Number(nums[0])
|
|
986
|
+
pos.y -= Number(nums[1])
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Not ideal, but works
|
|
990
|
+
ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
|
|
991
|
+
|
|
992
|
+
const prev = $elem.previousElementSibling
|
|
993
|
+
|
|
994
|
+
// Remove <use> element
|
|
995
|
+
batchCmd.addSubCommand(
|
|
996
|
+
new RemoveElementCommand(
|
|
997
|
+
$elem,
|
|
998
|
+
$elem.nextElementSibling,
|
|
999
|
+
$elem.parentNode
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
1002
|
+
$elem.remove()
|
|
1003
|
+
|
|
1004
|
+
// See if other elements reference this symbol
|
|
1005
|
+
const svgContent = svgCanvas.getSvgContent()
|
|
1006
|
+
// const hasMore = svgContent.querySelectorAll('use:data(symbol)').length;
|
|
1007
|
+
// @todo review this logic
|
|
1008
|
+
const hasMore = svgContent.querySelectorAll('use').length
|
|
1009
|
+
|
|
1010
|
+
const g = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'g')
|
|
1011
|
+
const childs = elem.childNodes
|
|
1012
|
+
|
|
1013
|
+
let i
|
|
1014
|
+
for (i = 0; i < childs.length; i++) {
|
|
1015
|
+
g.append(childs[i].cloneNode(true))
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Duplicate the gradients for Gecko, since they weren't included in the <symbol>
|
|
1019
|
+
if (isGecko()) {
|
|
1020
|
+
const svgElement = findDefs()
|
|
1021
|
+
const gradients = svgElement.querySelectorAll(
|
|
1022
|
+
'linearGradient,radialGradient,pattern'
|
|
1023
|
+
)
|
|
1024
|
+
for (let i = 0, im = gradients.length; im > i; i++) {
|
|
1025
|
+
g.appendChild(gradients[i].cloneNode(true))
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (ts) {
|
|
1030
|
+
g.setAttribute('transform', ts)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const parent = elem.parentNode
|
|
1034
|
+
|
|
1035
|
+
svgCanvas.uniquifyElems(g)
|
|
1036
|
+
|
|
1037
|
+
// Put the dupe gradients back into <defs> (after uniquifying them)
|
|
1038
|
+
if (isGecko()) {
|
|
1039
|
+
const svgElement = findDefs()
|
|
1040
|
+
const elements = g.querySelectorAll(
|
|
1041
|
+
'linearGradient,radialGradient,pattern'
|
|
1042
|
+
)
|
|
1043
|
+
for (let i = 0, im = elements.length; im > i; i++) {
|
|
1044
|
+
svgElement.appendChild(elements[i])
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// now give the g itself a new id
|
|
1049
|
+
g.id = svgCanvas.getNextId()
|
|
1050
|
+
|
|
1051
|
+
prev.after(g)
|
|
1052
|
+
|
|
1053
|
+
if (parent) {
|
|
1054
|
+
if (!hasMore) {
|
|
1055
|
+
// remove symbol/svg element
|
|
1056
|
+
const { nextSibling } = elem
|
|
1057
|
+
elem.remove()
|
|
1058
|
+
batchCmd.addSubCommand(
|
|
1059
|
+
new RemoveElementCommand(elem, nextSibling, parent)
|
|
1060
|
+
)
|
|
1061
|
+
}
|
|
1062
|
+
batchCmd.addSubCommand(new InsertElementCommand(g))
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
svgCanvas.setUseData(g)
|
|
1066
|
+
|
|
1067
|
+
if (isGecko()) {
|
|
1068
|
+
svgCanvas.convertGradients(findDefs())
|
|
1069
|
+
} else {
|
|
1070
|
+
svgCanvas.convertGradients(g)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// recalculate dimensions on the top-level children so that unnecessary transforms
|
|
1074
|
+
// are removed
|
|
1075
|
+
walkTreePost(g, n => {
|
|
1076
|
+
try {
|
|
1077
|
+
recalculateDimensions(n)
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
console.error(e)
|
|
1080
|
+
}
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
// Give ID for any visible element missing one
|
|
1084
|
+
const visElems = g.querySelectorAll(svgCanvas.getVisElems())
|
|
1085
|
+
Array.prototype.forEach.call(visElems, el => {
|
|
1086
|
+
if (!el.id) {
|
|
1087
|
+
el.id = svgCanvas.getNextId()
|
|
1088
|
+
}
|
|
1089
|
+
})
|
|
1090
|
+
|
|
1091
|
+
svgCanvas.selectOnly([g])
|
|
1092
|
+
|
|
1093
|
+
const cm = pushGroupProperty(g, true)
|
|
1094
|
+
if (cm) {
|
|
1095
|
+
batchCmd.addSubCommand(cm)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
1099
|
+
} else {
|
|
1100
|
+
console.warn('Unexpected element to ungroup:', elem)
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Unwraps all the elements in a selected group (`g`) element. This requires
|
|
1106
|
+
* significant recalculations to apply group's transforms, etc. to its children.
|
|
1107
|
+
* @function module:selected-elem.SvgCanvas#ungroupSelectedElement
|
|
1108
|
+
* @returns {void}
|
|
1109
|
+
*/
|
|
1110
|
+
const ungroupSelectedElement = () => {
|
|
1111
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
1112
|
+
const dataStorage = svgCanvas.getDataStorage()
|
|
1113
|
+
let g = selectedElements[0]
|
|
1114
|
+
if (!g) {
|
|
1115
|
+
return
|
|
1116
|
+
}
|
|
1117
|
+
if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
|
|
1118
|
+
// Is svg, so actually convert to group
|
|
1119
|
+
convertToGroup(g)
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
if (g.tagName === 'use') {
|
|
1123
|
+
// Somehow doesn't have data set, so retrieve
|
|
1124
|
+
const symbol = getElement(getHref(g).substr(1))
|
|
1125
|
+
dataStorage.put(g, 'symbol', symbol)
|
|
1126
|
+
dataStorage.put(g, 'ref', symbol)
|
|
1127
|
+
convertToGroup(g)
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
const parentsA = getParents(g.parentNode, 'a')
|
|
1131
|
+
if (parentsA?.length) {
|
|
1132
|
+
g = parentsA[0]
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Look for parent "a"
|
|
1136
|
+
if (g.tagName === 'g' || g.tagName === 'a') {
|
|
1137
|
+
const batchCmd = new BatchCommand('Ungroup Elements')
|
|
1138
|
+
const cmd = pushGroupProperty(g, true)
|
|
1139
|
+
if (cmd) {
|
|
1140
|
+
batchCmd.addSubCommand(cmd)
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const parent = g.parentNode
|
|
1144
|
+
const anchor = g.nextSibling
|
|
1145
|
+
const children = new Array(g.childNodes.length)
|
|
1146
|
+
|
|
1147
|
+
let i = 0
|
|
1148
|
+
while (g.firstChild) {
|
|
1149
|
+
const elem = g.firstChild
|
|
1150
|
+
const oldNextSibling = elem.nextSibling
|
|
1151
|
+
const oldParent = elem.parentNode
|
|
1152
|
+
|
|
1153
|
+
// Remove child title elements
|
|
1154
|
+
if (elem.tagName === 'title') {
|
|
1155
|
+
const { nextSibling } = elem
|
|
1156
|
+
batchCmd.addSubCommand(
|
|
1157
|
+
new RemoveElementCommand(elem, nextSibling, oldParent)
|
|
1158
|
+
)
|
|
1159
|
+
elem.remove()
|
|
1160
|
+
continue
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
children[i++] = parent.insertBefore(elem, anchor)
|
|
1164
|
+
batchCmd.addSubCommand(
|
|
1165
|
+
new MoveElementCommand(elem, oldNextSibling, oldParent)
|
|
1166
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// remove the group from the selection
|
|
1170
|
+
svgCanvas.clearSelection()
|
|
1171
|
+
|
|
1172
|
+
// delete the group element (but make undo-able)
|
|
1173
|
+
const gNextSibling = g.nextSibling
|
|
1174
|
+
g.remove()
|
|
1175
|
+
batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent))
|
|
1176
|
+
|
|
1177
|
+
if (!batchCmd.isEmpty()) {
|
|
1178
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// update selection
|
|
1182
|
+
svgCanvas.addToSelection(children)
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Updates the editor canvas width/height/position after a zoom has occurred.
|
|
1187
|
+
* @function module:svgcanvas.SvgCanvas#updateCanvas
|
|
1188
|
+
* @param {Float} w - Float with the new width
|
|
1189
|
+
* @param {Float} h - Float with the new height
|
|
1190
|
+
* @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
|
|
1191
|
+
* @returns {module:svgcanvas.CanvasInfo}
|
|
1192
|
+
*/
|
|
1193
|
+
const updateCanvas = (w, h) => {
|
|
1194
|
+
svgCanvas.getSvgRoot().setAttribute('width', w)
|
|
1195
|
+
svgCanvas.getSvgRoot().setAttribute('height', h)
|
|
1196
|
+
const zoom = svgCanvas.getZoom()
|
|
1197
|
+
const bg = document.getElementById('canvasBackground')
|
|
1198
|
+
const oldX = Number(svgCanvas.getSvgContent().getAttribute('x'))
|
|
1199
|
+
const oldY = Number(svgCanvas.getSvgContent().getAttribute('y'))
|
|
1200
|
+
const x = (w - svgCanvas.contentW * zoom) / 2
|
|
1201
|
+
const y = (h - svgCanvas.contentH * zoom) / 2
|
|
1202
|
+
|
|
1203
|
+
assignAttributes(svgCanvas.getSvgContent(), {
|
|
1204
|
+
width: svgCanvas.contentW * zoom,
|
|
1205
|
+
height: svgCanvas.contentH * zoom,
|
|
1206
|
+
x,
|
|
1207
|
+
y,
|
|
1208
|
+
viewBox: '0 0 ' + svgCanvas.contentW + ' ' + svgCanvas.contentH
|
|
1209
|
+
})
|
|
1210
|
+
|
|
1211
|
+
assignAttributes(bg, {
|
|
1212
|
+
width: svgCanvas.getSvgContent().getAttribute('width'),
|
|
1213
|
+
height: svgCanvas.getSvgContent().getAttribute('height'),
|
|
1214
|
+
x,
|
|
1215
|
+
y
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
const bgImg = getElement('background_image')
|
|
1219
|
+
if (bgImg) {
|
|
1220
|
+
assignAttributes(bgImg, {
|
|
1221
|
+
width: '100%',
|
|
1222
|
+
height: '100%'
|
|
1223
|
+
})
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
svgCanvas.selectorManager.selectorParentGroup.setAttribute(
|
|
1227
|
+
'transform',
|
|
1228
|
+
'translate(' + x + ',' + y + ')'
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Invoked upon updates to the canvas.
|
|
1233
|
+
* @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
|
|
1234
|
+
* @type {PlainObject}
|
|
1235
|
+
* @property {Integer} new_x
|
|
1236
|
+
* @property {Integer} new_y
|
|
1237
|
+
* @property {string} old_x (Of Integer)
|
|
1238
|
+
* @property {string} old_y (Of Integer)
|
|
1239
|
+
* @property {Integer} d_x
|
|
1240
|
+
* @property {Integer} d_y
|
|
1241
|
+
*/
|
|
1242
|
+
svgCanvas.runExtensions(
|
|
1243
|
+
'canvasUpdated',
|
|
1244
|
+
/**
|
|
1245
|
+
* @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
|
|
1246
|
+
*/
|
|
1247
|
+
{
|
|
1248
|
+
new_x: x,
|
|
1249
|
+
new_y: y,
|
|
1250
|
+
old_x: oldX,
|
|
1251
|
+
old_y: oldY,
|
|
1252
|
+
d_x: x - oldX,
|
|
1253
|
+
d_y: y - oldY
|
|
1254
|
+
}
|
|
1255
|
+
)
|
|
1256
|
+
return { x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY }
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Select the next/previous element within the current layer.
|
|
1260
|
+
* @function module:svgcanvas.SvgCanvas#cycleElement
|
|
1261
|
+
* @param {boolean} next - true = next and false = previous element
|
|
1262
|
+
* @fires module:svgcanvas.SvgCanvas#event:selected
|
|
1263
|
+
* @returns {void}
|
|
1264
|
+
*/
|
|
1265
|
+
const cycleElement = next => {
|
|
1266
|
+
const selectedElements = svgCanvas.getSelectedElements()
|
|
1267
|
+
const currentGroup = svgCanvas.getCurrentGroup()
|
|
1268
|
+
let num
|
|
1269
|
+
const curElem = selectedElements[0]
|
|
1270
|
+
let elem = false
|
|
1271
|
+
const allElems = getVisibleElements(
|
|
1272
|
+
currentGroup || svgCanvas.getCurrentDrawing().getCurrentLayer()
|
|
1273
|
+
)
|
|
1274
|
+
if (!allElems.length) {
|
|
1275
|
+
return
|
|
1276
|
+
}
|
|
1277
|
+
if (!curElem) {
|
|
1278
|
+
num = next ? allElems.length - 1 : 0
|
|
1279
|
+
elem = allElems[num]
|
|
1280
|
+
} else {
|
|
1281
|
+
let i = allElems.length
|
|
1282
|
+
while (i--) {
|
|
1283
|
+
if (allElems[i] === curElem) {
|
|
1284
|
+
num = next ? i - 1 : i + 1
|
|
1285
|
+
if (num >= allElems.length) {
|
|
1286
|
+
num = 0
|
|
1287
|
+
} else if (num < 0) {
|
|
1288
|
+
num = allElems.length - 1
|
|
1289
|
+
}
|
|
1290
|
+
elem = allElems[num]
|
|
1291
|
+
break
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
svgCanvas.selectOnly([elem], true)
|
|
1296
|
+
svgCanvas.call('selected', selectedElements)
|
|
1297
|
+
}
|