Source: svgcanvas/event.js

/**
 * Tools for event.
 * @module event
 * @license MIT
 * @copyright 2011 Jeff Schiller
 */
import {
  assignAttributes, cleanupElement, getElem, getRotationAngle, snapToGrid, walkTree,
  getBBox as utilsGetBBox, isNullish, preventClickDefault, setHref
} from './utilities.js';
import {
  convertAttrs
} from '../common/units.js';
import {
  transformPoint, hasMatrixTransform, getMatrix, snapToAngle
} from './math.js';
import { supportsNonScalingStroke } from '../common/browser.js';
import * as draw from './draw.js';
import * as pathModule from './path.js';
import * as hstry from './history.js';
import { findPos } from '../editor/components/jgraduate/Util.js';

const {
  InsertElementCommand
} = hstry;

let eventContext_ = null;

/**
* @function module:undo.init
* @param {module:undo.eventContext} eventContext
* @returns {void}
*/
export const init = function (eventContext) {
  eventContext_ = eventContext;
};

export const getBsplinePoint = function (t) {
  const spline = { x: 0, y: 0 };
  const p0 = { x: eventContext_.getControllPoint2('x'), y: eventContext_.getControllPoint2('y') };
  const p1 = { x: eventContext_.getControllPoint1('x'), y: eventContext_.getControllPoint1('y') };
  const p2 = { x: eventContext_.getStart('x'), y: eventContext_.getStart('y') };
  const p3 = { x: eventContext_.getEnd('x'), y: eventContext_.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
  };
};

/**
 *
 * @param {MouseEvent} evt
 * @fires module:svgcanvas.SvgCanvas#event:transition
 * @fires module:svgcanvas.SvgCanvas#event:ext_mouseMove
 * @returns {void}
 */
