/**
* Path functionality.
* @module path
* @license MIT
*
* @copyright 2011 Alexis Deveria, 2011 Jeff Schiller
*/
import { NS } from './namespaces.js';
import { shortFloat } from '../common/units.js';
import { ChangeElementCommand, BatchCommand } from './history.js';
import {
transformPoint, snapToAngle, rectsIntersect,
transformListToTransform
} from './math.js';
import {
assignAttributes, getElem, getRotationAngle, snapToGrid, isNullish,
getBBox as utilsGetBBox
} from './utilities.js';
let pathActionsContext_ = null;
let editorContext_ = null;
let path = null;
/**
* @function module:path-actions.init
* @param {module:path-actions.pathActionsContext_} pathActionsContext
* @returns {void}
*/
export const init = function (pathActionsContext) {
pathActionsContext_ = pathActionsContext;
// editorContext_ = pathActionsContext_.getEditorContext();
// path = pathActionsContext_.getPathObj();
};
/**
* Convert a path to one with only absolute or relative values.
* @todo move to pathActions.js
* @function module:path.convertPath
* @param {SVGPathElement} pth - the path to convert
* @param {boolean} toRel - true of convert to relative
* @returns {string}
*/
export const convertPath = function (pth, toRel) {
const { pathSegList } = pth;
const len = pathSegList.numberOfItems;
let curx = 0; let cury = 0;
let d = '';
let lastM = null;
for (let i = 0; i < len; ++i) {
const seg = pathSegList.getItem(i);
// if these properties are not in the segment, set them to zero
let x = seg.x || 0;
let y = seg.y || 0;
let x1 = seg.x1 || 0;
let y1 = seg.y1 || 0;
let x2 = seg.x2 || 0;
let y2 = seg.y2 || 0;
// const type = seg.pathSegType;
// const pathMap = pathActionsContext_.getPathMap();
// let letter = pathMap[type][toRel ? 'toLowerCase' : 'toUpperCase']();
let letter = seg.pathSegTypeAsLetter;
switch (letter) {
case 'z': // z,Z closepath (Z/z)
case 'Z':
d += 'z';
if (lastM && !toRel) {
curx = lastM[0];
cury = lastM[1];
}
break;
case 'H': // absolute horizontal line (H)
x -= curx;
// Fallthrough
case 'h': // relative horizontal line (h)
if (toRel) {
y = 0;
curx += x;
letter = 'l';
} else {
y = cury;
x += curx;
curx = x;
letter = 'L';
}
// Convert to "line" for easier editing
d += pathDSegment(letter, [ [ x, y ] ]);
break;
case 'V': // absolute vertical line (V)
y -= cury;
// Fallthrough
case 'v': // relative vertical line (v)
if (toRel) {
x = 0;
cury += y;
letter = 'l';
} else {
x = curx;
y += cury;
cury = y;
letter = 'L';
}
// Convert to "line" for easier editing
d += pathDSegment(letter, [ [ x, y ] ]);
break;
case 'M': // absolute move (M)
case 'L': // absolute line (L)
case 'T': // absolute smooth quad (T)
x -= curx;
y -= cury;
// Fallthrough
case 'l': // relative line (l)
case 'm': // relative move (m)
case 't': // relative smooth quad (t)
if (toRel) {
curx += x;
cury += y;
letter = letter.toLowerCase();
} else {
x += curx;
y += cury;
curx = x;
cury = y;
letter = letter.toUpperCase();
}
if (letter === 'm' || letter === 'M') { lastM = [ curx, cury ]; }
d += pathDSegment(letter, [ [ x, y ] ]);
break;
case 'C': // absolute cubic (C)
x -= curx; x1 -= curx; x2 -= curx;
y -= cury; y1 -= cury; y2 -= cury;
// Fallthrough
case 'c': // relative cubic (c)
if (toRel) {
curx += x;
cury += y;
letter = 'c';
} else {
x += curx; x1 += curx; x2 += curx;
y += cury; y1 += cury; y2 += cury;
curx = x;
cury = y;
letter = 'C';
}
d += pathDSegment(letter, [ [ x1, y1 ], [ x2, y2 ], [ x, y ] ]);
break;
case 'Q': // absolute quad (Q)
x -= curx; x1 -= curx;
y -= cury; y1 -= cury;
// Fallthrough
case 'q': // relative quad (q)
if (toRel) {
curx += x;
cury += y;
letter = 'q';
} else {
x += curx; x1 += curx;
y += cury; y1 += cury;
curx = x;
cury = y;
letter = 'Q';
}
d += pathDSegment(letter, [ [ x1, y1 ], [ x, y ] ]);
break;
case 'A':
x -= curx;
y -= cury;
// fallthrough
case 'a': // relative elliptical arc (a)
if (toRel) {
curx += x;
cury += y;
letter = 'a';
} else {
x += curx;
y += cury;
curx = x;
cury = y;
letter = 'A';
}
d += pathDSegment(letter, [ [ seg.r1, seg.r2 ] ], [
seg.angle,
(seg.largeArcFlag ? 1 : 0),
(seg.sweepFlag ? 1 : 0)
], [ x, y ]);
break;
case 'S': // absolute smooth cubic (S)
x -= curx; x2 -= curx;
y -= cury; y2 -= cury;
// Fallthrough
case 's': // relative smooth cubic (s)
if (toRel) {
curx += x;
cury += y;
letter = 's';
} else {
x += curx; x2 += curx;
y += cury; y2 += cury;
curx = x;
cury = y;
letter = 'S';
}
d += pathDSegment(letter, [ [ x2, y2 ], [ x, y ] ]);
break;
} // switch on path segment type
} // for each segment
return d;
};
/**
* TODO: refactor callers in `convertPath` to use `getPathDFromSegments` instead of this function.
* Legacy code refactored from `svgcanvas.pathActions.convertPath`.
* @param {string} letter - path segment command (letter in potentially either case from {@link module:path.pathMap}; see [SVGPathSeg#pathSegTypeAsLetter]{@link https://www.w3.org/TR/SVG/single-page.html#paths-__svg__SVGPathSeg__pathSegTypeAsLetter})
* @param {GenericArray<GenericArray<Integer>>} points - x,y points
* @param {GenericArray<GenericArray<Integer>>} [morePoints] - x,y points
* @param {Integer[]} [lastPoint] - x,y point
* @returns {string}
*/
function pathDSegment (letter, points, morePoints, lastPoint) {
points.forEach(function(pnt, i){
points[i] = shortFloat(pnt);
});
let segment = letter + points.join(' ');
if (morePoints) {
segment += ' ' + morePoints.join(' ');
}
if (lastPoint) {
segment += ' ' + shortFloat(lastPoint);
}
return segment;
}
/**
* Group: Path edit functions.
* Functions relating to editing path elements.
* @namespace {PlainObject} pathActions
* @memberof module:path
*/
export const pathActionsMethod = (function () {
let subpath = false;
let newPoint; let firstCtrl;
let currentPath = null;
let hasMoved = false;
// No `editorContext_` yet but should be ok as is `null` by default
// editorContext_.setDrawnPath(null);
/**
* This function converts a polyline (created by the fh_path tool) into
* a path element and coverts every three line segments into a single bezier
* curve in an attempt to smooth out the free-hand.
* @function smoothPolylineIntoPath
* @param {Element} element
* @returns {Element}
*/
const smoothPolylineIntoPath = function (element) {
let i;
const { points } = element;
const N = points.numberOfItems;
if (N >= 4) {
// loop through every 3 points and convert to a cubic bezier curve segment
//
// NOTE: this is cheating, it means that every 3 points has the potential to
// be a corner instead of treating each point in an equal manner. In general,
// this technique does not look that good.
//
// I am open to better ideas!
//
// Reading:
// - http://www.efg2.com/Lab/Graphics/Jean-YvesQueinecBezierCurves.htm
// - https://www.codeproject.com/KB/graphics/BezierSpline.aspx?msg=2956963
// - https://www.ian-ko.com/ET_GeoWizards/UserGuide/smooth.htm
// - https://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html
let curpos = points.getItem(0); let prevCtlPt = null;
let d = [];
d.push([ 'M', curpos.x, ',', curpos.y, ' C' ].join(''));
for (i = 1; i <= (N - 4); i += 3) {
let ct1 = points.getItem(i);
const ct2 = points.getItem(i + 1);
const end = points.getItem(i + 2);
// if the previous segment had a control point, we want to smooth out
// the control points on both sides
if (prevCtlPt) {
const newpts = pathActionsContext_.smoothControlPoints(prevCtlPt, ct1, curpos);
if (newpts && newpts.length === 2) {
const prevArr = d[d.length - 1].split(',');
prevArr[2] = newpts[0].x;
prevArr[3] = newpts[0].y;
d[d.length - 1] = prevArr.join(',');
ct1 = newpts[1];
}
}
d.push([ ct1.x, ct1.y, ct2.x, ct2.y, end.x, end.y ].join(','));
curpos = end;
prevCtlPt = ct2;
}
// handle remaining line segments
d.push('L');
while (i < N) {
const pt = points.getItem(i);
d.push([ pt.x, pt.y ].join(','));
i++;
}
d = d.join(' ');
// create new path element
editorContext_ = pathActionsContext_.getEditorContext();
element = editorContext_.addSVGElementFromJson({
element: 'path',
curStyles: true,
attr: {
id: editorContext_.getId(),
d,
fill: 'none'
}
});
// No need to call "changed", as this is already done under mouseUp
}
return element;
};
return (/** @lends module:path.pathActions */ {
/**
* @param {MouseEvent} evt
* @param {Element} mouseTarget
* @param {Float} startX
* @param {Float} startY
* @returns {boolean|void}
*/
mouseDown (evt, mouseTarget, startX, startY) {
let id;
editorContext_ = pathActionsContext_.getEditorContext();
if (editorContext_.getCurrentMode() === 'path') {
let mouseX = startX; // Was this meant to work with the other `mouseX`? (was defined globally so adding `let` to at least avoid a global)
let mouseY = startY; // Was this meant to work with the other `mouseY`? (was defined globally so adding `let` to at least avoid a global)
const currentZoom = editorContext_.getCurrentZoom();
let x = mouseX / currentZoom;
let y = mouseY / currentZoom;
let stretchy = getElem('path_stretch_line');
newPoint = [ x, y ];
if (editorContext_.getGridSnapping()) {
x = snapToGrid(x);
y = snapToGrid(y);
mouseX = snapToGrid(mouseX);
mouseY = snapToGrid(mouseY);
}
if (!stretchy) {
stretchy = document.createElementNS(NS.SVG, 'path');
assignAttributes(stretchy, {
id: 'path_stretch_line',
stroke: '#22C',
'stroke-width': '0.5',
fill: 'none'
});
getElem('selectorParentGroup').append(stretchy);
}
stretchy.setAttribute('display', 'inline');
let keep = null;
let index;
// if pts array is empty, create path element with M at current point
const drawnPath = editorContext_.getDrawnPath();
if (!drawnPath) {
const dAttr = 'M' + x + ',' + y + ' '; // Was this meant to work with the other `dAttr`? (was defined globally so adding `var` to at least avoid a global)
/* drawnPath = */ editorContext_.setDrawnPath(editorContext_.addSVGElementFromJson({
element: 'path',
curStyles: true,
attr: {
d: dAttr,
id: editorContext_.getNextId(),
opacity: editorContext_.getOpacity() / 2
}
}));
// set stretchy line to first point
stretchy.setAttribute('d', [ 'M', mouseX, mouseY, mouseX, mouseY ].join(' '));
index = subpath ? path.segs.length : 0;
pathActionsContext_.addPointGrip(index, mouseX, mouseY);
} else {
// determine if we clicked on an existing point
const seglist = drawnPath.pathSegList;
let i = seglist.numberOfItems;
const FUZZ = 6 / currentZoom;
let clickOnPoint = false;
while (i) {
i--;
const item = seglist.getItem(i);
const px = item.x; const py = item.y;
// found a matching point
if (x >= (px - FUZZ) && x <= (px + FUZZ) &&
y >= (py - FUZZ) && y <= (py + FUZZ)
) {
clickOnPoint = true;
break;
}
}
// get path element that we are in the process of creating
id = editorContext_.getId();
// Remove previous path object if previously created
pathActionsContext_.removePath_(id);
const newpath = getElem(id);
let newseg;
let sSeg;
const len = seglist.numberOfItems;
// if we clicked on an existing point, then we are done this path, commit it
// (i, i+1) are the x,y that were clicked on
if (clickOnPoint) {
// if clicked on any other point but the first OR
// the first point was clicked on and there are less than 3 points
// then leave the path open
// otherwise, close the path
if (i <= 1 && len >= 2) {
// Create end segment
const absX = seglist.getItem(0).x;
const absY = seglist.getItem(0).y;
sSeg = stretchy.pathSegList.getItem(1);
newseg = sSeg.pathSegType === 4
? drawnPath.createSVGPathSegLinetoAbs(absX, absY)
: drawnPath.createSVGPathSegCurvetoCubicAbs(absX, absY, sSeg.x1 / currentZoom, sSeg.y1 / currentZoom, absX, absY);
const endseg = drawnPath.createSVGPathSegClosePath();
seglist.appendItem(newseg);
seglist.appendItem(endseg);
} else if (len < 3) {
keep = false;
return keep;
}
stretchy.remove();
// This will signal to commit the path
// const element = newpath; // Other event handlers define own `element`, so this was probably not meant to interact with them or one which shares state (as there were none); I therefore adding a missing `var` to avoid a global
/* drawnPath = */ editorContext_.setDrawnPath(null);
editorContext_.setStarted(false);
if (subpath) {
if (path.matrix) {
editorContext_.remapElement(newpath, {}, path.matrix.inverse());
}
const newD = newpath.getAttribute('d');
const origD = path.elem.getAttribute('d');
path.elem.setAttribute('d', origD + newD);
newpath.parentNode.removeChild(newpath);
if (path.matrix) {
pathActionsContext_.recalcRotatedPath();
}
pathActionsMethod.toEditMode(path.elem);
path.selectPt();
return false;
}
// else, create a new point, update path element
} else {
// Checks if current target or parents are #svgcontent
if (!(editorContext_.getContainer() !== editorContext_.getMouseTarget(evt) && editorContext_.getContainer().contains(
editorContext_.getMouseTarget(evt)
))) {
// Clicked outside canvas, so don't make point
return false;
}
const num = drawnPath.pathSegList.numberOfItems;
const last = drawnPath.pathSegList.getItem(num - 1);
const lastx = last.x; const lasty = last.y;
if (evt.shiftKey) {
const xya = snapToAngle(lastx, lasty, x, y);
({ x, y } = xya);
}
// Use the segment defined by stretchy
sSeg = stretchy.pathSegList.getItem(1);
newseg = sSeg.pathSegType === 4
? drawnPath.createSVGPathSegLinetoAbs(editorContext_.round(x), editorContext_.round(y))
: drawnPath.createSVGPathSegCurvetoCubicAbs(
editorContext_.round(x),
editorContext_.round(y),
sSeg.x1 / currentZoom,
sSeg.y1 / currentZoom,
sSeg.x2 / currentZoom,
sSeg.y2 / currentZoom
);
drawnPath.pathSegList.appendItem(newseg);
x *= currentZoom;
y *= currentZoom;
// set stretchy line to latest point
stretchy.setAttribute('d', [ 'M', x, y, x, y ].join(' '));
index = num;
if (subpath) { index += path.segs.length; }
pathActionsContext_.addPointGrip(index, x, y);
}
// keep = true;
}
return undefined;
}
// TODO: Make sure currentPath isn't null at this point
if (!path) { return undefined; }
path.storeD();
({ id } = evt.target);
let curPt;
if (id.substr(0, 14) === 'pathpointgrip_') {
// Select this point
curPt = path.cur_pt = Number.parseInt(id.substr(14));
path.dragging = [ startX, startY ];
const seg = path.segs[curPt];
// only clear selection if shift is not pressed (otherwise, add
// node to selection)
if (!evt.shiftKey) {
if (path.selected_pts.length <= 1 || !seg.selected) {
path.clearSelection();
}
path.addPtsToSelection(curPt);
} else if (seg.selected) {
path.removePtFromSelection(curPt);
} else {
path.addPtsToSelection(curPt);
}
} else if (id.startsWith('ctrlpointgrip_')) {
path.dragging = [ startX, startY ];
const parts = id.split('_')[1].split('c');
curPt = Number(parts[0]);
const ctrlNum = Number(parts[1]);
path.selectPt(curPt, ctrlNum);
}
// Start selection box
if (!path.dragging) {
let rubberBox = editorContext_.getRubberBox();
if (isNullish(rubberBox)) {
rubberBox = editorContext_.setRubberBox(
editorContext_.selectorManager.getRubberBandBox()
);
}
const currentZoom = editorContext_.getCurrentZoom();
assignAttributes(rubberBox, {
x: startX * currentZoom,
y: startY * currentZoom,
width: 0,
height: 0,
display: 'inline'
}, 100);
}
return undefined;
},
/**
* @param {Float} mouseX
* @param {Float} mouseY
* @returns {void}
*/
mouseMove (mouseX, mouseY) {
editorContext_ = pathActionsContext_.getEditorContext();
const currentZoom = editorContext_.getCurrentZoom();
hasMoved = true;
const drawnPath = editorContext_.getDrawnPath();
if (editorContext_.getCurrentMode() === 'path') {
if (!drawnPath) { return; }
const seglist = drawnPath.pathSegList;
const index = seglist.numberOfItems - 1;
if (newPoint) {
// First point
// if (!index) { return; }
// Set control points
const pointGrip1 = pathActionsContext_.addCtrlGrip('1c1');
const pointGrip2 = pathActionsContext_.addCtrlGrip('0c2');
// dragging pointGrip1
pointGrip1.setAttribute('cx', mouseX);
pointGrip1.setAttribute('cy', mouseY);
pointGrip1.setAttribute('display', 'inline');
const ptX = newPoint[0];
const ptY = newPoint[1];
// set curve
// const seg = seglist.getItem(index);
const curX = mouseX / currentZoom;
const curY = mouseY / currentZoom;
const altX = (ptX + (ptX - curX));
const altY = (ptY + (ptY - curY));
pointGrip2.setAttribute('cx', altX * currentZoom);
pointGrip2.setAttribute('cy', altY * currentZoom);
pointGrip2.setAttribute('display', 'inline');
const ctrlLine = pathActionsContext_.getCtrlLine(1);
assignAttributes(ctrlLine, {
x1: mouseX,
y1: mouseY,
x2: altX * currentZoom,
y2: altY * currentZoom,
display: 'inline'
});
if (index === 0) {
firstCtrl = [ mouseX, mouseY ];
} else {
const last = seglist.getItem(index - 1);
let lastX = last.x;
let lastY = last.y;
if (last.pathSegType === 6) {
lastX += (lastX - last.x2);
lastY += (lastY - last.y2);
} else if (firstCtrl) {
lastX = firstCtrl[0] / currentZoom;
lastY = firstCtrl[1] / currentZoom;
}
pathActionsContext_.replacePathSeg(6, index, [ ptX, ptY, lastX, lastY, altX, altY ], drawnPath);
}
} else {
const stretchy = getElem('path_stretch_line');
if (stretchy) {
const prev = seglist.getItem(index);
if (prev.pathSegType === 6) {
const prevX = prev.x + (prev.x - prev.x2);
const prevY = prev.y + (prev.y - prev.y2);
pathActionsContext_.replacePathSeg(
6,
1,
[ mouseX, mouseY, prevX * currentZoom, prevY * currentZoom, mouseX, mouseY ],
stretchy
);
} else if (firstCtrl) {
pathActionsContext_.replacePathSeg(6, 1, [ mouseX, mouseY, firstCtrl[0], firstCtrl[1], mouseX, mouseY ], stretchy);
} else {
pathActionsContext_.replacePathSeg(4, 1, [ mouseX, mouseY ], stretchy);
}
}
}
return;
}
// if we are dragging a point, let's move it
if (path.dragging) {
const pt = pathActionsContext_.getPointFromGrip({
x: path.dragging[0],
y: path.dragging[1]
}, path);
const mpt = pathActionsContext_.getPointFromGrip({
x: mouseX,
y: mouseY
}, path);
const diffX = mpt.x - pt.x;
const diffY = mpt.y - pt.y;
path.dragging = [ mouseX, mouseY ];
if (path.dragctrl) {
path.moveCtrl(diffX, diffY);
} else {
path.movePts(diffX, diffY);
}
} else {
path.selected_pts = [];
path.eachSeg(function (_i) {
const seg = this;
if (!seg.next && !seg.prev) { return; }
// const {item} = seg;
const rubberBox = editorContext_.getRubberBox();
const rbb = rubberBox.getBBox();
const pt = pathActionsContext_.getGripPt(seg);
const ptBb = {
x: pt.x,
y: pt.y,
width: 0,
height: 0
};
const sel = rectsIntersect(rbb, ptBb);
this.select(sel);
// Note that addPtsToSelection is not being run
if (sel) { path.selected_pts.push(seg.index); }
});
}
},
/**
* @typedef module:path.keepElement
* @type {PlainObject}
* @property {boolean} keep
* @property {Element} element
*/
/**
* @param {Event} evt
* @param {Element} element
* @param {Float} _mouseX
* @param {Float} _mouseY
* @returns {module:path.keepElement|void}
*/
mouseUp (evt, element, _mouseX, _mouseY) {
editorContext_ = pathActionsContext_.getEditorContext();
const drawnPath = editorContext_.getDrawnPath();
// Create mode
if (editorContext_.getCurrentMode() === 'path') {
newPoint = null;
if (!drawnPath) {
element = getElem(editorContext_.getId());
editorContext_.setStarted(false);
firstCtrl = null;
}
return {
keep: true,
element
};
}
// Edit mode
const rubberBox = editorContext_.getRubberBox();
if (path.dragging) {
const lastPt = path.cur_pt;
path.dragging = false;
path.dragctrl = false;
path.update();
if (hasMoved) {
path.endChanges('Move path point(s)');
}
if (!evt.shiftKey && !hasMoved) {
path.selectPt(lastPt);
}
} else if (rubberBox && rubberBox.getAttribute('display') !== 'none') {
// Done with multi-node-select
rubberBox.setAttribute('display', 'none');
if (rubberBox.getAttribute('width') <= 2 && rubberBox.getAttribute('height') <= 2) {
pathActionsMethod.toSelectMode(evt.target);
}
// else, move back to select mode
} else {
pathActionsMethod.toSelectMode(evt.target);
}
hasMoved = false;
return undefined;
},
/**
* @param {Element} element
* @returns {void}
*/
toEditMode (element) {
editorContext_ = pathActionsContext_.getEditorContext();
path = pathActionsContext_.getPath_(element);
editorContext_.setCurrentMode('pathedit');
editorContext_.clearSelection();
path.setPathContext();
path.show(true).update();
path.oldbbox = utilsGetBBox(path.elem);
subpath = false;
},
/**
* @param {Element} elem
* @fires module:svgcanvas.SvgCanvas#event:selected
* @returns {void}
*/
toSelectMode (elem) {
editorContext_ = pathActionsContext_.getEditorContext();
const selPath = (elem === path.elem);
editorContext_.setCurrentMode('select');
path.setPathContext();
path.show(false);
currentPath = false;
editorContext_.clearSelection();
if (path.matrix) {
// Rotated, so may need to re-calculate the center
pathActionsContext_.recalcRotatedPath();
}
if (selPath) {
editorContext_.call('selected', [ elem ]);
editorContext_.addToSelection([ elem ], true);
}
},
/**
* @param {boolean} on
* @returns {void}
*/
addSubPath (on) {
editorContext_ = pathActionsContext_.getEditorContext();
if (on) {
// Internally we go into "path" mode, but in the UI it will
// still appear as if in "pathedit" mode.
editorContext_.setCurrentMode('path');
subpath = true;
} else {
pathActionsMethod.clear(true);
pathActionsMethod.toEditMode(path.elem);
}
},
/**
* @param {Element} target
* @returns {void}
*/
select (target) {
editorContext_ = pathActionsContext_.getEditorContext();
if (currentPath === target) {
pathActionsMethod.toEditMode(target);
editorContext_.setCurrentMode('pathedit');
// going into pathedit mode
} else {
currentPath = target;
}
},
/**
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
reorient () {
editorContext_ = pathActionsContext_.getEditorContext();
const elem = editorContext_.getSelectedElements()[0];
if (!elem) { return; }
const angl = getRotationAngle(elem);
if (angl === 0) { return; }
const batchCmd = new BatchCommand('Reorient path');
const changes = {
d: elem.getAttribute('d'),
transform: elem.getAttribute('transform')
};
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));
editorContext_.clearSelection();
this.resetOrientation(elem);
editorContext_.addCommandToHistory(batchCmd);
// Set matrix to null
pathActionsContext_.getPath_(elem).show(false).matrix = null;
this.clear();
editorContext_.addToSelection([ elem ], true);
editorContext_.call('changed', editorContext_.getSelectedElements());
},
/**
* @param {boolean} remove Not in use
* @returns {void}
*/
clear () {
editorContext_ = pathActionsContext_.getEditorContext();
const drawnPath = editorContext_.getDrawnPath();
currentPath = null;
if (drawnPath) {
const elem = getElem(editorContext_.getId());
const psl = getElem('path_stretch_line');
psl.parentNode.removeChild(psl);
elem.parentNode.removeChild(elem);
const pathpointgripContainer = getElem('pathpointgrip_container');
const elements = pathpointgripContainer.querySelectorAll('*');
Array.prototype.forEach.call(elements, function(el){
el.setAttribute('display', 'none');
});
firstCtrl = null;
editorContext_.setDrawnPath(null);
editorContext_.setStarted(false);
} else if (editorContext_.getCurrentMode() === 'pathedit') {
this.toSelectMode();
}
if (path) { path.init().show(false); }
},
/**
* @param {?(Element|SVGPathElement)} pth
* @returns {false|void}
*/
resetOrientation (pth) {
if (isNullish(pth) || pth.nodeName !== 'path') { return false; }
const tlist = pth.transform.baseVal;
const m = transformListToTransform(tlist).matrix;
tlist.clear();
pth.removeAttribute('transform');
const segList = pth.pathSegList;
// Opera/win/non-EN throws an error here.
// TODO: Find out why!
// Presumed fixed in Opera 10.5, so commented out for now
// try {
const len = segList.numberOfItems;
// } catch(err) {
// const fixed_d = pathActions.convertPath(pth);
// pth.setAttribute('d', fixed_d);
// segList = pth.pathSegList;
// const len = segList.numberOfItems;
// }
// let lastX, lastY;
for (let i = 0; i < len; ++i) {
const seg = segList.getItem(i);
const type = seg.pathSegType;
if (type === 1) { continue; }
const pts = [];
[ '', 1, 2 ].forEach(function(n){
const x = seg['x' + n]; const y = seg['y' + n];
if (x !== undefined && y !== undefined) {
const pt = transformPoint(x, y, m);
pts.splice(pts.length, 0, pt.x, pt.y);
}
});
pathActionsContext_.replacePathSeg(type, i, pts, pth);
}
pathActionsContext_.reorientGrads(pth, m);
return undefined;
},
/**
* @returns {void}
*/
zoomChange () {
editorContext_ = pathActionsContext_.getEditorContext();
if (editorContext_.getCurrentMode() === 'pathedit') {
path.update();
}
},
/**
* @typedef {PlainObject} module:path.NodePoint
* @property {Float} x
* @property {Float} y
* @property {Integer} type
*/
/**
* @returns {module:path.NodePoint}
*/
getNodePoint () {
const selPt = path.selected_pts.length ? path.selected_pts[0] : 1;
const seg = path.segs[selPt];
return {
x: seg.item.x,
y: seg.item.y,
type: seg.type
};
},
/**
* @param {boolean} linkPoints
* @returns {void}
*/
linkControlPoints (linkPoints) {
pathActionsContext_.setLinkControlPoints(linkPoints);
},
/**
* @returns {void}
*/
clonePathNode () {
path.storeD();
const selPts = path.selected_pts;
// const {segs} = path;
let i = selPts.length;
const nums = [];
while (i--) {
const pt = selPts[i];
path.addSeg(pt);
nums.push(pt + i);
nums.push(pt + i + 1);
}
path.init().addPtsToSelection(nums);
path.endChanges('Clone path node(s)');
},
/**
* @returns {void}
*/
opencloseSubPath () {
const selPts = path.selected_pts;
// Only allow one selected node for now
if (selPts.length !== 1) { return; }
const { elem } = path;
const list = elem.pathSegList;
// const len = list.numberOfItems;
const index = selPts[0];
let openPt = null;
let startItem = null;
// Check if subpath is already open
path.eachSeg(function (i) {
if (this.type === 2 && i <= index) {
startItem = this.item;
}
if (i <= index) { return true; }
if (this.type === 2) {
// Found M first, so open
openPt = i;
return false;
}
if (this.type === 1) {
// Found Z first, so closed
openPt = false;
return false;
}
return true;
});
if (isNullish(openPt)) {
// Single path, so close last seg
openPt = path.segs.length - 1;
}
if (openPt !== false) {
// Close this path
// Create a line going to the previous "M"
const newseg = elem.createSVGPathSegLinetoAbs(startItem.x, startItem.y);
const closer = elem.createSVGPathSegClosePath();
if (openPt === path.segs.length - 1) {
list.appendItem(newseg);
list.appendItem(closer);
} else {
list.insertItemBefore(closer, openPt);
list.insertItemBefore(newseg, openPt);
}
path.init().selectPt(openPt + 1);
return;
}
// M 1,1 L 2,2 L 3,3 L 1,1 z // open at 2,2
// M 2,2 L 3,3 L 1,1
// M 1,1 L 2,2 L 1,1 z M 4,4 L 5,5 L6,6 L 5,5 z
// M 1,1 L 2,2 L 1,1 z [M 4,4] L 5,5 L(M)6,6 L 5,5 z
const seg = path.segs[index];
if (seg.mate) {
list.removeItem(index); // Removes last "L"
list.removeItem(index); // Removes the "Z"
path.init().selectPt(index - 1);
return;
}
let lastM; let zSeg;
// Find this sub-path's closing point and remove
for (let i = 0; i < list.numberOfItems; i++) {
const item = list.getItem(i);
if (item.pathSegType === 2) {
// Find the preceding M
lastM = i;
} else if (i === index) {
// Remove it
list.removeItem(lastM);
// index--;
} else if (item.pathSegType === 1 && index < i) {
// Remove the closing seg of this subpath
zSeg = i - 1;
list.removeItem(i);
break;
}
}
let num = (index - lastM) - 1;
while (num--) {
list.insertItemBefore(list.getItem(lastM), zSeg);
}
const pt = list.getItem(lastM);
// Make this point the new "M"
pathActionsContext_.replacePathSeg(2, lastM, [ pt.x, pt.y ]);
// i = index; // i is local here, so has no effect; what was the intent for this?
path.init().selectPt(0);
},
/**
* @returns {void}
*/
deletePathNode () {
if (!pathActionsMethod.canDeleteNodes) { return; }
path.storeD();
const selPts = path.selected_pts;
let i = selPts.length;
while (i--) {
const pt = selPts[i];
path.deleteSeg(pt);
}
// Cleanup
const cleanup = function () {
const segList = path.elem.pathSegList;
let len = segList.numberOfItems;
const remItems = function (pos, count) {
while (count--) {
segList.removeItem(pos);
}
};
if (len <= 1) { return true; }
while (len--) {
const item = segList.getItem(len);
if (item.pathSegType === 1) {
const prev = segList.getItem(len - 1);
const nprev = segList.getItem(len - 2);
if (prev.pathSegType === 2) {
remItems(len - 1, 2);
cleanup();
break;
} else if (nprev.pathSegType === 2) {
remItems(len - 2, 3);
cleanup();
break;
}
} else if (item.pathSegType === 2 && len > 0) {
const prevType = segList.getItem(len - 1).pathSegType;
// Path has M M
if (prevType === 2) {
remItems(len - 1, 1);
cleanup();
break;
// Entire path ends with Z M
} else if (prevType === 1 && segList.numberOfItems - 1 === len) {
remItems(len, 1);
cleanup();
break;
}
}
}
return false;
};
cleanup();
// Completely delete a path with 1 or 0 segments
if (path.elem.pathSegList.numberOfItems <= 1) {
pathActionsMethod.toSelectMode(path.elem);
editorContext_ = pathActionsContext_.getEditorContext();
editorContext_.canvas.deleteSelectedElements();
return;
}
path.init();
path.clearSelection();
// TODO: Find right way to select point now
// path.selectPt(selPt);
if (window.opera) { // Opera repaints incorrectly
path.elem.setAttribute('d', path.elem.getAttribute('d'));
}
path.endChanges('Delete path node(s)');
},
// Can't seem to use `@borrows` here, so using `@see`
/**
* Smooth polyline into path.
* @function module:path.pathActions.smoothPolylineIntoPath
* @see module:path~smoothPolylineIntoPath
*/
smoothPolylineIntoPath,
/* eslint-enable */
/**
* @param {?Integer} v See {@link https://www.w3.org/TR/SVG/single-page.html#paths-InterfaceSVGPathSeg}
* @returns {void}
*/
setSegType (v) {
path?.setSegType(v);
},
/**
* @param {string} attr
* @param {Float} newValue
* @returns {void}
*/
moveNode (attr, newValue) {
const selPts = path.selected_pts;
if (!selPts.length) { return; }
path.storeD();
// Get first selected point
const seg = path.segs[selPts[0]];
const diff = { x: 0, y: 0 };
diff[attr] = newValue - seg.item[attr];
seg.move(diff.x, diff.y);
path.endChanges('Move path point');
},
/**
* @param {Element} elem
* @returns {void}
*/
fixEnd (elem) {
// Adds an extra segment if the last seg before a Z doesn't end
// at its M point
// M0,0 L0,100 L100,100 z
const segList = elem.pathSegList;
const len = segList.numberOfItems;
let lastM;
for (let i = 0; i < len; ++i) {
const item = segList.getItem(i);
if (item.pathSegType === 2) { // 2 => M segment type (move to)
lastM = item;
}
if (item.pathSegType === 1) { // 1 => Z segment type (close path)
const prev = segList.getItem(i - 1);
if (prev.x !== lastM.x || prev.y !== lastM.y) {
// Add an L segment here
const newseg = elem.createSVGPathSegLinetoAbs(lastM.x, lastM.y);
segList.insertItemBefore(newseg, i);
// Can this be done better?
pathActionsMethod.fixEnd(elem);
break;
}
}
}
},
// Can't seem to use `@borrows` here, so using `@see`
/**
* Convert a path to one with only absolute or relative values.
* @function module:path.pathActions.convertPath
* @see module:path.convertPath
*/
convertPath
});
})();
// end pathActions