svgedit/src/svgcanvas/event.js

1388 lines
47 KiB
JavaScript

/**
* 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.shiftKey) { return; }
e.preventDefault();
const evt = e.originalEvent;
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');
};