export const mouseMoveEvent = function (evt) {
  const selectedElements = eventContext_.getSelectedElements;
  const currentZoom = eventContext_.getCurrentZoom();
  const svgRoot = eventContext_.getSVGRoot();
  const svgCanvas = eventContext_.getCanvas();

  if (!eventContext_.getStarted()) { return; }
  if (evt.button === 1 || svgCanvas.spaceKey) { return; }

  let i;
  let xya;
  let cx;
  let cy;
  let dx;
  let dy;
  let len;
  let angle;
  let box;
  let selected = selectedElements()[0];
  const pt = transformPoint(evt.clientX, evt.clientY, eventContext_.getrootSctm());
  const mouseX = pt.x * currentZoom;
  const mouseY = pt.y * currentZoom;
  const shape = getElem(eventContext_.getId());

  let realX = mouseX / currentZoom;
  let x = realX;
  let realY = mouseY / currentZoom;
  let y = realY;

  if (eventContext_.getCurConfig().gridSnapping) {
    x = snapToGrid(x);
    y = snapToGrid(y);
  }

  evt.preventDefault();
  let tlist;
  switch (eventContext_.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 (selectedElements()[0] !== null) {
      dx = x - eventContext_.getStartX();
      dy = y - eventContext_.getStartY();
      if (eventContext_.getCurConfig().gridSnapping) {
        dx = snapToGrid(dx);
        dy = snapToGrid(dy);
      }

      if (dx !== 0 || dy !== 0) {
        len = selectedElements().length;
        for (i = 0; i < len; ++i) {
          selected = selectedElements()[i];
          if (isNullish(selected)) { break; }
          // if (i === 0) {
          //   const box = utilsGetBBox(selected);
          //     selectedBBoxes[i].x = box.x + dx;
          //     selectedBBoxes[i].y = box.y + dy;
          // }

          // update the dummy transform in our transform list
          // to be a translate
          const xform = svgRoot.createSVGTransform();
          tlist = selected.transform?.baseVal;
          // Note that if Webkit and there's no ID for this
          // element, the dummy transform may have gotten lost.
          // This results in unexpected behaviour

          xform.setTranslate(dx, dy);
          if (tlist.numberOfItems) {
            tlist.replaceItem(xform, 0);
          } else {
            tlist.appendItem(xform);
          }

          // update our internal bbox that we're tracking while dragging
          svgCanvas.selectorManager.requestSelector(selected).resize();
        }

        svgCanvas.call('transition', selectedElements());
      }
    }
    break;
  } case 'multiselect': {
    realX *= currentZoom;
    realY *= currentZoom;
    assignAttributes(eventContext_.getRubberBox(), {
      x: Math.min(eventContext_.getRStartX(), realX),
      y: Math.min(eventContext_.getRStartY(), realY),
      width: Math.abs(realX - eventContext_.getRStartX()),
      height: Math.abs(realY - eventContext_.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 = eventContext_.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 ? eventContext_.getInitBbox() : utilsGetBBox(selected);
    let left = box.x;
    let top = box.y;
    let { width, height } = box;
    dx = (x - eventContext_.getStartX());
    dy = (y - eventContext_.getStartY());

    if (eventContext_.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 (!eventContext_.getCurrentResizeMode().includes('n') && !eventContext_.getCurrentResizeMode().includes('s')) {
      dy = 0;
    }
    if (!eventContext_.getCurrentResizeMode().includes('e') && !eventContext_.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 (eventContext_.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 (eventContext_.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 (eventContext_.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 *= currentZoom;
    realY *= currentZoom;
    assignAttributes(eventContext_.getRubberBox(), {
      x: Math.min(eventContext_.getRStartX() * currentZoom, realX),
      y: Math.min(eventContext_.getRStartY() * currentZoom, realY),
      width: Math.abs(realX - eventContext_.getRStartX() * currentZoom),
      height: Math.abs(realY - eventContext_.getRStartY() * currentZoom)
    }, 100);
    break;
  } case 'text': {
    assignAttributes(shape, {
      x,
      y
    }, 1000);
    break;
  } case 'line': {
    if (eventContext_.getCurConfig().gridSnapping) {
      x = snapToGrid(x);
      y = snapToGrid(y);
    }

    let x2 = x;
    let y2 = y;

    if (evt.shiftKey) {
      xya = snapToAngle(eventContext_.getStartX(), eventContext_.getStartY(), x2, y2);
      x2 = xya.x;
      y2 = xya.y;
    }

    shape.setAttribute('x2', x2);
    shape.setAttribute('y2', y2);
    break;
  } case 'foreignObject':
    // fall through
  case 'square':
    // fall through
  case 'rect':
    // fall through
  case 'image': {
    const square = (eventContext_.getCurrentMode() === 'square') || evt.shiftKey;
    let
      w = Math.abs(x - eventContext_.getStartX());
    let h = Math.abs(y - eventContext_.getStartY());
    let newX; let newY;
    if (square) {
      w = h = Math.max(w, h);
      newX = eventContext_.getStartX() < x ? eventContext_.getStartX() : eventContext_.getStartX() - w;
      newY = eventContext_.getStartY() < y ? eventContext_.getStartY() : eventContext_.getStartY() - h;
    } else {
      newX = Math.min(eventContext_.getStartX(), x);
      newY = Math.min(eventContext_.getStartY(), y);
    }

    if (eventContext_.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 (eventContext_.getCurConfig().gridSnapping) {
      rad = snapToGrid(rad);
    }
    shape.setAttribute('r', rad);
    break;
  } case 'ellipse': {
    cx = Number(shape.getAttribute('cx'));
    cy = Number(shape.getAttribute('cy'));
    if (eventContext_.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': {
    eventContext_.setFreehand('minx', Math.min(realX, eventContext_.getFreehand('minx')));
    eventContext_.setFreehand('maxx', Math.max(realX, eventContext_.getFreehand('maxx')));
    eventContext_.setFreehand('miny', Math.min(realY, eventContext_.getFreehand('miny')));
    eventContext_.setFreehand('maxy', Math.max(realY, eventContext_.getFreehand('maxy')));
  }
  // Fallthrough
  case 'fhpath': {
    // dAttr += + realX + ',' + realY + ' ';
    // shape.setAttribute('points', dAttr);
    eventContext_.setEnd('x', realX);
    eventContext_.setEnd('y', realY);
    if (eventContext_.getControllPoint2('x') && eventContext_.getControllPoint2('y')) {
      for (i = 0; i < eventContext_.getStepCount() - 1; i++) {
        eventContext_.setParameter(i / eventContext_.getStepCount());
        eventContext_.setNextParameter((i + 1) / eventContext_.getStepCount());
        eventContext_.setbSpline(getBsplinePoint(eventContext_.getNextParameter()));
        eventContext_.setNextPos({ x: eventContext_.getbSpline('x'), y: eventContext_.getbSpline('y') });
        eventContext_.setbSpline(getBsplinePoint(eventContext_.getParameter()));
        eventContext_.setSumDistance(
          eventContext_.getSumDistance() + Math.sqrt((eventContext_.getNextPos('x') -
              eventContext_.getbSpline('x')) * (eventContext_.getNextPos('x') -
                eventContext_.getbSpline('x')) + (eventContext_.getNextPos('y') -
                  eventContext_.getbSpline('y')) * (eventContext_.getNextPos('y') - eventContext_.getbSpline('y')))
        );
        if (eventContext_.getSumDistance() > eventContext_.getThreSholdDist()) {
          eventContext_.setSumDistance(eventContext_.getSumDistance() - eventContext_.getThreSholdDist());

          // Faster than completely re-writing the points attribute.
          const point = eventContext_.getSVGContent().createSVGPoint();
          point.x = eventContext_.getbSpline('x');
          point.y = eventContext_.getbSpline('y');
          shape.points.appendItem(point);
        }
      }
    }
    eventContext_.setControllPoint2('x', eventContext_.getControllPoint1('x'));
    eventContext_.setControllPoint2('y', eventContext_.getControllPoint1('y'));
    eventContext_.setControllPoint1('x', eventContext_.getStart('x'));
    eventContext_.setControllPoint1('y', eventContext_.getStart('y'));
    eventContext_.setStart({ x: eventContext_.getEnd('x'), y: eventContext_.getEnd('y') });
    break;
    // update path stretch line coordinates
  } case 'path':
    // fall through
  case 'pathedit': {
    x *= currentZoom;
    y *= currentZoom;

    if (eventContext_.getCurConfig().gridSnapping) {
      x = snapToGrid(x);
      y = snapToGrid(y);
      eventContext_.setStartX(snapToGrid(eventContext_.getStartX()));
      eventContext_.setStartY(snapToGrid(eventContext_.getStartY()));
    }
    if (evt.shiftKey) {
      const { path } = pathModule;
      let x1; let y1;
      if (path) {
        x1 = path.dragging ? path.dragging[0] : eventContext_.getStartX();
        y1 = path.dragging ? path.dragging[1] : eventContext_.getStartY();
      } else {
        x1 = eventContext_.getStartX();
        y1 = eventContext_.getStartY();
      }
      xya = snapToAngle(x1, y1, x, y);
      ({ x, y } = xya);
    }

    if (eventContext_.getRubberBox() && eventContext_.getRubberBox().getAttribute('display') !== 'none') {
      realX *= currentZoom;
      realY *= currentZoom;
      assignAttributes(eventContext_.getRubberBox(), {
        x: Math.min(eventContext_.getRStartX() * currentZoom, realX),
        y: Math.min(eventContext_.getRStartY() * currentZoom, realY),
        width: Math.abs(realX - eventContext_.getRStartX() * currentZoom),
        height: Math.abs(realY - eventContext_.getRStartY() * currentZoom)
      }, 100);
    }
    svgCanvas.pathActions.mouseMove(x, y);

    break;
  } case 'textedit': {
    x *= currentZoom;
    y *= currentZoom;
    // if (eventContext_.getRubberBox() && eventContext_.getRubberBox().getAttribute('display') !== 'none') {
    //   assignAttributes(eventContext_.getRubberBox(), {
    //     x: Math.min(eventContext_.getStartX(), x),
    //     y: Math.min(eventContext_.getStartY(), y),
    //     width: Math.abs(x - eventContext_.getStartX()),
    //     height: Math.abs(y - eventContext_.getStartY())
    //   }, 100);
    // }

    svgCanvas.textActions.mouseMove(mouseX, mouseY);

    break;
  } case 'rotate': {
    box = utilsGetBBox(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 (eventContext_.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:
    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}
*/
export const mouseOutEvent = function () {
  const svgCanvas = eventContext_.getCanvas();
  const { $id } = svgCanvas;
  if(eventContext_.getCurrentMode() !== 'select' && eventContext_.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}
*/
export const mouseUpEvent = function (evt) {
  const selectedElements = eventContext_.getSelectedElements();
  const currentZoom = eventContext_.getCurrentZoom();
  const svgCanvas = eventContext_.getCanvas();
  if (evt.button === 2) { return; }
  const tempJustSelected = eventContext_.getJustSelected();
  eventContext_.setJustSelected(null);
  if (!eventContext_.getStarted()) { return; }
  const pt = transformPoint(evt.clientX, evt.clientY, eventContext_.getrootSctm());
  const mouseX = pt.x * currentZoom;
  const mouseY = pt.y * currentZoom;
  const x = mouseX / currentZoom;
  const y = mouseY / currentZoom;

  let element = getElem(eventContext_.getId());
  let keep = false;

  const realX = x;
  const realY = y;

  // TODO: Make true when in multi-unit mode
  const useUnit = false; // (eventContext_.getCurConfig().baseUnit !== 'px');
  eventContext_.setStarted(false);
  let t;
  switch (eventContext_.getCurrentMode()) {
  // intentionally fall-through to select here
  case 'resize':
  case 'multiselect':
    if (!isNullish(eventContext_.getRubberBox())) {
      eventContext_.getRubberBox().setAttribute('display', 'none');
      eventContext_.setCurBBoxes([]);
    }
    eventContext_.setCurrentMode('select');
    // Fallthrough
  case 'select':
    if (!isNullish(selectedElements[0])) {
      // if we only have one selected element
      if (isNullish(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;
        default:
          eventContext_.setCurProperties('fill', selected.getAttribute('fill'));
          eventContext_.setCurProperties('fill_opacity', selected.getAttribute('fill-opacity'));
          eventContext_.setCurProperties('stroke', selected.getAttribute('stroke'));
          eventContext_.setCurProperties('stroke_opacity', selected.getAttribute('stroke-opacity'));
          eventContext_.setCurProperties('stroke_width', selected.getAttribute('stroke-width'));
          eventContext_.setCurProperties('stroke_dasharray', selected.getAttribute('stroke-dasharray'));
          eventContext_.setCurProperties('stroke_linejoin', selected.getAttribute('stroke-linejoin'));
          eventContext_.setCurProperties('stroke_linecap', selected.getAttribute('stroke-linecap'));
        }

        if (selected.tagName === 'text') {
          eventContext_.setCurText('font_size', selected.getAttribute('font-size'));
          eventContext_.setCurText('font_family', selected.getAttribute('font-family'));
        }
        svgCanvas.selectorManager.requestSelector(selected).showGrips(true);

        // This shouldn't be necessary as it was done on mouseDown...
        //  svgCanvas.call('selected', [selected]);
      }
      // always recalculate dimensions to strip off stray identity transforms
      svgCanvas.recalculateAllSelectedDimensions();
      // if it was being dragged/resized
      if (realX !== eventContext_.getRStartX() || realY !== eventContext_.getRStartY()) {
        const len = selectedElements.length;
        for (let i = 0; i < len; ++i) {
          if (isNullish(selectedElements[i])) { break; }
          if (!selectedElements[i].firstChild) {
            // Not needed for groups (incorrectly resizes elems), possibly not needed at all?
            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' && isNullish(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
      if (supportsNonScalingStroke()) {
        const elem = selectedElements[0];
        if (elem) {
          elem.removeAttribute('style');
          walkTree(elem, function (el) {
            el.removeAttribute('style');
          });
        }
      }
    }
    return;
  case 'zoom': {
    if (!isNullish(eventContext_.getRubberBox())) {
      eventContext_.getRubberBox().setAttribute('display', 'none');
    }
    const factor = evt.shiftKey ? 0.5 : 2;
    svgCanvas.call('zoomed', {
      x: Math.min(eventContext_.getRStartX(), realX),
      y: Math.min(eventContext_.getRStartY(), realY),
      width: Math.abs(realX - eventContext_.getRStartX()),
      height: Math.abs(realY - eventContext_.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
    eventContext_.setSumDistance(0);
    eventContext_.setControllPoint2('x', 0);
    eventContext_.setControllPoint2('y', 0);
    eventContext_.setControllPoint1('x', 0);
    eventContext_.setControllPoint1('y', 0);
    eventContext_.setStart({ x: 0, y: 0 });
    eventContext_.setEnd('x', 0);
    eventContext_.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) || eventContext_.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 ((eventContext_.getFreehand('maxx') - eventContext_.getFreehand('minx')) > 0 &&
        (eventContext_.getFreehand('maxy') - eventContext_.getFreehand('miny')) > 0) {
      element = svgCanvas.addSVGElementFromJson({
        element: 'ellipse',
        curStyles: true,
        attr: {
          cx: (eventContext_.getFreehand('minx') + eventContext_.getFreehand('maxx')) / 2,
          cy: (eventContext_.getFreehand('miny') + eventContext_.getFreehand('maxy')) / 2,
          rx: (eventContext_.getFreehand('maxx') - eventContext_.getFreehand('minx')) / 2,
          ry: (eventContext_.getFreehand('maxy') - eventContext_.getFreehand('miny')) / 2,
          id: eventContext_.getId()
        }
      });
      svgCanvas.call('changed', [ element ]);
      keep = true;
    }
    break;
  case 'fhrect':
    if ((eventContext_.getFreehand('maxx') - eventContext_.getFreehand('minx')) > 0 &&
        (eventContext_.getFreehand('maxy') - eventContext_.getFreehand('miny')) > 0) {
      element = svgCanvas.addSVGElementFromJson({
        element: 'rect',
        curStyles: true,
        attr: {
          x: eventContext_.getFreehand('minx'),
          y: eventContext_.getFreehand('miny'),
          width: (eventContext_.getFreehand('maxx') - eventContext_.getFreehand('minx')),
          height: (eventContext_.getFreehand('maxy') - eventContext_.getFreehand('miny')),
          id: eventContext_.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
    eventContext_.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;
    eventContext_.setCurrentMode('select');
    const batchCmd = svgCanvas.undoMgr.finishUndoableChange();
    if (!batchCmd.isEmpty()) {
      eventContext_.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(function(r){
    if (r) {
      keep = r.keep || keep;
      ({ element } = r);
      eventContext_.setStarted(r.started || eventContext_.getStarted());
    }
  });

  if (!keep && !isNullish(element)) {
    svgCanvas.getCurrentDrawing().releaseId(eventContext_.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 && t.parentNode && t.parentNode.parentNode && 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 ((eventContext_.getCurrentMode() !== 'path' || !eventContext_.getDrawnPath()) &&
      t && t.parentNode &&
      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 (!isNullish(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 = eventContext_.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(function () {
      if (cAni) { cAni.remove(); }
      element.setAttribute('opacity', curShape.opacity);
      element.setAttribute('style', 'pointer-events:inherit');
      cleanupElement(element);
      if (eventContext_.getCurrentMode() === 'path') {
        svgCanvas.pathActions.toEditMode(element);
      } else if (eventContext_.getCurConfig().selectNew) {
        const modes = [ 'circle', 'ellipse', 'square', 'rect', 'fhpath', 'line', 'fhellipse', 'fhrect', 'star', 'polygon' ];
        if ( modes.indexOf(eventContext_.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()
      eventContext_.addCommandToHistory(new InsertElementCommand(element));
      svgCanvas.call('changed', [ element ]);
    }, aniDur * 1000);
  }
  eventContext_.setStartTransform(null);
};

export const dblClickEvent = function (evt) {
  const selectedElements = eventContext_.getSelectedElements();
  const evtTarget = evt.target;
  const parent = evtTarget.parentNode;
  const svgCanvas = eventContext_.getCanvas();

  let mouseTarget = svgCanvas.getMouseTarget(evt);
  const { tagName } = mouseTarget;

  if (tagName === 'text' && eventContext_.getCurrentMode() !== 'textedit') {
    const pt = transformPoint(evt.clientX, evt.clientY, eventContext_.getrootSctm());
    svgCanvas.textActions.select(mouseTarget, pt.x, pt.y);
  }

  // Do nothing if already in current group
  if (parent === eventContext_.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 (eventContext_.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}
 */
export const mouseDownEvent = function (evt) {
  const dataStorage = eventContext_.getDataStorage();
  const selectedElements = eventContext_.getSelectedElements;
  const currentZoom = eventContext_.getCurrentZoom();
  const svgCanvas = eventContext_.getCanvas();
  const curShape = svgCanvas.getStyle();
  const svgRoot = eventContext_.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);
  }

  eventContext_.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse());

  const pt = transformPoint(evt.clientX, evt.clientY, eventContext_.getrootSctm());
  const mouseX = pt.x * currentZoom;
  const mouseY = pt.y * currentZoom;

  evt.preventDefault();

  if (rightClick) {
    if(eventContext_.getCurrentMode() === 'path'){
      return;
    }
    eventContext_.setCurrentMode('select');
    eventContext_.setLastClickPoint(pt);
  }

  let x = mouseX / currentZoom;
  let y = mouseY / currentZoom;
  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;
  eventContext_.setStartX(x);
  eventContext_.setRStartX(x);
  const realY = y;
  eventContext_.setStartY(y);
  eventContext_.setRStartY(y);

  if (eventContext_.getCurConfig().gridSnapping) {
    x = snapToGrid(x);
    y = snapToGrid(y);
    eventContext_.setStartX(snapToGrid(eventContext_.getStartX()));
    eventContext_.setStartY(snapToGrid(eventContext_.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 && !isNullish(selectedElements()[0])) {
    const grip = evt.target;
    const griptype = dataStorage.get(grip, 'type');
    // rotating
    if (griptype === 'rotate') {
      eventContext_.setCurrentMode('rotate');
      // eventContext_.setCurrentRotateMode(dataStorage.get(grip, 'dir'));
      // resizing
    } else if (griptype === 'resize') {
      eventContext_.setCurrentMode('resize');
      eventContext_.setCurrentResizeMode(dataStorage.get(grip, 'dir'));
    }
    mouseTarget = selectedElements()[0];
  }

  eventContext_.setStartTransform(mouseTarget.getAttribute('transform'));

  const tlist = mouseTarget.transform.baseVal;
  switch (eventContext_.getCurrentMode()) {
  case 'select':
    eventContext_.setStarted(true);
    eventContext_.setCurrentResizeMode('none');
    if (rightClick) { eventContext_.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 ]);
        eventContext_.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 (isNullish(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();
      eventContext_.setCurrentMode('multiselect');
      if (isNullish(eventContext_.getRubberBox())) {
        eventContext_.setRubberBox(svgCanvas.selectorManager.getRubberBandBox());
      }
      eventContext_.setRStartX(eventContext_.getRStartX() * currentZoom);
      eventContext_.setRStartY(eventContext_.getRStartY() * currentZoom);

      assignAttributes(eventContext_.getRubberBox(), {
        x: eventContext_.getRStartX(),
        y: eventContext_.getRStartY(),
        width: 0,
        height: 0,
        display: 'inline'
      }, 100);
    }
    break;
  case 'zoom':
    eventContext_.setStarted(true);
    if (isNullish(eventContext_.getRubberBox())) {
      eventContext_.setRubberBox(svgCanvas.selectorManager.getRubberBandBox());
    }
    assignAttributes(eventContext_.getRubberBox(), {
      x: realX * currentZoom,
      y: realX * currentZoom,
      width: 0,
      height: 0,
      display: 'inline'
    }, 100);
    break;
  case 'resize': {
    eventContext_.setStarted(true);
    eventContext_.setStartX(x);
    eventContext_.setStartY(y);

    // Getting the BBox from the selection box, since we know we
    // want to orient around it
    eventContext_.setInitBbox(utilsGetBBox($id('selectedBox0')));
    const bb = {};
    for (const [ key, val ] of Object.entries(eventContext_.getInitBbox())) {
      bb[key] = val / currentZoom;
    }
    eventContext_.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':
    eventContext_.setStart({ x: realX, y: realY });
    eventContext_.setControllPoint1('x', 0);
    eventContext_.setControllPoint1('y', 0);
    eventContext_.setControllPoint2('x', 0);
    eventContext_.setControllPoint2('y', 0);
    eventContext_.setStarted(true);
    eventContext_.setDAttr(realX + ',' + realY + ' ');
    // Commented out as doing nothing now:
    // strokeW = parseFloat(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;
    svgCanvas.addSVGElementFromJson({
      element: 'polyline',
      curStyles: true,
      attr: {
        points: eventContext_.getDAttr(),
        id: svgCanvas.getNextId(),
        fill: 'none',
        opacity: curShape.opacity / 2,
        'stroke-linecap': 'round',
        style: 'pointer-events:none'
      }
    });
    eventContext_.setFreehand('minx', realX);
    eventContext_.setFreehand('maxx', realX);
    eventContext_.setFreehand('miny', realY);
    eventContext_.setFreehand('maxy', realY);
    break;
  case 'image': {
    eventContext_.setStarted(true);
    const newImage = svgCanvas.addSVGElementFromJson({
      element: 'image',
      attr: {
        x,
        y,
        width: 0,
        height: 0,
        id: svgCanvas.getNextId(),
        opacity: curShape.opacity / 2,
        style: 'pointer-events:inherit'
      }
    });
    setHref(newImage, eventContext_.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':
    eventContext_.setStarted(true);
    eventContext_.setStartX(x);
    eventContext_.setStartY(y);
    svgCanvas.addSVGElementFromJson({
      element: 'rect',
      curStyles: true,
      attr: {
        x,
        y,
        width: 0,
        height: 0,
        id: svgCanvas.getNextId(),
        opacity: curShape.opacity / 2
      }
    });
    break;
  case 'line': {
    eventContext_.setStarted(true);
    const strokeW = Number(curShape.stroke_width) === 0 ? 1 : curShape.stroke_width;
    svgCanvas.addSVGElementFromJson({
      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':
    eventContext_.setStarted(true);
    svgCanvas.addSVGElementFromJson({
      element: 'circle',
      curStyles: true,
      attr: {
        cx: x,
        cy: y,
        r: 0,
        id: svgCanvas.getNextId(),
        opacity: curShape.opacity / 2
      }
    });
    break;
  case 'ellipse':
    eventContext_.setStarted(true);
    svgCanvas.addSVGElementFromJson({
      element: 'ellipse',
      curStyles: true,
      attr: {
        cx: x,
        cy: y,
        rx: 0,
        ry: 0,
        id: svgCanvas.getNextId(),
        opacity: curShape.opacity / 2
      }
    });
    break;
  case 'text':
    eventContext_.setStarted(true);
    /* const newText = */ svgCanvas.addSVGElementFromJson({
      element: 'text',
      curStyles: true,
      attr: {
        x,
        y,
        id: svgCanvas.getNextId(),
        fill: eventContext_.getCurText('fill'),
        'stroke-width': eventContext_.getCurText('stroke_width'),
        'font-size': eventContext_.getCurText('font_size'),
        'font-family': eventContext_.getCurText('font_family'),
        'text-anchor': 'middle',
        'xml:space': 'preserve',
        opacity: curShape.opacity
      }
    });
    // newText.textContent = 'text';
    break;
  case 'path':
    // Fall through
  case 'pathedit':
    eventContext_.setStartX(eventContext_.getStartX() * currentZoom);
    eventContext_.setStartY(eventContext_.getStartY() * currentZoom);
    svgCanvas.pathActions.mouseDown(evt, mouseTarget, eventContext_.getStartX(), eventContext_.getStartY());
    eventContext_.setStarted(true);
    break;
  case 'textedit':
    eventContext_.setStartX(eventContext_.getStartX() * currentZoom);
    eventContext_.setStartY(eventContext_.getStartY() * currentZoom);
    svgCanvas.textActions.mouseDown(evt, mouseTarget, eventContext_.getStartX(), eventContext_.getStartY());
    eventContext_.setStarted(true);
    break;
  case 'rotate':
    eventContext_.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: eventContext_.getStartX(),
    start_y: eventContext_.getStartY(),
    selectedElements: selectedElements()
  }, true);

  extResult.forEach(function(r){
    if (r && r.started) {
      eventContext_.setStarted(true);
    }
  });
};
/**
 * @param {Event} e
 * @fires module:event.SvgCanvas#event:updateCanvas
 * @fires module:event.SvgCanvas#event:zoomDone
 * @returns {void}
 */
export const DOMMouseScrollEvent = function (e) {
  const currentZoom = eventContext_.getCurrentZoom();
  const svgCanvas = eventContext_.getCanvas();
  const { $id } = svgCanvas;
  if (e.ctrlKey) { e.preventDefault(); return false; }
  if (!e.shiftKey) { return; }

  e.preventDefault();
  const evt = e;

  eventContext_.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse());

  const workarea = document.getElementById('workarea');
  const scrbar = 15;
  const rulerwidth = eventContext_.getCurConfig().showRulers ? 16 : 0;

  // mouse relative to content area in content pixels
  const pt = transformPoint(evt.clientX, evt.clientY, eventContext_.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 * eventContext_.getrootSctm().a;
  const workareaViewH = editorH * eventContext_.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 = (evt.wheelDelta) ? evt.wheelDelta : (evt.detail) ? -evt.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 === currentZoom) {
    return;
  }
  factor = zoomlevel / currentZoom;

  // top left of workarea in content pixels before zoom
  const topLeftOld = transformPoint(wOffsetLeft, wOffsetTop, eventContext_.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');
};