/** * 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 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
when the canvas is clicked, Firefox/Opera return 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') }