svgedit/packages/svgcanvas/event.js

1389 lines
46 KiB
JavaScript

/**
* Tools for event.
* @module event
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import {
assignAttributes, cleanupElement, getElement, getRotationAngle, snapToGrid, walkTree,
preventClickDefault, setHref, getBBox
} from './utilities.js'
import {
convertAttrs
} from './units.js'
import {
transformPoint, hasMatrixTransform, getMatrix, snapToAngle
} from './math.js'
import * as draw from './draw.js'
import * as pathModule from './path.js'
import * as hstry from './history.js'
import { findPos } from '../../src/common/util.js'
const {
InsertElementCommand
} = hstry
let svgCanvas = null
/**
* @function module:undo.init
* @param {module:undo.eventContext} eventContext
* @returns {void}
*/
export const init = (canvas) => {
svgCanvas = canvas
svgCanvas.mouseDownEvent = mouseDownEvent
svgCanvas.mouseMoveEvent = mouseMoveEvent
svgCanvas.dblClickEvent = dblClickEvent
svgCanvas.mouseUpEvent = mouseUpEvent
svgCanvas.mouseOutEvent = mouseOutEvent
svgCanvas.DOMMouseScrollEvent = DOMMouseScrollEvent
}
const getBsplinePoint = (t) => {
const spline = { x: 0, y: 0 }
const p0 = { x: svgCanvas.getControllPoint2('x'), y: svgCanvas.getControllPoint2('y') }
const p1 = { x: svgCanvas.getControllPoint1('x'), y: svgCanvas.getControllPoint1('y') }
const p2 = { x: svgCanvas.getStart('x'), y: svgCanvas.getStart('y') }
const p3 = { x: svgCanvas.getEnd('x'), y: svgCanvas.getEnd('y') }
const S = 1.0 / 6.0
const t2 = t * t
const t3 = t2 * t
const m = [
[-1, 3, -3, 1],
[3, -6, 3, 0],
[-3, 0, 3, 0],
[1, 4, 1, 0]
]
spline.x = S * (
(p0.x * m[0][0] + p1.x * m[0][1] + p2.x * m[0][2] + p3.x * m[0][3]) * t3 +
(p0.x * m[1][0] + p1.x * m[1][1] + p2.x * m[1][2] + p3.x * m[1][3]) * t2 +
(p0.x * m[2][0] + p1.x * m[2][1] + p2.x * m[2][2] + p3.x * m[2][3]) * t +
(p0.x * m[3][0] + p1.x * m[3][1] + p2.x * m[3][2] + p3.x * m[3][3])
)
spline.y = S * (
(p0.y * m[0][0] + p1.y * m[0][1] + p2.y * m[0][2] + p3.y * m[0][3]) * t3 +
(p0.y * m[1][0] + p1.y * m[1][1] + p2.y * m[1][2] + p3.y * m[1][3]) * t2 +
(p0.y * m[2][0] + p1.y * m[2][1] + p2.y * m[2][2] + p3.y * m[2][3]) * t +
(p0.y * m[3][0] + p1.y * m[3][1] + p2.y * m[3][2] + p3.y * m[3][3])
)
return {
x: spline.x,
y: spline.y
}
}
// update the dummy transform in our transform list
// to be a translate. We need to check if there was a transformation
// to avoid loosing it
const updateTransformList = (svgRoot, element, dx, dy) => {
const xform = svgRoot.createSVGTransform()
xform.setTranslate(dx, dy)
const tlist = element.transform?.baseVal
if (tlist.numberOfItems) {
const firstItem = tlist.getItem(0)
if (firstItem.type === 2) { // SVG_TRANSFORM_TRANSLATE = 2
tlist.replaceItem(xform, 0)
} else {
tlist.insertItemBefore(xform, 0)
}
} else {
tlist.appendItem(xform)
}
}
/**
*
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:transition
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseMove
* @returns {void}
*/
const mouseMoveEvent = (evt) => {
// if the mouse is move without dragging an element, just return.
if (!svgCanvas.getStarted()) { return }
if (evt.button === 1 || svgCanvas.spaceKey) { return }
svgCanvas.textActions.init()
evt.preventDefault()
const selectedElements = svgCanvas.getSelectedElements()
const zoom = svgCanvas.getZoom()
const svgRoot = svgCanvas.getSvgRoot()
const selected = selectedElements[0]
let i
let xya
let cx
let cy
let dx
let dy
let len
let angle
let box
const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
const mouseX = pt.x * zoom
const mouseY = pt.y * zoom
const shape = getElement(svgCanvas.getId())
let realX = mouseX / zoom
let x = realX
let realY = mouseY / zoom
let y = realY
if (svgCanvas.getCurConfig().gridSnapping) {
x = snapToGrid(x)
y = snapToGrid(y)
}
let tlist
switch (svgCanvas.getCurrentMode()) {
case 'select': {
// we temporarily use a translate on the element(s) being dragged
// this transform is removed upon mousing up and the element is
// relocated to the new location
if (selected) {
dx = x - svgCanvas.getStartX()
dy = y - svgCanvas.getStartY()
if (svgCanvas.getCurConfig().gridSnapping) {
dx = snapToGrid(dx)
dy = snapToGrid(dy)
}
if (dx || dy) {
selectedElements.forEach((el) => {
if (el) {
updateTransformList(svgRoot, el, dx, dy)
// update our internal bbox that we're tracking while dragging
svgCanvas.selectorManager.requestSelector(el).resize()
}
})
svgCanvas.call('transition', selectedElements)
}
}
break
}
case 'multiselect': {
realX *= zoom
realY *= zoom
assignAttributes(svgCanvas.getRubberBox(), {
x: Math.min(svgCanvas.getRStartX(), realX),
y: Math.min(svgCanvas.getRStartY(), realY),
width: Math.abs(realX - svgCanvas.getRStartX()),
height: Math.abs(realY - svgCanvas.getRStartY())
}, 100)
// for each selected:
// - if newList contains selected, do nothing
// - if newList doesn't contain selected, remove it from selected
// - for any newList that was not in selectedElements, add it to selected
const elemsToRemove = selectedElements.slice(); const elemsToAdd = []
const newList = svgCanvas.getIntersectionList()
// For every element in the intersection, add if not present in selectedElements.
len = newList.length
for (i = 0; i < len; ++i) {
const intElem = newList[i]
// Found an element that was not selected before, so we should add it.
if (!selectedElements.includes(intElem)) {
elemsToAdd.push(intElem)
}
// Found an element that was already selected, so we shouldn't remove it.
const foundInd = elemsToRemove.indexOf(intElem)
if (foundInd !== -1) {
elemsToRemove.splice(foundInd, 1)
}
}
if (elemsToRemove.length > 0) {
svgCanvas.removeFromSelection(elemsToRemove)
}
if (elemsToAdd.length > 0) {
svgCanvas.addToSelection(elemsToAdd)
}
break
}
case 'resize': {
// we track the resize bounding box and translate/scale the selected element
// while the mouse is down, when mouse goes up, we use this to recalculate
// the shape's coordinates
tlist = selected.transform.baseVal
const hasMatrix = hasMatrixTransform(tlist)
box = hasMatrix ? svgCanvas.getInitBbox() : getBBox(selected)
let left = box.x
let top = box.y
let { width, height } = box
dx = (x - svgCanvas.getStartX())
dy = (y - svgCanvas.getStartY())
if (svgCanvas.getCurConfig().gridSnapping) {
dx = snapToGrid(dx)
dy = snapToGrid(dy)
height = snapToGrid(height)
width = snapToGrid(width)
}
// if rotated, adjust the dx,dy values
angle = getRotationAngle(selected)
if (angle) {
const r = Math.sqrt(dx * dx + dy * dy)
const theta = Math.atan2(dy, dx) - angle * Math.PI / 180.0
dx = r * Math.cos(theta)
dy = r * Math.sin(theta)
}
// if not stretching in y direction, set dy to 0
// if not stretching in x direction, set dx to 0
if (!svgCanvas.getCurrentResizeMode().includes('n') && !svgCanvas.getCurrentResizeMode().includes('s')) {
dy = 0
}
if (!svgCanvas.getCurrentResizeMode().includes('e') && !svgCanvas.getCurrentResizeMode().includes('w')) {
dx = 0
}
let // ts = null,
tx = 0; let ty = 0
let sy = height ? (height + dy) / height : 1
let sx = width ? (width + dx) / width : 1
// if we are dragging on the north side, then adjust the scale factor and ty
if (svgCanvas.getCurrentResizeMode().includes('n')) {
sy = height ? (height - dy) / height : 1
ty = height
}
// if we dragging on the east side, then adjust the scale factor and tx
if (svgCanvas.getCurrentResizeMode().includes('w')) {
sx = width ? (width - dx) / width : 1
tx = width
}
// update the transform list with translate,scale,translate
const translateOrigin = svgRoot.createSVGTransform()
const scale = svgRoot.createSVGTransform()
const translateBack = svgRoot.createSVGTransform()
if (svgCanvas.getCurConfig().gridSnapping) {
left = snapToGrid(left)
tx = snapToGrid(tx)
top = snapToGrid(top)
ty = snapToGrid(ty)
}
translateOrigin.setTranslate(-(left + tx), -(top + ty))
if (evt.shiftKey) {
if (sx === 1) {
sx = sy
} else { sy = sx }
}
scale.setScale(sx, sy)
translateBack.setTranslate(left + tx, top + ty)
if (hasMatrix) {
const diff = angle ? 1 : 0
tlist.replaceItem(translateOrigin, 2 + diff)
tlist.replaceItem(scale, 1 + diff)
tlist.replaceItem(translateBack, Number(diff))
} else {
const N = tlist.numberOfItems
tlist.replaceItem(translateBack, N - 3)
tlist.replaceItem(scale, N - 2)
tlist.replaceItem(translateOrigin, N - 1)
}
svgCanvas.selectorManager.requestSelector(selected).resize()
svgCanvas.call('transition', selectedElements)
break
}
case 'zoom': {
realX *= zoom
realY *= zoom
assignAttributes(svgCanvas.getRubberBox(), {
x: Math.min(svgCanvas.getRStartX() * zoom, realX),
y: Math.min(svgCanvas.getRStartY() * zoom, realY),
width: Math.abs(realX - svgCanvas.getRStartX() * zoom),
height: Math.abs(realY - svgCanvas.getRStartY() * zoom)
}, 100)
break
}
case 'text': {
assignAttributes(shape, {
x,
y
}, 1000)
break
}
case 'line': {
if (svgCanvas.getCurConfig().gridSnapping) {
x = snapToGrid(x)
y = snapToGrid(y)
}
let x2 = x
let y2 = y
if (evt.shiftKey) {
xya = snapToAngle(svgCanvas.getStartX(), svgCanvas.getStartY(), x2, y2)
x2 = xya.x
y2 = xya.y
}
shape.setAttribute('x2', x2)
shape.setAttribute('y2', y2)
break
}
case 'foreignObject': // fall through
case 'square':
case 'rect':
case 'image': {
const square = (svgCanvas.getCurrentMode() === 'square') || evt.shiftKey
let
w = Math.abs(x - svgCanvas.getStartX())
let h = Math.abs(y - svgCanvas.getStartY())
let newX; let newY
if (square) {
w = h = Math.max(w, h)
newX = svgCanvas.getStartX() < x ? svgCanvas.getStartX() : svgCanvas.getStartX() - w
newY = svgCanvas.getStartY() < y ? svgCanvas.getStartY() : svgCanvas.getStartY() - h
} else {
newX = Math.min(svgCanvas.getStartX(), x)
newY = Math.min(svgCanvas.getStartY(), y)
}
if (svgCanvas.getCurConfig().gridSnapping) {
w = snapToGrid(w)
h = snapToGrid(h)
newX = snapToGrid(newX)
newY = snapToGrid(newY)
}
assignAttributes(shape, {
width: w,
height: h,
x: newX,
y: newY
}, 1000)
break
}
case 'circle': {
cx = Number(shape.getAttribute('cx'))
cy = Number(shape.getAttribute('cy'))
let rad = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))
if (svgCanvas.getCurConfig().gridSnapping) {
rad = snapToGrid(rad)
}
shape.setAttribute('r', rad)
break
}
case 'ellipse': {
cx = Number(shape.getAttribute('cx'))
cy = Number(shape.getAttribute('cy'))
if (svgCanvas.getCurConfig().gridSnapping) {
x = snapToGrid(x)
cx = snapToGrid(cx)
y = snapToGrid(y)
cy = snapToGrid(cy)
}
shape.setAttribute('rx', Math.abs(x - cx))
const ry = Math.abs(evt.shiftKey ? (x - cx) : (y - cy))
shape.setAttribute('ry', ry)
break
}
case 'fhellipse':
case 'fhrect': {
svgCanvas.setFreehand('minx', Math.min(realX, svgCanvas.getFreehand('minx')))
svgCanvas.setFreehand('maxx', Math.max(realX, svgCanvas.getFreehand('maxx')))
svgCanvas.setFreehand('miny', Math.min(realY, svgCanvas.getFreehand('miny')))
svgCanvas.setFreehand('maxy', Math.max(realY, svgCanvas.getFreehand('maxy')))
}
// Fallthrough
case 'fhpath': {
// dAttr += + realX + ',' + realY + ' ';
// shape.setAttribute('points', dAttr);
svgCanvas.setEnd('x', realX)
svgCanvas.setEnd('y', realY)
if (svgCanvas.getControllPoint2('x') && svgCanvas.getControllPoint2('y')) {
for (i = 0; i < svgCanvas.getStepCount() - 1; i++) {
svgCanvas.setParameter(i / svgCanvas.getStepCount())
svgCanvas.setNextParameter((i + 1) / svgCanvas.getStepCount())
svgCanvas.setbSpline(getBsplinePoint(svgCanvas.getNextParameter()))
svgCanvas.setNextPos({ x: svgCanvas.getbSpline('x'), y: svgCanvas.getbSpline('y') })
svgCanvas.setbSpline(getBsplinePoint(svgCanvas.getParameter()))
svgCanvas.setSumDistance(
svgCanvas.getSumDistance() + Math.sqrt((svgCanvas.getNextPos('x') -
svgCanvas.getbSpline('x')) * (svgCanvas.getNextPos('x') -
svgCanvas.getbSpline('x')) + (svgCanvas.getNextPos('y') -
svgCanvas.getbSpline('y')) * (svgCanvas.getNextPos('y') - svgCanvas.getbSpline('y')))
)
if (svgCanvas.getSumDistance() > svgCanvas.getThreSholdDist()) {
svgCanvas.setSumDistance(svgCanvas.getSumDistance() - svgCanvas.getThreSholdDist())
// Faster than completely re-writing the points attribute.
const point = svgCanvas.getSvgContent().createSVGPoint()
point.x = svgCanvas.getbSpline('x')
point.y = svgCanvas.getbSpline('y')
shape.points.appendItem(point)
}
}
}
svgCanvas.setControllPoint2('x', svgCanvas.getControllPoint1('x'))
svgCanvas.setControllPoint2('y', svgCanvas.getControllPoint1('y'))
svgCanvas.setControllPoint1('x', svgCanvas.getStart('x'))
svgCanvas.setControllPoint1('y', svgCanvas.getStart('y'))
svgCanvas.setStart({ x: svgCanvas.getEnd('x'), y: svgCanvas.getEnd('y') })
break
// update path stretch line coordinates
}
case 'path': // fall through
case 'pathedit': {
x *= zoom
y *= zoom
if (svgCanvas.getCurConfig().gridSnapping) {
x = snapToGrid(x)
y = snapToGrid(y)
svgCanvas.setStartX(snapToGrid(svgCanvas.getStartX()))
svgCanvas.setStartY(snapToGrid(svgCanvas.getStartY()))
}
if (evt.shiftKey) {
const { path } = pathModule
let x1; let y1
if (path) {
x1 = path.dragging ? path.dragging[0] : svgCanvas.getStartX()
y1 = path.dragging ? path.dragging[1] : svgCanvas.getStartY()
} else {
x1 = svgCanvas.getStartX()
y1 = svgCanvas.getStartY()
}
xya = snapToAngle(x1, y1, x, y);
({ x, y } = xya)
}
if (svgCanvas.getRubberBox()?.getAttribute('display') !== 'none') {
realX *= zoom
realY *= zoom
assignAttributes(svgCanvas.getRubberBox(), {
x: Math.min(svgCanvas.getRStartX() * zoom, realX),
y: Math.min(svgCanvas.getRStartY() * zoom, realY),
width: Math.abs(realX - svgCanvas.getRStartX() * zoom),
height: Math.abs(realY - svgCanvas.getRStartY() * zoom)
}, 100)
}
svgCanvas.pathActions.mouseMove(x, y)
break
}
case 'textedit': {
x *= zoom
y *= zoom
svgCanvas.textActions.mouseMove(mouseX, mouseY)
break
}
case 'rotate': {
box = getBBox(selected)
cx = box.x + box.width / 2
cy = box.y + box.height / 2
const m = getMatrix(selected)
const center = transformPoint(cx, cy, m)
cx = center.x
cy = center.y
angle = ((Math.atan2(cy - y, cx - x) * (180 / Math.PI)) - 90) % 360
if (svgCanvas.getCurConfig().gridSnapping) {
angle = snapToGrid(angle)
}
if (evt.shiftKey) { // restrict rotations to nice angles (WRS)
const snap = 45
angle = Math.round(angle / snap) * snap
}
svgCanvas.setRotationAngle(angle < -180 ? (360 + angle) : angle, true)
svgCanvas.call('transition', selectedElements)
break
}
default:
// A mode can be defined by an extenstion
break
}
/**
* The mouse has moved on the canvas area.
* @event module:svgcanvas.SvgCanvas#event:ext_mouseMove
* @type {PlainObject}
* @property {MouseEvent} event The event object
* @property {Float} mouse_x x coordinate on canvas
* @property {Float} mouse_y y coordinate on canvas
* @property {Element} selected Refers to the first selected element
*/
svgCanvas.runExtensions('mouseMove', /** @type {module:svgcanvas.SvgCanvas#event:ext_mouseMove} */ {
event: evt,
mouse_x: mouseX,
mouse_y: mouseY,
selected
})
} // mouseMove()
/**
*
* @returns {void}
*/
const mouseOutEvent = () => {
const { $id } = svgCanvas
if (svgCanvas.getCurrentMode() !== 'select' && svgCanvas.getStarted()) {
const event = new Event('mouseup')
$id('svgcanvas').dispatchEvent(event)
}
}
// - in create mode, the element's opacity is set properly, we create an InsertElementCommand
// and store it on the Undo stack
// - in move/resize mode, the element's attributes which were affected by the move/resize are
// identified, a ChangeElementCommand is created and stored on the stack for those attrs
// this is done in when we recalculate the selected dimensions()
/**
*
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:zoomed
* @fires module:svgcanvas.SvgCanvas#event:changed
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseUp
* @returns {void}
*/
const mouseUpEvent = (evt) => {
if (evt.button === 2) { return }
if (!svgCanvas.getStarted()) { return }
svgCanvas.textActions.init()
const selectedElements = svgCanvas.getSelectedElements()
const zoom = svgCanvas.getZoom()
const tempJustSelected = svgCanvas.getJustSelected()
svgCanvas.setJustSelected(null)
const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
const mouseX = pt.x * zoom
const mouseY = pt.y * zoom
const x = mouseX / zoom
const y = mouseY / zoom
let element = getElement(svgCanvas.getId())
let keep = false
const realX = x
const realY = y
// TODO: Make true when in multi-unit mode
const useUnit = false // (svgCanvas.getCurConfig().baseUnit !== 'px');
svgCanvas.setStarted(false)
let t
switch (svgCanvas.getCurrentMode()) {
// intentionally fall-through to select here
case 'resize':
case 'multiselect':
if (svgCanvas.getRubberBox()) {
svgCanvas.getRubberBox().setAttribute('display', 'none')
svgCanvas.setCurBBoxes([])
}
svgCanvas.setCurrentMode('select')
// Fallthrough
case 'select':
if (selectedElements[0]) {
// if we only have one selected element
if (!selectedElements[1]) {
// set our current stroke/fill properties to the element's
const selected = selectedElements[0]
switch (selected.tagName) {
case 'g':
case 'use':
case 'image':
case 'foreignObject':
break
case 'text':
svgCanvas.setCurText('font_size', selected.getAttribute('font-size'))
svgCanvas.setCurText('font_family', selected.getAttribute('font-family'))
// fallthrough
default:
svgCanvas.setCurProperties('fill', selected.getAttribute('fill'))
svgCanvas.setCurProperties('fill_opacity', selected.getAttribute('fill-opacity'))
svgCanvas.setCurProperties('stroke', selected.getAttribute('stroke'))
svgCanvas.setCurProperties('stroke_opacity', selected.getAttribute('stroke-opacity'))
svgCanvas.setCurProperties('stroke_width', selected.getAttribute('stroke-width'))
svgCanvas.setCurProperties('stroke_dasharray', selected.getAttribute('stroke-dasharray'))
svgCanvas.setCurProperties('stroke_linejoin', selected.getAttribute('stroke-linejoin'))
svgCanvas.setCurProperties('stroke_linecap', selected.getAttribute('stroke-linecap'))
}
svgCanvas.selectorManager.requestSelector(selected).showGrips(true)
}
// always recalculate dimensions to strip off stray identity transforms
svgCanvas.recalculateAllSelectedDimensions()
// if it was being dragged/resized
if (realX !== svgCanvas.getRStartX() || realY !== svgCanvas.getRStartY()) {
const len = selectedElements.length
for (let i = 0; i < len; ++i) {
if (!selectedElements[i]) { break }
svgCanvas.selectorManager.requestSelector(selectedElements[i]).resize()
}
// no change in position/size, so maybe we should move to pathedit
} else {
t = evt.target
if (selectedElements[0].nodeName === 'path' && !selectedElements[1]) {
svgCanvas.pathActions.select(selectedElements[0])
// if it was a path
// else, if it was selected and this is a shift-click, remove it from selection
} else if (evt.shiftKey && tempJustSelected !== t) {
svgCanvas.removeFromSelection([t])
}
} // no change in mouse position
// Remove non-scaling stroke
const elem = selectedElements[0]
if (elem) {
elem.removeAttribute('style')
walkTree(elem, (el) => {
el.removeAttribute('style')
})
}
}
return
case 'zoom': {
svgCanvas.getRubberBox()?.setAttribute('display', 'none')
const factor = evt.shiftKey ? 0.5 : 2
svgCanvas.call('zoomed', {
x: Math.min(svgCanvas.getRStartX(), realX),
y: Math.min(svgCanvas.getRStartY(), realY),
width: Math.abs(realX - svgCanvas.getRStartX()),
height: Math.abs(realY - svgCanvas.getRStartY()),
factor
})
return
} case 'fhpath': {
// Check that the path contains at least 2 points; a degenerate one-point path
// causes problems.
// Webkit ignores how we set the points attribute with commas and uses space
// to separate all coordinates, see https://bugs.webkit.org/show_bug.cgi?id=29870
svgCanvas.setSumDistance(0)
svgCanvas.setControllPoint2('x', 0)
svgCanvas.setControllPoint2('y', 0)
svgCanvas.setControllPoint1('x', 0)
svgCanvas.setControllPoint1('y', 0)
svgCanvas.setStart({ x: 0, y: 0 })
svgCanvas.setEnd('x', 0)
svgCanvas.setEnd('y', 0)
const coords = element.getAttribute('points')
const commaIndex = coords.indexOf(',')
keep = commaIndex >= 0 ? coords.includes(',', commaIndex + 1) : coords.includes(' ', coords.indexOf(' ') + 1)
if (keep) {
element = svgCanvas.pathActions.smoothPolylineIntoPath(element)
}
break
} case 'line': {
const x1 = element.getAttribute('x1')
const y1 = element.getAttribute('y1')
const x2 = element.getAttribute('x2')
const y2 = element.getAttribute('y2')
keep = (x1 !== x2 || y1 !== y2)
}
break
case 'foreignObject':
case 'square':
case 'rect':
case 'image': {
const width = element.getAttribute('width')
const height = element.getAttribute('height')
// Image should be kept regardless of size (use inherit dimensions later)
keep = (width || height) || svgCanvas.getCurrentMode() === 'image'
}
break
case 'circle':
keep = (element.getAttribute('r') !== '0')
break
case 'ellipse': {
const rx = Number(element.getAttribute('rx'))
const ry = Number(element.getAttribute('ry'))
keep = (rx || ry)
}
break
case 'fhellipse':
if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
(svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
element = svgCanvas.addSVGElementsFromJson({
element: 'ellipse',
curStyles: true,
attr: {
cx: (svgCanvas.getFreehand('minx') + svgCanvas.getFreehand('maxx')) / 2,
cy: (svgCanvas.getFreehand('miny') + svgCanvas.getFreehand('maxy')) / 2,
rx: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) / 2,
ry: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) / 2,
id: svgCanvas.getId()
}
})
svgCanvas.call('changed', [element])
keep = true
}
break
case 'fhrect':
if ((svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')) > 0 &&
(svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')) > 0) {
element = svgCanvas.addSVGElementsFromJson({
element: 'rect',
curStyles: true,
attr: {
x: svgCanvas.getFreehand('minx'),
y: svgCanvas.getFreehand('miny'),
width: (svgCanvas.getFreehand('maxx') - svgCanvas.getFreehand('minx')),
height: (svgCanvas.getFreehand('maxy') - svgCanvas.getFreehand('miny')),
id: svgCanvas.getId()
}
})
svgCanvas.call('changed', [element])
keep = true
}
break
case 'text':
keep = true
svgCanvas.selectOnly([element])
svgCanvas.textActions.start(element)
break
case 'path': {
// set element to null here so that it is not removed nor finalized
element = null
// continue to be set to true so that mouseMove happens
svgCanvas.setStarted(true)
const res = svgCanvas.pathActions.mouseUp(evt, element, mouseX, mouseY);
({ element } = res);
({ keep } = res)
break
} case 'pathedit':
keep = true
element = null
svgCanvas.pathActions.mouseUp(evt)
break
case 'textedit':
keep = false
element = null
svgCanvas.textActions.mouseUp(evt, mouseX, mouseY)
break
case 'rotate': {
keep = true
element = null
svgCanvas.setCurrentMode('select')
const batchCmd = svgCanvas.undoMgr.finishUndoableChange()
if (!batchCmd.isEmpty()) {
svgCanvas.addCommandToHistory(batchCmd)
}
// perform recalculation to weed out any stray identity transforms that might get stuck
svgCanvas.recalculateAllSelectedDimensions()
svgCanvas.call('changed', selectedElements)
break
} default:
// This could occur in an extension
break
}
/**
* The main (left) mouse button is released (anywhere).
* @event module:svgcanvas.SvgCanvas#event:ext_mouseUp
* @type {PlainObject}
* @property {MouseEvent} event The event object
* @property {Float} mouse_x x coordinate on canvas
* @property {Float} mouse_y y coordinate on canvas
*/
const extResult = svgCanvas.runExtensions('mouseUp', {
event: evt,
mouse_x: mouseX,
mouse_y: mouseY
}, true)
extResult.forEach((r) => {
if (r) {
keep = r.keep || keep;
({ element } = r)
svgCanvas.setStarted(r.started || svgCanvas.getStarted())
}
})
if (!keep && element) {
svgCanvas.getCurrentDrawing().releaseId(svgCanvas.getId())
element.remove()
element = null
t = evt.target
// if this element is in a group, go up until we reach the top-level group
// just below the layer groups
// TODO: once we implement links, we also would have to check for <a> elements
while (t?.parentNode?.parentNode?.tagName === 'g') {
t = t.parentNode
}
// if we are not in the middle of creating a path, and we've clicked on some shape,
// then go to Select mode.
// WebKit returns <div> when the canvas is clicked, Firefox/Opera return <svg>
if ((svgCanvas.getCurrentMode() !== 'path' || !svgCanvas.getDrawnPath()) &&
t &&
t.parentNode?.id !== 'selectorParentGroup' &&
t.id !== 'svgcanvas' && t.id !== 'svgroot'
) {
// switch into "select" mode if we've clicked on an element
svgCanvas.setMode('select')
svgCanvas.selectOnly([t], true)
}
} else if (element) {
/**
* @name module:svgcanvas.SvgCanvas#addedNew
* @type {boolean}
*/
svgCanvas.addedNew = true
if (useUnit) { convertAttrs(element) }
let aniDur = 0.2
let cAni
const curShape = svgCanvas.getStyle()
const opacAni = svgCanvas.getOpacAni()
if (opacAni.beginElement && Number.parseFloat(element.getAttribute('opacity')) !== curShape.opacity) {
cAni = opacAni.cloneNode(true)
cAni.setAttribute('to', curShape.opacity)
cAni.setAttribute('dur', aniDur)
element.appendChild(cAni)
try {
// Fails in FF4 on foreignObject
cAni.beginElement()
} catch (e) { /* empty fn */ }
} else {
aniDur = 0
}
// Ideally this would be done on the endEvent of the animation,
// but that doesn't seem to be supported in Webkit
setTimeout(() => {
if (cAni) { cAni.remove() }
element.setAttribute('opacity', curShape.opacity)
element.setAttribute('style', 'pointer-events:inherit')
cleanupElement(element)
if (svgCanvas.getCurrentMode() === 'path') {
svgCanvas.pathActions.toEditMode(element)
} else if (svgCanvas.getCurConfig().selectNew) {
const modes = ['circle', 'ellipse', 'square', 'rect', 'fhpath', 'line', 'fhellipse', 'fhrect', 'star', 'polygon']
if (modes.indexOf(svgCanvas.getCurrentMode()) !== -1) {
svgCanvas.setMode('select')
}
svgCanvas.selectOnly([element], true)
}
// we create the insert command that is stored on the stack
// undo means to call cmd.unapply(), redo means to call cmd.apply()
svgCanvas.addCommandToHistory(new InsertElementCommand(element))
svgCanvas.call('changed', [element])
}, aniDur * 1000)
}
svgCanvas.setStartTransform(null)
}
const dblClickEvent = (evt) => {
const selectedElements = svgCanvas.getSelectedElements()
const evtTarget = evt.target
const parent = evtTarget.parentNode
let mouseTarget = svgCanvas.getMouseTarget(evt)
const { tagName } = mouseTarget
if (tagName === 'text' && svgCanvas.getCurrentMode() !== 'textedit') {
const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
svgCanvas.textActions.select(mouseTarget, pt.x, pt.y)
}
// Do nothing if already in current group
if (parent === svgCanvas.getCurrentGroup()) { return }
if ((tagName === 'g' || tagName === 'a') && getRotationAngle(mouseTarget)) {
// TODO: Allow method of in-group editing without having to do
// this (similar to editing rotated paths)
// Ungroup and regroup
svgCanvas.pushGroupProperties(mouseTarget)
mouseTarget = selectedElements[0]
svgCanvas.clearSelection(true)
}
// Reset context
if (svgCanvas.getCurrentGroup()) {
draw.leaveContext()
}
if ((parent.tagName !== 'g' && parent.tagName !== 'a') ||
parent === svgCanvas.getCurrentDrawing().getCurrentLayer() ||
mouseTarget === svgCanvas.selectorManager.selectorParentGroup
) {
// Escape from in-group edit
return
}
draw.setContext(mouseTarget)
}
/**
* Follows these conditions:
* - When we are in a create mode, the element is added to the canvas but the
* action is not recorded until mousing up.
* - When we are in select mode, select the element, remember the position
* and do nothing else.
* @param {MouseEvent} evt
* @fires module:svgcanvas.SvgCanvas#event:ext_mouseDown
* @returns {void}
*/
const mouseDownEvent = (evt) => {
const dataStorage = svgCanvas.getDataStorage()
const selectedElements = svgCanvas.getSelectedElements()
const zoom = svgCanvas.getZoom()
const curShape = svgCanvas.getStyle()
const svgRoot = svgCanvas.getSvgRoot()
const { $id } = svgCanvas
if (svgCanvas.spaceKey || evt.button === 1) { return }
const rightClick = (evt.button === 2)
if (evt.altKey) { // duplicate when dragging
svgCanvas.cloneSelectedElements(0, 0)
}
svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
const mouseX = pt.x * zoom
const mouseY = pt.y * zoom
evt.preventDefault()
if (rightClick) {
if (svgCanvas.getCurrentMode() === 'path') {
return
}
svgCanvas.setCurrentMode('select')
svgCanvas.setLastClickPoint(pt)
}
let x = mouseX / zoom
let y = mouseY / zoom
let mouseTarget = svgCanvas.getMouseTarget(evt)
if (mouseTarget.tagName === 'a' && mouseTarget.childNodes.length === 1) {
mouseTarget = mouseTarget.firstChild
}
// realX/y ignores grid-snap value
const realX = x
svgCanvas.setStartX(x)
svgCanvas.setRStartX(x)
const realY = y
svgCanvas.setStartY(y)
svgCanvas.setRStartY(y)
if (svgCanvas.getCurConfig().gridSnapping) {
x = snapToGrid(x)
y = snapToGrid(y)
svgCanvas.setStartX(snapToGrid(svgCanvas.getStartX()))
svgCanvas.setStartY(snapToGrid(svgCanvas.getStartY()))
}
// if it is a selector grip, then it must be a single element selected,
// set the mouseTarget to that and update the mode to rotate/resize
if (mouseTarget === svgCanvas.selectorManager.selectorParentGroup && selectedElements[0]) {
const grip = evt.target
const griptype = dataStorage.get(grip, 'type')
// rotating
if (griptype === 'rotate') {
svgCanvas.setCurrentMode('rotate')
// svgCanvas.setCurrentRotateMode(dataStorage.get(grip, 'dir'));
// resizing
} else if (griptype === 'resize') {
svgCanvas.setCurrentMode('resize')
svgCanvas.setCurrentResizeMode(dataStorage.get(grip, 'dir'))
}
mouseTarget = selectedElements[0]
}
svgCanvas.setStartTransform(mouseTarget.getAttribute('transform'))
const tlist = mouseTarget.transform.baseVal
// consolidate transforms using standard SVG but keep the transformation used for the move/scale
if (tlist.numberOfItems > 1) {
const firstTransform = tlist.getItem(0)
tlist.removeItem(0)
tlist.consolidate()
tlist.insertItemBefore(firstTransform, 0)
}
switch (svgCanvas.getCurrentMode()) {
case 'select':
svgCanvas.setStarted(true)
svgCanvas.setCurrentResizeMode('none')
if (rightClick) { svgCanvas.setStarted(false) }
if (mouseTarget !== svgRoot) {
// if this element is not yet selected, clear selection and select it
if (!selectedElements.includes(mouseTarget)) {
// only clear selection if shift is not pressed (otherwise, add
// element to selection)
if (!evt.shiftKey) {
// No need to do the call here as it will be done on addToSelection
svgCanvas.clearSelection(true)
}
svgCanvas.addToSelection([mouseTarget])
svgCanvas.setJustSelected(mouseTarget)
svgCanvas.pathActions.clear()
}
// else if it's a path, go into pathedit mode in mouseup
if (!rightClick) {
// insert a dummy transform so if the element(s) are moved it will have
// a transform to use for its translate
for (const selectedElement of selectedElements) {
if (!selectedElement) { continue }
const slist = selectedElement.transform?.baseVal
if (slist.numberOfItems) {
slist.insertItemBefore(svgRoot.createSVGTransform(), 0)
} else {
slist.appendItem(svgRoot.createSVGTransform())
}
}
}
} else if (!rightClick) {
svgCanvas.clearSelection()
svgCanvas.setCurrentMode('multiselect')
if (!svgCanvas.getRubberBox()) {
svgCanvas.setRubberBox(svgCanvas.selectorManager.getRubberBandBox())
}
svgCanvas.setRStartX(svgCanvas.getRStartX() * zoom)
svgCanvas.setRStartY(svgCanvas.getRStartY() * zoom)
assignAttributes(svgCanvas.getRubberBox(), {
x: svgCanvas.getRStartX(),
y: svgCanvas.getRStartY(),
width: 0,
height: 0,
display: 'inline'
}, 100)
}
break
case 'zoom':
svgCanvas.setStarted(true)
if (!svgCanvas.getRubberBox()) {
svgCanvas.setRubberBox(svgCanvas.selectorManager.getRubberBandBox())
}
assignAttributes(svgCanvas.getRubberBox(), {
x: realX * zoom,
y: realX * zoom,
width: 0,
height: 0,
display: 'inline'
}, 100)
break
case 'resize': {
svgCanvas.setStarted(true)
svgCanvas.setStartX(x)
svgCanvas.setStartY(y)
// Getting the BBox from the selection box, since we know we
// want to orient around it
svgCanvas.setInitBbox(getBBox($id('selectedBox0')))
const bb = {}
for (const [key, val] of Object.entries(svgCanvas.getInitBbox())) {
bb[key] = val / zoom
}
svgCanvas.setInitBbox(bb)
// append three dummy transforms to the tlist so that
// we can translate,scale,translate in mousemove
const pos = getRotationAngle(mouseTarget) ? 1 : 0
if (hasMatrixTransform(tlist)) {
tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
tlist.insertItemBefore(svgRoot.createSVGTransform(), pos)
} else {
tlist.appendItem(svgRoot.createSVGTransform())
tlist.appendItem(svgRoot.createSVGTransform())
tlist.appendItem(svgRoot.createSVGTransform())
}
break
}
case 'fhellipse':
case 'fhrect':
case 'fhpath':
svgCanvas.setStart({ x: realX, y: realY })
svgCanvas.setControllPoint1('x', 0)
svgCanvas.setControllPoint1('y', 0)
svgCanvas.setControllPoint2('x', 0)
svgCanvas.setControllPoint2('y', 0)
svgCanvas.setStarted(true)
svgCanvas.setDAttr(realX + ',' + realY + ' ')
// Commented out as doing nothing now:
// strokeW = parseFloat(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;
svgCanvas.addSVGElementsFromJson({
element: 'polyline',
curStyles: true,
attr: {
points: svgCanvas.getDAttr(),
id: svgCanvas.getNextId(),
fill: 'none',
opacity: curShape.opacity / 2,
'stroke-linecap': 'round',
style: 'pointer-events:none'
}
})
svgCanvas.setFreehand('minx', realX)
svgCanvas.setFreehand('maxx', realX)
svgCanvas.setFreehand('miny', realY)
svgCanvas.setFreehand('maxy', realY)
break
case 'image': {
svgCanvas.setStarted(true)
const newImage = svgCanvas.addSVGElementsFromJson({
element: 'image',
attr: {
x,
y,
width: 0,
height: 0,
id: svgCanvas.getNextId(),
opacity: curShape.opacity / 2,
style: 'pointer-events:inherit'
}
})
setHref(newImage, svgCanvas.getLastGoodImgUrl())
preventClickDefault(newImage)
break
} case 'square':
// TODO: once we create the rect, we lose information that this was a square
// (for resizing purposes this could be important)
// Fallthrough
case 'rect':
svgCanvas.setStarted(true)
svgCanvas.setStartX(x)
svgCanvas.setStartY(y)
svgCanvas.addSVGElementsFromJson({
element: 'rect',
curStyles: true,
attr: {
x,
y,
width: 0,
height: 0,
id: svgCanvas.getNextId(),
opacity: curShape.opacity / 2
}
})
break
case 'line': {
svgCanvas.setStarted(true)
const strokeW = Number(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width
svgCanvas.addSVGElementsFromJson({
element: 'line',
curStyles: true,
attr: {
x1: x,
y1: y,
x2: x,
y2: y,
id: svgCanvas.getNextId(),
stroke: curShape.stroke,
'stroke-width': strokeW,
'stroke-dasharray': curShape.stroke_dasharray,
'stroke-linejoin': curShape.stroke_linejoin,
'stroke-linecap': curShape.stroke_linecap,
'stroke-opacity': curShape.stroke_opacity,
fill: 'none',
opacity: curShape.opacity / 2,
style: 'pointer-events:none'
}
})
break
} case 'circle':
svgCanvas.setStarted(true)
svgCanvas.addSVGElementsFromJson({
element: 'circle',
curStyles: true,
attr: {
cx: x,
cy: y,
r: 0,
id: svgCanvas.getNextId(),
opacity: curShape.opacity / 2
}
})
break
case 'ellipse':
svgCanvas.setStarted(true)
svgCanvas.addSVGElementsFromJson({
element: 'ellipse',
curStyles: true,
attr: {
cx: x,
cy: y,
rx: 0,
ry: 0,
id: svgCanvas.getNextId(),
opacity: curShape.opacity / 2
}
})
break
case 'text':
svgCanvas.setStarted(true)
/* const newText = */ svgCanvas.addSVGElementsFromJson({
element: 'text',
curStyles: true,
attr: {
x,
y,
id: svgCanvas.getNextId(),
fill: svgCanvas.getCurText('fill'),
'stroke-width': svgCanvas.getCurText('stroke_width'),
'font-size': svgCanvas.getCurText('font_size'),
'font-family': svgCanvas.getCurText('font_family'),
'text-anchor': 'middle',
'xml:space': 'preserve',
opacity: curShape.opacity
}
})
// newText.textContent = 'text';
break
case 'path':
// Fall through
case 'pathedit':
svgCanvas.setStartX(svgCanvas.getStartX() * zoom)
svgCanvas.setStartY(svgCanvas.getStartY() * zoom)
svgCanvas.pathActions.mouseDown(evt, mouseTarget, svgCanvas.getStartX(), svgCanvas.getStartY())
svgCanvas.setStarted(true)
break
case 'textedit':
svgCanvas.setStartX(svgCanvas.getStartX() * zoom)
svgCanvas.setStartY(svgCanvas.getStartY() * zoom)
svgCanvas.textActions.mouseDown(evt, mouseTarget, svgCanvas.getStartX(), svgCanvas.getStartY())
svgCanvas.setStarted(true)
break
case 'rotate':
svgCanvas.setStarted(true)
// we are starting an undoable change (a drag-rotation)
svgCanvas.undoMgr.beginUndoableChange('transform', selectedElements)
break
default:
// This could occur in an extension
break
}
/**
* The main (left) mouse button is held down on the canvas area.
* @event module:svgcanvas.SvgCanvas#event:ext_mouseDown
* @type {PlainObject}
* @property {MouseEvent} event The event object
* @property {Float} start_x x coordinate on canvas
* @property {Float} start_y y coordinate on canvas
* @property {Element[]} selectedElements An array of the selected Elements
*/
const extResult = svgCanvas.runExtensions('mouseDown', {
event: evt,
start_x: svgCanvas.getStartX(),
start_y: svgCanvas.getStartY(),
selectedElements
}, true)
extResult.forEach((r) => {
if (r?.started) {
svgCanvas.setStarted(true)
}
})
}
/**
* @param {Event} e
* @fires module:event.SvgCanvas#event:updateCanvas
* @fires module:event.SvgCanvas#event:zoomDone
* @returns {void}
*/
const DOMMouseScrollEvent = (e) => {
const zoom = svgCanvas.getZoom()
const { $id } = svgCanvas
if (!e.shiftKey) { return }
e.preventDefault()
svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
const workarea = document.getElementById('workarea')
const scrbar = 15
const rulerwidth = svgCanvas.getCurConfig().showRulers ? 16 : 0
// mouse relative to content area in content pixels
const pt = transformPoint(e.clientX, e.clientY, svgCanvas.getrootSctm())
// full work area width in screen pixels
const editorFullW = parseFloat(getComputedStyle(workarea, null).width.replace('px', ''))
const editorFullH = parseFloat(getComputedStyle(workarea, null).height.replace('px', ''))
// work area width minus scroll and ruler in screen pixels
const editorW = editorFullW - scrbar - rulerwidth
const editorH = editorFullH - scrbar - rulerwidth
// work area width in content pixels
const workareaViewW = editorW * svgCanvas.getrootSctm().a
const workareaViewH = editorH * svgCanvas.getrootSctm().d
// content offset from canvas in screen pixels
const wOffset = findPos(workarea)
const wOffsetLeft = wOffset.left + rulerwidth
const wOffsetTop = wOffset.top + rulerwidth
const delta = (e.wheelDelta) ? e.wheelDelta : (e.detail) ? -e.detail : 0
if (!delta) { return }
let factor = Math.max(3 / 4, Math.min(4 / 3, (delta)))
let wZoom; let hZoom
if (factor > 1) {
wZoom = Math.ceil(editorW / workareaViewW * factor * 100) / 100
hZoom = Math.ceil(editorH / workareaViewH * factor * 100) / 100
} else {
wZoom = Math.floor(editorW / workareaViewW * factor * 100) / 100
hZoom = Math.floor(editorH / workareaViewH * factor * 100) / 100
}
let zoomlevel = Math.min(wZoom, hZoom)
zoomlevel = Math.min(10, Math.max(0.01, zoomlevel))
if (zoomlevel === zoom) {
return
}
factor = zoomlevel / zoom
// top left of workarea in content pixels before zoom
const topLeftOld = transformPoint(wOffsetLeft, wOffsetTop, svgCanvas.getrootSctm())
// top left of workarea in content pixels after zoom
const topLeftNew = {
x: pt.x - (pt.x - topLeftOld.x) / factor,
y: pt.y - (pt.y - topLeftOld.y) / factor
}
// top left of workarea in canvas pixels relative to content after zoom
const topLeftNewCanvas = {
x: topLeftNew.x * zoomlevel,
y: topLeftNew.y * zoomlevel
}
// new center in canvas pixels
const newCtr = {
x: topLeftNewCanvas.x - rulerwidth + editorFullW / 2,
y: topLeftNewCanvas.y - rulerwidth + editorFullH / 2
}
svgCanvas.setZoom(zoomlevel)
document.getElementById('zoom').value = ((zoomlevel * 100).toFixed(1))
svgCanvas.call('updateCanvas', { center: false, newCtr })
svgCanvas.call('zoomDone')
}