1298 lines
38 KiB
JavaScript
1298 lines
38 KiB
JavaScript
/**
|
|
* Tools for SVG selected element operation.
|
|
* @module selected-elem
|
|
* @license MIT
|
|
*
|
|
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
|
|
*/
|
|
|
|
import { NS } from './namespaces.js'
|
|
import * as hstry from './history.js'
|
|
import * as pathModule from './path.js'
|
|
import {
|
|
getStrokedBBoxDefaultVisible,
|
|
setHref,
|
|
getElement,
|
|
getHref,
|
|
getVisibleElements,
|
|
findDefs,
|
|
getRotationAngle,
|
|
getRefElem,
|
|
getBBox as utilsGetBBox,
|
|
walkTreePost,
|
|
assignAttributes,
|
|
getFeGaussianBlur
|
|
} from './utilities.js'
|
|
import {
|
|
transformPoint,
|
|
matrixMultiply,
|
|
transformListToTransform
|
|
} from './math.js'
|
|
import { recalculateDimensions } from './recalculate.js'
|
|
import { isGecko } from '../../src/common/browser.js'
|
|
import { getParents } from '../../src/common/util.js'
|
|
|
|
const {
|
|
MoveElementCommand,
|
|
BatchCommand,
|
|
InsertElementCommand,
|
|
RemoveElementCommand,
|
|
ChangeElementCommand
|
|
} = hstry
|
|
|
|
let svgCanvas = null
|
|
|
|
/**
|
|
* @function module:selected-elem.init
|
|
* @param {module:selected-elem.elementContext} elementContext
|
|
* @returns {void}
|
|
*/
|
|
export const init = canvas => {
|
|
svgCanvas = canvas
|
|
svgCanvas.copySelectedElements = copySelectedElements
|
|
svgCanvas.groupSelectedElements = groupSelectedElements // Wraps all the selected elements in a group (`g`) element.
|
|
svgCanvas.pushGroupProperties = pushGroupProperty // Pushes all appropriate parent group properties down to its children
|
|
svgCanvas.ungroupSelectedElement = ungroupSelectedElement // Unwraps all the elements in a selected group (`g`) element
|
|
svgCanvas.moveToTopSelectedElement = moveToTopSelectedElem // Repositions the selected element to the bottom in the DOM to appear on top
|
|
svgCanvas.moveToBottomSelectedElement = moveToBottomSelectedElem // Repositions the selected element to the top in the DOM to appear under other elements
|
|
svgCanvas.moveUpDownSelected = moveUpDownSelected // Moves the select element up or down the stack, based on the visibly
|
|
svgCanvas.moveSelectedElements = moveSelectedElements // Moves selected elements on the X/Y axis.
|
|
svgCanvas.cloneSelectedElements = cloneSelectedElements // Create deep DOM copies (clones) of all selected elements and move them slightly
|
|
svgCanvas.alignSelectedElements = alignSelectedElements // Aligns selected elements.
|
|
svgCanvas.updateCanvas = updateCanvas // Updates the editor canvas width/height/position after a zoom has occurred.
|
|
svgCanvas.cycleElement = cycleElement // Select the next/previous element within the current layer.
|
|
svgCanvas.deleteSelectedElements = deleteSelectedElements // Removes all selected elements from the DOM and adds the change to the history
|
|
}
|
|
|
|
/**
|
|
* Repositions the selected element to the bottom in the DOM to appear on top of
|
|
* other elements.
|
|
* @function module:selected-elem.SvgCanvas#moveToTopSelectedElem
|
|
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
* @returns {void}
|
|
*/
|
|
const moveToTopSelectedElem = () => {
|
|
const [selected] = svgCanvas.getSelectedElements()
|
|
if (selected) {
|
|
const t = selected
|
|
const oldParent = t.parentNode
|
|
const oldNextSibling = t.nextSibling
|
|
t.parentNode.append(t)
|
|
// If the element actually moved position, add the command and fire the changed
|
|
// event handler.
|
|
if (oldNextSibling !== t.nextSibling) {
|
|
svgCanvas.addCommandToHistory(
|
|
new MoveElementCommand(t, oldNextSibling, oldParent, 'top')
|
|
)
|
|
svgCanvas.call('changed', [t])
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Repositions the selected element to the top in the DOM to appear under
|
|
* other elements.
|
|
* @function module:selected-elem.SvgCanvas#moveToBottomSelectedElement
|
|
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
* @returns {void}
|
|
*/
|
|
const moveToBottomSelectedElem = () => {
|
|
const [selected] = svgCanvas.getSelectedElements()
|
|
if (selected) {
|
|
let t = selected
|
|
const oldParent = t.parentNode
|
|
const oldNextSibling = t.nextSibling
|
|
let { firstChild } = t.parentNode
|
|
if (firstChild.tagName === 'title') {
|
|
firstChild = firstChild.nextSibling
|
|
}
|
|
// This can probably be removed, as the defs should not ever apppear
|
|
// inside a layer group
|
|
if (firstChild.tagName === 'defs') {
|
|
firstChild = firstChild.nextSibling
|
|
}
|
|
t = t.parentNode.insertBefore(t, firstChild)
|
|
// If the element actually moved position, add the command and fire the changed
|
|
// event handler.
|
|
if (oldNextSibling !== t.nextSibling) {
|
|
svgCanvas.addCommandToHistory(
|
|
new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom')
|
|
)
|
|
svgCanvas.call('changed', [t])
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves the select element up or down the stack, based on the visibly
|
|
* intersecting elements.
|
|
* @function module:selected-elem.SvgCanvas#moveUpDownSelected
|
|
* @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
|
|
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
* @returns {void}
|
|
*/
|
|
const moveUpDownSelected = dir => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const selected = selectedElements[0]
|
|
if (!selected) {
|
|
return
|
|
}
|
|
|
|
svgCanvas.setCurBBoxes([])
|
|
let closest
|
|
let foundCur
|
|
// jQuery sorts this list
|
|
const list = svgCanvas.getIntersectionList(
|
|
getStrokedBBoxDefaultVisible([selected])
|
|
)
|
|
if (dir === 'Down') {
|
|
list.reverse()
|
|
}
|
|
|
|
Array.prototype.forEach.call(list, el => {
|
|
if (!foundCur) {
|
|
if (el === selected) {
|
|
foundCur = true
|
|
}
|
|
return true
|
|
}
|
|
if (closest === undefined) {
|
|
closest = el
|
|
}
|
|
return false
|
|
})
|
|
if (!closest) {
|
|
return
|
|
}
|
|
|
|
const t = selected
|
|
const oldParent = t.parentNode
|
|
const oldNextSibling = t.nextSibling
|
|
if (dir === 'Down') {
|
|
closest.insertAdjacentElement('beforebegin', t)
|
|
} else {
|
|
closest.insertAdjacentElement('afterend', t)
|
|
}
|
|
// If the element actually moved position, add the command and fire the changed
|
|
// event handler.
|
|
if (oldNextSibling !== t.nextSibling) {
|
|
svgCanvas.addCommandToHistory(
|
|
new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir)
|
|
)
|
|
svgCanvas.call('changed', [t])
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves selected elements on the X/Y axis.
|
|
* @function module:selected-elem.SvgCanvas#moveSelectedElements
|
|
* @param {Float} dx - Float with the distance to move on the x-axis
|
|
* @param {Float} dy - Float with the distance to move on the y-axis
|
|
* @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
|
|
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
* @returns {BatchCommand|void} Batch command for the move
|
|
*/
|
|
|
|
const moveSelectedElements = (dx, dy, undoable = true) => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const zoom = svgCanvas.getZoom()
|
|
// if undoable is not sent, default to true
|
|
// if single values, scale them to the zoom
|
|
if (!Array.isArray(dx)) {
|
|
dx /= zoom
|
|
dy /= zoom
|
|
}
|
|
|
|
const batchCmd = new BatchCommand('position')
|
|
selectedElements.forEach((selected, i) => {
|
|
if (selected) {
|
|
const xform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
const tlist = selected.transform?.baseVal
|
|
|
|
// dx and dy could be arrays
|
|
if (Array.isArray(dx)) {
|
|
xform.setTranslate(dx[i], dy[i])
|
|
} else {
|
|
xform.setTranslate(dx, dy)
|
|
}
|
|
|
|
if (tlist.numberOfItems) {
|
|
tlist.insertItemBefore(xform, 0)
|
|
} else {
|
|
tlist.appendItem(xform)
|
|
}
|
|
|
|
const cmd = recalculateDimensions(selected)
|
|
if (cmd) {
|
|
batchCmd.addSubCommand(cmd)
|
|
}
|
|
|
|
svgCanvas
|
|
.gettingSelectorManager()
|
|
.requestSelector(selected)
|
|
.resize()
|
|
}
|
|
})
|
|
if (!batchCmd.isEmpty()) {
|
|
if (undoable) {
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
}
|
|
svgCanvas.call('changed', selectedElements)
|
|
return batchCmd
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Create deep DOM copies (clones) of all selected elements and move them slightly
|
|
* from their originals.
|
|
* @function module:selected-elem.SvgCanvas#cloneSelectedElements
|
|
* @param {Float} x Float with the distance to move on the x-axis
|
|
* @param {Float} y Float with the distance to move on the y-axis
|
|
* @returns {void}
|
|
*/
|
|
const cloneSelectedElements = (x, y) => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const currentGroup = svgCanvas.getCurrentGroup()
|
|
let i
|
|
let elem
|
|
const batchCmd = new BatchCommand('Clone Elements')
|
|
// find all the elements selected (stop at first null)
|
|
const len = selectedElements.length
|
|
|
|
const index = el => {
|
|
if (!el) return -1
|
|
let i = 0
|
|
do {
|
|
i++
|
|
} while (el === el.previousElementSibling)
|
|
return i
|
|
}
|
|
|
|
/**
|
|
* Sorts an array numerically and ascending.
|
|
* @param {Element} a
|
|
* @param {Element} b
|
|
* @returns {Integer}
|
|
*/
|
|
const sortfunction = (a, b) => {
|
|
return index(b) - index(a)
|
|
}
|
|
selectedElements.sort(sortfunction)
|
|
for (i = 0; i < len; ++i) {
|
|
elem = selectedElements[i]
|
|
if (!elem) {
|
|
break
|
|
}
|
|
}
|
|
// use slice to quickly get the subset of elements we need
|
|
const copiedElements = selectedElements.slice(0, i)
|
|
svgCanvas.clearSelection(true)
|
|
// note that we loop in the reverse way because of the way elements are added
|
|
// to the selectedElements array (top-first)
|
|
const drawing = svgCanvas.getDrawing()
|
|
i = copiedElements.length
|
|
while (i--) {
|
|
// clone each element and replace it within copiedElements
|
|
elem = copiedElements[i] = drawing.copyElem(copiedElements[i])
|
|
;(currentGroup || drawing.getCurrentLayer()).append(elem)
|
|
batchCmd.addSubCommand(new InsertElementCommand(elem))
|
|
}
|
|
|
|
if (!batchCmd.isEmpty()) {
|
|
svgCanvas.addToSelection(copiedElements.reverse()) // Need to reverse for correct selection-adding
|
|
moveSelectedElements(x, y, false)
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
}
|
|
}
|
|
/**
|
|
* Aligns selected elements.
|
|
* @function module:selected-elem.SvgCanvas#alignSelectedElements
|
|
* @param {string} type - String with single character indicating the alignment type
|
|
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
|
|
* @returns {void}
|
|
*/
|
|
const alignSelectedElements = (type, relativeTo) => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const bboxes = [] // angles = [];
|
|
const len = selectedElements.length
|
|
if (!len) {
|
|
return
|
|
}
|
|
let minx = Number.MAX_VALUE
|
|
let maxx = Number.MIN_VALUE
|
|
let miny = Number.MAX_VALUE
|
|
let maxy = Number.MIN_VALUE
|
|
|
|
const isHorizontalAlign = (type) => ['l', 'c', 'r', 'left', 'center', 'right'].includes(type)
|
|
const isVerticalAlign = (type) => ['t', 'm', 'b', 'top', 'middle', 'bottom'].includes(type)
|
|
|
|
for (let i = 0; i < len; ++i) {
|
|
if (!selectedElements[i]) {
|
|
break
|
|
}
|
|
const elem = selectedElements[i]
|
|
bboxes[i] = getStrokedBBoxDefaultVisible([elem])
|
|
}
|
|
|
|
// distribute horizontal and vertical align is not support smallest and largest
|
|
if (['smallest', 'largest'].includes(relativeTo) && ['dh', 'distrib_horiz', 'dv', 'distrib_verti'].includes(type)) {
|
|
relativeTo = 'selected'
|
|
}
|
|
|
|
switch (relativeTo) {
|
|
case 'smallest':
|
|
if (isHorizontalAlign(type) || isVerticalAlign(type)) {
|
|
const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
|
|
const minBbox = sortedBboxes[0]
|
|
minx = minBbox.x
|
|
miny = minBbox.y
|
|
maxx = minBbox.x + minBbox.width
|
|
maxy = minBbox.y + minBbox.height
|
|
}
|
|
break
|
|
case 'largest':
|
|
if (isHorizontalAlign(type) || isVerticalAlign(type)) {
|
|
const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
|
|
const maxBbox = sortedBboxes[bboxes.length - 1]
|
|
minx = maxBbox.x
|
|
miny = maxBbox.y
|
|
maxx = maxBbox.x + maxBbox.width
|
|
maxy = maxBbox.y + maxBbox.height
|
|
}
|
|
break
|
|
case 'page':
|
|
minx = 0
|
|
miny = 0
|
|
maxx = svgCanvas.getContentW()
|
|
maxy = svgCanvas.getContentH()
|
|
break
|
|
default:
|
|
// 'selected'
|
|
minx = Math.min(...bboxes.map(box => box.x))
|
|
miny = Math.min(...bboxes.map(box => box.y))
|
|
maxx = Math.max(...bboxes.map(box => box.x + box.width))
|
|
maxy = Math.max(...bboxes.map(box => box.y + box.height))
|
|
break
|
|
} // adjust min/max
|
|
|
|
let dx = []
|
|
let dy = []
|
|
|
|
if (['dh', 'distrib_horiz'].includes(type)) { // distribute horizontal align
|
|
[dx, dy] = _getDistributeHorizontalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
} else if (['dv', 'distrib_verti'].includes(type)) { // distribute vertical align
|
|
[dx, dy] = _getDistributeVerticalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
} else { // normal align (top, left, right, ...)
|
|
[dx, dy] = _getNormalDistances(type, selectedElements, bboxes, minx, maxx, miny, maxy)
|
|
}
|
|
|
|
moveSelectedElements(dx, dy)
|
|
}
|
|
|
|
/**
|
|
* Aligns selected elements.
|
|
* @function module:selected-elem.SvgCanvas#alignSelectedElements
|
|
* @param {string} type - String with single character indicating the alignment type
|
|
* @param {"selected"|"largest"|"smallest"|"page"} relativeTo
|
|
* @returns {void}
|
|
*/
|
|
|
|
/**
|
|
* get distribution horizontal distances.
|
|
* (internal call only)
|
|
*
|
|
* @param {string} relativeTo
|
|
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
* @param {Float} minx - selected area min-x
|
|
* @param {Float} maxx - selected area max-x
|
|
* @param {Float} miny - selected area min-y
|
|
* @param {Float} maxy - selected area max-y
|
|
* @returns {Array.Float[]} x and y distances array
|
|
* @private
|
|
*/
|
|
const _getDistributeHorizontalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
const dx = []
|
|
const dy = []
|
|
|
|
for (let i = 0; i < selectedElements.length; i++) {
|
|
dy[i] = 0
|
|
}
|
|
|
|
const bboxesSortedClone = bboxes
|
|
.slice()
|
|
.sort((firstBox, secondBox) => {
|
|
const firstMaxX = firstBox.x + firstBox.width
|
|
const secondMaxX = secondBox.x + secondBox.width
|
|
|
|
if (firstMaxX === secondMaxX) { return 0 } else if (firstMaxX > secondMaxX) { return 1 } else { return -1 }
|
|
})
|
|
|
|
if (relativeTo === 'page') {
|
|
bboxesSortedClone.unshift({ x: 0, y: 0, width: 0, height: maxy }) // virtual left box
|
|
bboxesSortedClone.push({ x: maxx, y: 0, width: 0, height: maxy }) // virtual right box
|
|
}
|
|
|
|
const totalWidth = maxx - minx
|
|
const totalBoxWidth = bboxesSortedClone.map(b => b.width).reduce((w1, w2) => w1 + w2, 0)
|
|
const space = (totalWidth - totalBoxWidth) / (bboxesSortedClone.length - 1)
|
|
const _dx = []
|
|
|
|
for (let i = 0; i < bboxesSortedClone.length; ++i) {
|
|
_dx[i] = 0
|
|
|
|
if (i === 0) { continue }
|
|
|
|
const orgX = bboxesSortedClone[i].x
|
|
bboxesSortedClone[i].x = bboxesSortedClone[i - 1].x + bboxesSortedClone[i - 1].width + space
|
|
_dx[i] = bboxesSortedClone[i].x - orgX
|
|
}
|
|
|
|
bboxesSortedClone.forEach((boxClone, idx) => {
|
|
const orgIdx = bboxes.findIndex(box => box === boxClone)
|
|
if (orgIdx !== -1) {
|
|
dx[orgIdx] = _dx[idx]
|
|
}
|
|
})
|
|
|
|
return [dx, dy]
|
|
}
|
|
|
|
/**
|
|
* get distribution vertical distances.
|
|
* (internal call only)
|
|
*
|
|
* @param {string} relativeTo
|
|
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
* @param {Float} minx - selected area min-x
|
|
* @param {Float} maxx - selected area max-x
|
|
* @param {Float} miny - selected area min-y
|
|
* @param {Float} maxy - selected area max-y
|
|
* @returns {Array.Float[]}} x and y distances array
|
|
* @private
|
|
*/
|
|
const _getDistributeVerticalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
const dx = []
|
|
const dy = []
|
|
|
|
for (let i = 0; i < selectedElements.length; i++) {
|
|
dx[i] = 0
|
|
}
|
|
|
|
const bboxesSortedClone = bboxes
|
|
.slice()
|
|
.sort((firstBox, secondBox) => {
|
|
const firstMaxY = firstBox.y + firstBox.height
|
|
const secondMaxY = secondBox.y + secondBox.height
|
|
|
|
if (firstMaxY === secondMaxY) { return 0 } else if (firstMaxY > secondMaxY) { return 1 } else { return -1 }
|
|
})
|
|
|
|
if (relativeTo === 'page') {
|
|
bboxesSortedClone.unshift({ x: 0, y: 0, width: maxx, height: 0 }) // virtual top box
|
|
bboxesSortedClone.push({ x: 0, y: maxy, width: maxx, height: 0 }) // virtual bottom box
|
|
}
|
|
|
|
const totalHeight = maxy - miny
|
|
const totalBoxHeight = bboxesSortedClone.map(b => b.height).reduce((h1, h2) => h1 + h2, 0)
|
|
const space = (totalHeight - totalBoxHeight) / (bboxesSortedClone.length - 1)
|
|
const _dy = []
|
|
|
|
for (let i = 0; i < bboxesSortedClone.length; ++i) {
|
|
_dy[i] = 0
|
|
|
|
if (i === 0) { continue }
|
|
|
|
const orgY = bboxesSortedClone[i].y
|
|
bboxesSortedClone[i].y = bboxesSortedClone[i - 1].y + bboxesSortedClone[i - 1].height + space
|
|
_dy[i] = bboxesSortedClone[i].y - orgY
|
|
}
|
|
|
|
bboxesSortedClone.forEach((boxClone, idx) => {
|
|
const orgIdx = bboxes.findIndex(box => box === boxClone)
|
|
if (orgIdx !== -1) {
|
|
dy[orgIdx] = _dy[idx]
|
|
}
|
|
})
|
|
|
|
return [dx, dy]
|
|
}
|
|
|
|
/**
|
|
* get normal align distances.
|
|
* (internal call only)
|
|
*
|
|
* @param {string} type
|
|
* @param {Element[]} selectedElements - the array with selected DOM elements
|
|
* @param {module:utilities.BBoxObject} bboxes - bounding box objects
|
|
* @param {Float} minx - selected area min-x
|
|
* @param {Float} maxx - selected area max-x
|
|
* @param {Float} miny - selected area min-y
|
|
* @param {Float} maxy - selected area max-y
|
|
* @returns {Array.Float[]} x and y distances array
|
|
* @private
|
|
*/
|
|
const _getNormalDistances = (type, selectedElements, bboxes, minx, maxx, miny, maxy) => {
|
|
const len = selectedElements.length
|
|
const dx = new Array(len)
|
|
const dy = new Array(len)
|
|
|
|
for (let i = 0; i < len; ++i) {
|
|
if (!selectedElements[i]) {
|
|
break
|
|
}
|
|
// const elem = selectedElements[i];
|
|
const bbox = bboxes[i]
|
|
dx[i] = 0
|
|
dy[i] = 0
|
|
|
|
switch (type) {
|
|
case 'l': // left (horizontal)
|
|
case 'left': // left (horizontal)
|
|
dx[i] = minx - bbox.x
|
|
break
|
|
case 'c': // center (horizontal)
|
|
case 'center': // center (horizontal)
|
|
dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2)
|
|
break
|
|
case 'r': // right (horizontal)
|
|
case 'right': // right (horizontal)
|
|
dx[i] = maxx - (bbox.x + bbox.width)
|
|
break
|
|
case 't': // top (vertical)
|
|
case 'top': // top (vertical)
|
|
dy[i] = miny - bbox.y
|
|
break
|
|
case 'm': // middle (vertical)
|
|
case 'middle': // middle (vertical)
|
|
dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2)
|
|
break
|
|
case 'b': // bottom (vertical)
|
|
case 'bottom': // bottom (vertical)
|
|
dy[i] = maxy - (bbox.y + bbox.height)
|
|
break
|
|
}
|
|
}
|
|
|
|
return [dx, dy]
|
|
}
|
|
|
|
/**
|
|
* Removes all selected elements from the DOM and adds the change to the
|
|
* history stack.
|
|
* @function module:selected-elem.SvgCanvas#deleteSelectedElements
|
|
* @fires module:selected-elem.SvgCanvas#event:changed
|
|
* @returns {void}
|
|
*/
|
|
const deleteSelectedElements = () => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const batchCmd = new BatchCommand('Delete Elements')
|
|
const selectedCopy = [] // selectedElements is being deleted
|
|
|
|
selectedElements.forEach(selected => {
|
|
if (selected) {
|
|
let parent = selected.parentNode
|
|
let t = selected
|
|
// this will unselect the element and remove the selectedOutline
|
|
svgCanvas.gettingSelectorManager().releaseSelector(t)
|
|
// Remove the path if present.
|
|
pathModule.removePath_(t.id)
|
|
// Get the parent if it's a single-child anchor
|
|
if (parent.tagName === 'a' && parent.childNodes.length === 1) {
|
|
t = parent
|
|
parent = parent.parentNode
|
|
}
|
|
const { nextSibling } = t
|
|
t.remove()
|
|
const elem = t
|
|
selectedCopy.push(selected) // for the copy
|
|
batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent))
|
|
}
|
|
})
|
|
svgCanvas.setEmptySelectedElements()
|
|
|
|
if (!batchCmd.isEmpty()) {
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
}
|
|
svgCanvas.call('changed', selectedCopy)
|
|
svgCanvas.clearSelection()
|
|
}
|
|
|
|
/**
|
|
* Remembers the current selected elements on the clipboard.
|
|
* @function module:selected-elem.SvgCanvas#copySelectedElements
|
|
* @returns {void}
|
|
*/
|
|
const copySelectedElements = () => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const data = JSON.stringify(
|
|
selectedElements.map(x => svgCanvas.getJsonFromSvgElements(x))
|
|
)
|
|
// Use sessionStorage for the clipboard data.
|
|
sessionStorage.setItem(svgCanvas.getClipboardID(), data)
|
|
svgCanvas.flashStorage()
|
|
|
|
// Context menu might not exist (it is provided by editor.js).
|
|
const canvMenu = document.getElementById('se-cmenu_canvas')
|
|
canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place')
|
|
}
|
|
|
|
/**
|
|
* Wraps all the selected elements in a group (`g`) element.
|
|
* @function module:selected-elem.SvgCanvas#groupSelectedElements
|
|
* @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
|
|
* @param {string} [urlArg]
|
|
* @returns {void}
|
|
*/
|
|
const groupSelectedElements = (type, urlArg) => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
if (!type) {
|
|
type = 'g'
|
|
}
|
|
let cmdStr = ''
|
|
let url
|
|
|
|
switch (type) {
|
|
case 'a': {
|
|
cmdStr = 'Make hyperlink'
|
|
url = urlArg || ''
|
|
break
|
|
}
|
|
default: {
|
|
type = 'g'
|
|
cmdStr = 'Group Elements'
|
|
break
|
|
}
|
|
}
|
|
|
|
const batchCmd = new BatchCommand(cmdStr)
|
|
|
|
// create and insert the group element
|
|
const g = svgCanvas.addSVGElementsFromJson({
|
|
element: type,
|
|
attr: {
|
|
id: svgCanvas.getNextId()
|
|
}
|
|
})
|
|
if (type === 'a') {
|
|
setHref(g, url)
|
|
}
|
|
batchCmd.addSubCommand(new InsertElementCommand(g))
|
|
|
|
// now move all children into the group
|
|
let i = selectedElements.length
|
|
while (i--) {
|
|
let elem = selectedElements[i]
|
|
if (!elem) {
|
|
continue
|
|
}
|
|
|
|
if (
|
|
elem.parentNode.tagName === 'a' &&
|
|
elem.parentNode.childNodes.length === 1
|
|
) {
|
|
elem = elem.parentNode
|
|
}
|
|
|
|
const oldNextSibling = elem.nextSibling
|
|
const oldParent = elem.parentNode
|
|
g.append(elem)
|
|
batchCmd.addSubCommand(
|
|
new MoveElementCommand(elem, oldNextSibling, oldParent)
|
|
)
|
|
}
|
|
if (!batchCmd.isEmpty()) {
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
}
|
|
|
|
// update selection
|
|
svgCanvas.selectOnly([g], true)
|
|
}
|
|
|
|
/**
|
|
* Pushes all appropriate parent group properties down to its children, then
|
|
* removes them from the group.
|
|
* @function module:selected-elem.SvgCanvas#pushGroupProperty
|
|
* @param {SVGAElement|SVGGElement} g
|
|
* @param {boolean} undoable
|
|
* @returns {BatchCommand|void}
|
|
*/
|
|
const pushGroupProperty = (g, undoable) => {
|
|
const children = g.childNodes
|
|
const len = children.length
|
|
const xform = g.getAttribute('transform')
|
|
|
|
const glist = g.transform.baseVal
|
|
const m = transformListToTransform(glist).matrix
|
|
|
|
const batchCmd = new BatchCommand('Push group properties')
|
|
|
|
// TODO: get all fill/stroke properties from the group that we are about to destroy
|
|
// "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
|
|
// "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
|
|
// "stroke-width"
|
|
// and then for each child, if they do not have the attribute (or the value is 'inherit')
|
|
// then set the child's attribute
|
|
|
|
const gangle = getRotationAngle(g)
|
|
|
|
const gattrs = {
|
|
filter: g.getAttribute('filter'),
|
|
opacity: g.getAttribute('opacity')
|
|
}
|
|
let gfilter
|
|
let gblur
|
|
let changes
|
|
const drawing = svgCanvas.getDrawing()
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
const elem = children[i]
|
|
|
|
if (elem.nodeType !== 1) {
|
|
continue
|
|
}
|
|
|
|
if (gattrs.opacity !== null && gattrs.opacity !== 1) {
|
|
// const c_opac = elem.getAttribute('opacity') || 1;
|
|
const newOpac =
|
|
Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) /
|
|
100
|
|
svgCanvas.changeSelectedAttribute('opacity', newOpac, [elem])
|
|
}
|
|
|
|
if (gattrs.filter) {
|
|
let cblur = svgCanvas.getBlur(elem)
|
|
const origCblur = cblur
|
|
if (!gblur) {
|
|
gblur = svgCanvas.getBlur(g)
|
|
}
|
|
if (cblur) {
|
|
// Is this formula correct?
|
|
cblur = Number(gblur) + Number(cblur)
|
|
} else if (cblur === 0) {
|
|
cblur = gblur
|
|
}
|
|
|
|
// If child has no current filter, get group's filter or clone it.
|
|
if (!origCblur) {
|
|
// Set group's filter to use first child's ID
|
|
if (!gfilter) {
|
|
gfilter = getRefElem(gattrs.filter)
|
|
} else {
|
|
// Clone the group's filter
|
|
gfilter = drawing.copyElem(gfilter)
|
|
findDefs().append(gfilter)
|
|
|
|
// const filterElem = getRefElem(gfilter);
|
|
const blurElem = getFeGaussianBlur(gfilter)
|
|
// Change this in future for different filters
|
|
const suffix =
|
|
blurElem?.tagName === 'feGaussianBlur' ? 'blur' : 'filter'
|
|
gfilter.id = elem.id + '_' + suffix
|
|
svgCanvas.changeSelectedAttribute(
|
|
'filter',
|
|
'url(#' + gfilter.id + ')',
|
|
[elem]
|
|
)
|
|
}
|
|
} else {
|
|
gfilter = getRefElem(elem.getAttribute('filter'))
|
|
}
|
|
// const filterElem = getRefElem(gfilter);
|
|
const blurElem = getFeGaussianBlur(gfilter)
|
|
|
|
// Update blur value
|
|
if (cblur) {
|
|
svgCanvas.changeSelectedAttribute('stdDeviation', cblur, [blurElem])
|
|
svgCanvas.setBlurOffsets(gfilter, cblur)
|
|
}
|
|
}
|
|
|
|
let chtlist = elem.transform?.baseVal
|
|
|
|
// Don't process gradient transforms
|
|
if (elem.tagName.includes('Gradient')) {
|
|
chtlist = null
|
|
}
|
|
|
|
// Hopefully not a problem to add this. Necessary for elements like <desc/>
|
|
if (!chtlist) {
|
|
continue
|
|
}
|
|
|
|
// Apparently <defs> can get get a transformlist, but we don't want it to have one!
|
|
if (elem.tagName === 'defs') {
|
|
continue
|
|
}
|
|
|
|
if (glist.numberOfItems) {
|
|
// TODO: if the group's transform is just a rotate, we can always transfer the
|
|
// rotate() down to the children (collapsing consecutive rotates and factoring
|
|
// out any translates)
|
|
if (gangle && glist.numberOfItems === 1) {
|
|
// [Rg] [Rc] [Mc]
|
|
// we want [Tr] [Rc2] [Mc] where:
|
|
// - [Rc2] is at the child's current center but has the
|
|
// sum of the group and child's rotation angles
|
|
// - [Tr] is the equivalent translation that this child
|
|
// undergoes if the group wasn't there
|
|
|
|
// [Tr] = [Rg] [Rc] [Rc2_inv]
|
|
|
|
// get group's rotation matrix (Rg)
|
|
const rgm = glist.getItem(0).matrix
|
|
|
|
// get child's rotation matrix (Rc)
|
|
let rcm = svgCanvas.getSvgRoot().createSVGMatrix()
|
|
const cangle = getRotationAngle(elem)
|
|
if (cangle) {
|
|
rcm = chtlist.getItem(0).matrix
|
|
}
|
|
|
|
// get child's old center of rotation
|
|
const cbox = utilsGetBBox(elem)
|
|
const ceqm = transformListToTransform(chtlist).matrix
|
|
const coldc = transformPoint(
|
|
cbox.x + cbox.width / 2,
|
|
cbox.y + cbox.height / 2,
|
|
ceqm
|
|
)
|
|
|
|
// sum group and child's angles
|
|
const sangle = gangle + cangle
|
|
|
|
// get child's rotation at the old center (Rc2_inv)
|
|
const r2 = svgCanvas.getSvgRoot().createSVGTransform()
|
|
r2.setRotate(sangle, coldc.x, coldc.y)
|
|
|
|
// calculate equivalent translate
|
|
const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse())
|
|
|
|
// set up tlist
|
|
if (cangle) {
|
|
chtlist.removeItem(0)
|
|
}
|
|
|
|
if (sangle) {
|
|
if (chtlist.numberOfItems) {
|
|
chtlist.insertItemBefore(r2, 0)
|
|
} else {
|
|
chtlist.appendItem(r2)
|
|
}
|
|
}
|
|
|
|
if (trm.e || trm.f) {
|
|
const tr = svgCanvas.getSvgRoot().createSVGTransform()
|
|
tr.setTranslate(trm.e, trm.f)
|
|
if (chtlist.numberOfItems) {
|
|
chtlist.insertItemBefore(tr, 0)
|
|
} else {
|
|
chtlist.appendItem(tr)
|
|
}
|
|
}
|
|
} else {
|
|
// more complicated than just a rotate
|
|
// transfer the group's transform down to each child and then
|
|
// call recalculateDimensions()
|
|
const oldxform = elem.getAttribute('transform')
|
|
changes = {}
|
|
changes.transform = oldxform || ''
|
|
|
|
const newxform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
|
|
// [ gm ] [ chm ] = [ chm ] [ gm' ]
|
|
// [ gm' ] = [ chmInv ] [ gm ] [ chm ]
|
|
const chm = transformListToTransform(chtlist).matrix
|
|
const chmInv = chm.inverse()
|
|
const gm = matrixMultiply(chmInv, m, chm)
|
|
newxform.setMatrix(gm)
|
|
chtlist.appendItem(newxform)
|
|
}
|
|
const cmd = recalculateDimensions(elem)
|
|
if (cmd) {
|
|
batchCmd.addSubCommand(cmd)
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove transform and make it undo-able
|
|
if (xform) {
|
|
changes = {}
|
|
changes.transform = xform
|
|
g.setAttribute('transform', '')
|
|
g.removeAttribute('transform')
|
|
batchCmd.addSubCommand(new ChangeElementCommand(g, changes))
|
|
}
|
|
|
|
if (undoable && !batchCmd.isEmpty()) {
|
|
return batchCmd
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Converts selected/given `<use>` or child SVG element to a group.
|
|
* @function module:selected-elem.SvgCanvas#convertToGroup
|
|
* @param {Element} elem
|
|
* @fires module:selected-elem.SvgCanvas#event:selected
|
|
* @returns {void}
|
|
*/
|
|
const convertToGroup = elem => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
if (!elem) {
|
|
elem = selectedElements[0]
|
|
}
|
|
const $elem = elem
|
|
const batchCmd = new BatchCommand()
|
|
let ts
|
|
const dataStorage = svgCanvas.getDataStorage()
|
|
if (dataStorage.has($elem, 'gsvg')) {
|
|
// Use the gsvg as the new group
|
|
const svg = elem.firstChild
|
|
const pt = {
|
|
x: Number(svg.getAttribute('x')),
|
|
y: Number(svg.getAttribute('y'))
|
|
}
|
|
|
|
// $(elem.firstChild.firstChild).unwrap();
|
|
const firstChild = elem.firstChild.firstChild
|
|
if (firstChild) {
|
|
firstChild.outerHTML = firstChild.innerHTML
|
|
}
|
|
dataStorage.remove(elem, 'gsvg')
|
|
|
|
const tlist = elem.transform.baseVal
|
|
const xform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
xform.setTranslate(pt.x, pt.y)
|
|
tlist.appendItem(xform)
|
|
recalculateDimensions(elem)
|
|
svgCanvas.call('selected', [elem])
|
|
} else if (dataStorage.has($elem, 'symbol')) {
|
|
elem = dataStorage.get($elem, 'symbol')
|
|
|
|
ts = $elem.getAttribute('transform')
|
|
const pos = {
|
|
x: Number($elem.getAttribute('x')),
|
|
y: Number($elem.getAttribute('y'))
|
|
}
|
|
|
|
const vb = elem.getAttribute('viewBox')
|
|
|
|
if (vb) {
|
|
const nums = vb.split(' ')
|
|
pos.x -= Number(nums[0])
|
|
pos.y -= Number(nums[1])
|
|
}
|
|
|
|
// Not ideal, but works
|
|
ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
|
|
|
|
const prev = $elem.previousElementSibling
|
|
|
|
// Remove <use> element
|
|
batchCmd.addSubCommand(
|
|
new RemoveElementCommand(
|
|
$elem,
|
|
$elem.nextElementSibling,
|
|
$elem.parentNode
|
|
)
|
|
)
|
|
$elem.remove()
|
|
|
|
// See if other elements reference this symbol
|
|
const svgContent = svgCanvas.getSvgContent()
|
|
// const hasMore = svgContent.querySelectorAll('use:data(symbol)').length;
|
|
// @todo review this logic
|
|
const hasMore = svgContent.querySelectorAll('use').length
|
|
|
|
const g = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'g')
|
|
const childs = elem.childNodes
|
|
|
|
let i
|
|
for (i = 0; i < childs.length; i++) {
|
|
g.append(childs[i].cloneNode(true))
|
|
}
|
|
|
|
// Duplicate the gradients for Gecko, since they weren't included in the <symbol>
|
|
if (isGecko()) {
|
|
const svgElement = findDefs()
|
|
const gradients = svgElement.querySelectorAll(
|
|
'linearGradient,radialGradient,pattern'
|
|
)
|
|
for (let i = 0, im = gradients.length; im > i; i++) {
|
|
g.appendChild(gradients[i].cloneNode(true))
|
|
}
|
|
}
|
|
|
|
if (ts) {
|
|
g.setAttribute('transform', ts)
|
|
}
|
|
|
|
const parent = elem.parentNode
|
|
|
|
svgCanvas.uniquifyElems(g)
|
|
|
|
// Put the dupe gradients back into <defs> (after uniquifying them)
|
|
if (isGecko()) {
|
|
const svgElement = findDefs()
|
|
const elements = g.querySelectorAll(
|
|
'linearGradient,radialGradient,pattern'
|
|
)
|
|
for (let i = 0, im = elements.length; im > i; i++) {
|
|
svgElement.appendChild(elements[i])
|
|
}
|
|
}
|
|
|
|
// now give the g itself a new id
|
|
g.id = svgCanvas.getNextId()
|
|
|
|
prev.after(g)
|
|
|
|
if (parent) {
|
|
if (!hasMore) {
|
|
// remove symbol/svg element
|
|
const { nextSibling } = elem
|
|
elem.remove()
|
|
batchCmd.addSubCommand(
|
|
new RemoveElementCommand(elem, nextSibling, parent)
|
|
)
|
|
}
|
|
batchCmd.addSubCommand(new InsertElementCommand(g))
|
|
}
|
|
|
|
svgCanvas.setUseData(g)
|
|
|
|
if (isGecko()) {
|
|
svgCanvas.convertGradients(findDefs())
|
|
} else {
|
|
svgCanvas.convertGradients(g)
|
|
}
|
|
|
|
// recalculate dimensions on the top-level children so that unnecessary transforms
|
|
// are removed
|
|
walkTreePost(g, n => {
|
|
try {
|
|
recalculateDimensions(n)
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
})
|
|
|
|
// Give ID for any visible element missing one
|
|
const visElems = g.querySelectorAll(svgCanvas.getVisElems())
|
|
Array.prototype.forEach.call(visElems, el => {
|
|
if (!el.id) {
|
|
el.id = svgCanvas.getNextId()
|
|
}
|
|
})
|
|
|
|
svgCanvas.selectOnly([g])
|
|
|
|
const cm = pushGroupProperty(g, true)
|
|
if (cm) {
|
|
batchCmd.addSubCommand(cm)
|
|
}
|
|
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
} else {
|
|
console.warn('Unexpected element to ungroup:', elem)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unwraps all the elements in a selected group (`g`) element. This requires
|
|
* significant recalculations to apply group's transforms, etc. to its children.
|
|
* @function module:selected-elem.SvgCanvas#ungroupSelectedElement
|
|
* @returns {void}
|
|
*/
|
|
const ungroupSelectedElement = () => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const dataStorage = svgCanvas.getDataStorage()
|
|
let g = selectedElements[0]
|
|
if (!g) {
|
|
return
|
|
}
|
|
if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
|
|
// Is svg, so actually convert to group
|
|
convertToGroup(g)
|
|
return
|
|
}
|
|
if (g.tagName === 'use') {
|
|
// Somehow doesn't have data set, so retrieve
|
|
const symbol = getElement(getHref(g).substr(1))
|
|
dataStorage.put(g, 'symbol', symbol)
|
|
dataStorage.put(g, 'ref', symbol)
|
|
convertToGroup(g)
|
|
return
|
|
}
|
|
const parentsA = getParents(g.parentNode, 'a')
|
|
if (parentsA?.length) {
|
|
g = parentsA[0]
|
|
}
|
|
|
|
// Look for parent "a"
|
|
if (g.tagName === 'g' || g.tagName === 'a') {
|
|
const batchCmd = new BatchCommand('Ungroup Elements')
|
|
const cmd = pushGroupProperty(g, true)
|
|
if (cmd) {
|
|
batchCmd.addSubCommand(cmd)
|
|
}
|
|
|
|
const parent = g.parentNode
|
|
const anchor = g.nextSibling
|
|
const children = new Array(g.childNodes.length)
|
|
|
|
let i = 0
|
|
while (g.firstChild) {
|
|
const elem = g.firstChild
|
|
const oldNextSibling = elem.nextSibling
|
|
const oldParent = elem.parentNode
|
|
|
|
// Remove child title elements
|
|
if (elem.tagName === 'title') {
|
|
const { nextSibling } = elem
|
|
batchCmd.addSubCommand(
|
|
new RemoveElementCommand(elem, nextSibling, oldParent)
|
|
)
|
|
elem.remove()
|
|
continue
|
|
}
|
|
|
|
children[i++] = parent.insertBefore(elem, anchor)
|
|
batchCmd.addSubCommand(
|
|
new MoveElementCommand(elem, oldNextSibling, oldParent)
|
|
)
|
|
}
|
|
|
|
// remove the group from the selection
|
|
svgCanvas.clearSelection()
|
|
|
|
// delete the group element (but make undo-able)
|
|
const gNextSibling = g.nextSibling
|
|
g.remove()
|
|
batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent))
|
|
|
|
if (!batchCmd.isEmpty()) {
|
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
}
|
|
|
|
// update selection
|
|
svgCanvas.addToSelection(children)
|
|
}
|
|
}
|
|
/**
|
|
* Updates the editor canvas width/height/position after a zoom has occurred.
|
|
* @function module:svgcanvas.SvgCanvas#updateCanvas
|
|
* @param {Float} w - Float with the new width
|
|
* @param {Float} h - Float with the new height
|
|
* @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
|
|
* @returns {module:svgcanvas.CanvasInfo}
|
|
*/
|
|
const updateCanvas = (w, h) => {
|
|
svgCanvas.getSvgRoot().setAttribute('width', w)
|
|
svgCanvas.getSvgRoot().setAttribute('height', h)
|
|
const zoom = svgCanvas.getZoom()
|
|
const bg = document.getElementById('canvasBackground')
|
|
const oldX = Number(svgCanvas.getSvgContent().getAttribute('x'))
|
|
const oldY = Number(svgCanvas.getSvgContent().getAttribute('y'))
|
|
const x = (w - svgCanvas.contentW * zoom) / 2
|
|
const y = (h - svgCanvas.contentH * zoom) / 2
|
|
|
|
assignAttributes(svgCanvas.getSvgContent(), {
|
|
width: svgCanvas.contentW * zoom,
|
|
height: svgCanvas.contentH * zoom,
|
|
x,
|
|
y,
|
|
viewBox: '0 0 ' + svgCanvas.contentW + ' ' + svgCanvas.contentH
|
|
})
|
|
|
|
assignAttributes(bg, {
|
|
width: svgCanvas.getSvgContent().getAttribute('width'),
|
|
height: svgCanvas.getSvgContent().getAttribute('height'),
|
|
x,
|
|
y
|
|
})
|
|
|
|
const bgImg = getElement('background_image')
|
|
if (bgImg) {
|
|
assignAttributes(bgImg, {
|
|
width: '100%',
|
|
height: '100%'
|
|
})
|
|
}
|
|
|
|
svgCanvas.selectorManager.selectorParentGroup.setAttribute(
|
|
'transform',
|
|
'translate(' + x + ',' + y + ')'
|
|
)
|
|
|
|
/**
|
|
* Invoked upon updates to the canvas.
|
|
* @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
|
|
* @type {PlainObject}
|
|
* @property {Integer} new_x
|
|
* @property {Integer} new_y
|
|
* @property {string} old_x (Of Integer)
|
|
* @property {string} old_y (Of Integer)
|
|
* @property {Integer} d_x
|
|
* @property {Integer} d_y
|
|
*/
|
|
svgCanvas.runExtensions(
|
|
'canvasUpdated',
|
|
/**
|
|
* @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
|
|
*/
|
|
{
|
|
new_x: x,
|
|
new_y: y,
|
|
old_x: oldX,
|
|
old_y: oldY,
|
|
d_x: x - oldX,
|
|
d_y: y - oldY
|
|
}
|
|
)
|
|
return { x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY }
|
|
}
|
|
/**
|
|
* Select the next/previous element within the current layer.
|
|
* @function module:svgcanvas.SvgCanvas#cycleElement
|
|
* @param {boolean} next - true = next and false = previous element
|
|
* @fires module:svgcanvas.SvgCanvas#event:selected
|
|
* @returns {void}
|
|
*/
|
|
const cycleElement = next => {
|
|
const selectedElements = svgCanvas.getSelectedElements()
|
|
const currentGroup = svgCanvas.getCurrentGroup()
|
|
let num
|
|
const curElem = selectedElements[0]
|
|
let elem = false
|
|
const allElems = getVisibleElements(
|
|
currentGroup || svgCanvas.getCurrentDrawing().getCurrentLayer()
|
|
)
|
|
if (!allElems.length) {
|
|
return
|
|
}
|
|
if (!curElem) {
|
|
num = next ? allElems.length - 1 : 0
|
|
elem = allElems[num]
|
|
} else {
|
|
let i = allElems.length
|
|
while (i--) {
|
|
if (allElems[i] === curElem) {
|
|
num = next ? i - 1 : i + 1
|
|
if (num >= allElems.length) {
|
|
num = 0
|
|
} else if (num < 0) {
|
|
num = allElems.length - 1
|
|
}
|
|
elem = allElems[num]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
svgCanvas.selectOnly([elem], true)
|
|
svgCanvas.call('selected', selectedElements)
|
|
}
|