2018-05-18 03:25:45 +00:00
|
|
|
/* globals jQuery */
|
2011-02-04 08:02:46 +00:00
|
|
|
/**
|
|
|
|
* Package: svgedit.path
|
|
|
|
*
|
2012-09-16 18:53:27 +00:00
|
|
|
* Licensed under the MIT License
|
2011-02-04 08:02:46 +00:00
|
|
|
*
|
|
|
|
* Copyright(c) 2011 Alexis Deveria
|
|
|
|
* Copyright(c) 2011 Jeff Schiller
|
|
|
|
*/
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
import './pathseg.js';
|
|
|
|
import * as pathModule from './path.js';
|
|
|
|
import {NS} from './svgedit.js';
|
|
|
|
import {getTransformList} from './svgtransformlist.js';
|
|
|
|
import {ChangeElementCommand} from './history.js';
|
|
|
|
import {transformPoint, getMatrix} from './math.js';
|
|
|
|
import {
|
|
|
|
assignAttributes, getElem, getRotationAngle, getBBox
|
|
|
|
} from './svgutils.js';
|
|
|
|
import {
|
|
|
|
supportsPathInsertItemBefore, supportsPathReplaceItem, isWebkit
|
|
|
|
} from './browser.js';
|
|
|
|
|
|
|
|
const $ = jQuery;
|
|
|
|
|
|
|
|
const segData = {
|
2018-05-18 04:02:30 +00:00
|
|
|
2: ['x', 'y'],
|
|
|
|
4: ['x', 'y'],
|
|
|
|
6: ['x', 'y', 'x1', 'y1', 'x2', 'y2'],
|
|
|
|
8: ['x', 'y', 'x1', 'y1'],
|
|
|
|
10: ['x', 'y', 'r1', 'r2', 'angle', 'largeArcFlag', 'sweepFlag'],
|
|
|
|
12: ['x'],
|
|
|
|
14: ['y'],
|
|
|
|
16: ['x', 'y', 'x2', 'y2'],
|
|
|
|
18: ['x', 'y']
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const uiStrings = {};
|
|
|
|
export const setUiStrings = function (strs) {
|
|
|
|
Object.assign(uiStrings, strs.ui);
|
|
|
|
};
|
|
|
|
|
|
|
|
let pathFuncs = [];
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let linkControlPts = true;
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2011-02-23 16:24:07 +00:00
|
|
|
// Stores references to paths via IDs.
|
|
|
|
// TODO: Make this cross-document happy.
|
2018-05-18 03:25:45 +00:00
|
|
|
let pathData = {};
|
2011-02-23 16:24:07 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const setLinkControlPoints = function (lcp) {
|
2018-05-18 04:02:30 +00:00
|
|
|
linkControlPts = lcp;
|
2011-02-04 16:06:25 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const path = null;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let editorContext_ = null;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const init = function (editorContext) {
|
2018-05-18 04:02:30 +00:00
|
|
|
editorContext_ = editorContext;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
pathFuncs = [0, 'ClosePath'];
|
2018-05-18 03:25:45 +00:00
|
|
|
const pathFuncsStrs = ['Moveto', 'Lineto', 'CurvetoCubic', 'CurvetoQuadratic', 'Arc',
|
2018-05-18 04:02:30 +00:00
|
|
|
'LinetoHorizontal', 'LinetoVertical', 'CurvetoCubicSmooth', 'CurvetoQuadraticSmooth'];
|
|
|
|
$.each(pathFuncsStrs, function (i, s) {
|
|
|
|
pathFuncs.push(s + 'Abs');
|
|
|
|
pathFuncs.push(s + 'Rel');
|
|
|
|
});
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const insertItemBefore = function (elem, newseg, index) {
|
2018-05-18 04:02:30 +00:00
|
|
|
// Support insertItemBefore on paths for FF2
|
2018-05-18 03:25:45 +00:00
|
|
|
const list = elem.pathSegList;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (supportsPathInsertItemBefore()) {
|
2018-05-18 04:02:30 +00:00
|
|
|
list.insertItemBefore(newseg, index);
|
|
|
|
return;
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
const len = list.numberOfItems;
|
|
|
|
const arr = [];
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
const curSeg = list.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
arr.push(curSeg);
|
|
|
|
}
|
|
|
|
list.clear();
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 0; i < len; i++) {
|
2018-05-18 04:02:30 +00:00
|
|
|
if (i === index) { // index + 1
|
|
|
|
list.appendItem(newseg);
|
|
|
|
}
|
|
|
|
list.appendItem(arr[i]);
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: See if this should just live in replacePathSeg
|
2018-05-18 03:25:45 +00:00
|
|
|
export const ptObjToArr = function (type, segItem) {
|
|
|
|
const arr = segData[type], len = arr.length;
|
|
|
|
const out = [];
|
|
|
|
for (let i = 0; i < len; i++) {
|
2018-05-18 04:02:30 +00:00
|
|
|
out[i] = segItem[arr[i]];
|
|
|
|
}
|
|
|
|
return out;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getGripPt = function (seg, altPt) {
|
|
|
|
const {path} = seg;
|
|
|
|
let out = {
|
|
|
|
x: altPt ? altPt.x : seg.item.x,
|
|
|
|
y: altPt ? altPt.y : seg.item.y
|
|
|
|
};
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
if (path.matrix) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const pt = transformPoint(out.x, out.y, path.matrix);
|
2018-05-18 04:02:30 +00:00
|
|
|
out = pt;
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const currentZoom = editorContext_.getCurrentZoom();
|
|
|
|
out.x *= currentZoom;
|
|
|
|
out.y *= currentZoom;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
return out;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getPointFromGrip = function (pt, path) {
|
|
|
|
const out = {
|
2018-05-18 04:02:30 +00:00
|
|
|
x: pt.x,
|
|
|
|
y: pt.y
|
|
|
|
};
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
if (path.matrix) {
|
2018-05-18 03:25:45 +00:00
|
|
|
pt = transformPoint(out.x, out.y, path.imatrix);
|
2018-05-18 04:02:30 +00:00
|
|
|
out.x = pt.x;
|
|
|
|
out.y = pt.y;
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const currentZoom = editorContext_.getCurrentZoom();
|
|
|
|
out.x /= currentZoom;
|
|
|
|
out.y /= currentZoom;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
return out;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
/**
|
|
|
|
* Requires prior call to `setUiStrings` if `xlink:title`
|
|
|
|
* to be set on the grip
|
|
|
|
*/
|
|
|
|
export const addPointGrip = function (index, x, y) {
|
2018-05-18 04:02:30 +00:00
|
|
|
// create the container of all the point grips
|
2018-05-18 03:25:45 +00:00
|
|
|
const pointGripContainer = getGripContainer();
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let pointGrip = getElem('pathpointgrip_' + index);
|
2018-05-18 04:02:30 +00:00
|
|
|
// create it
|
|
|
|
if (!pointGrip) {
|
|
|
|
pointGrip = document.createElementNS(NS.SVG, 'circle');
|
2018-05-18 03:25:45 +00:00
|
|
|
const atts = {
|
|
|
|
id: 'pathpointgrip_' + index,
|
|
|
|
display: 'none',
|
|
|
|
r: 4,
|
|
|
|
fill: '#0FF',
|
|
|
|
stroke: '#00F',
|
2018-05-18 04:02:30 +00:00
|
|
|
'stroke-width': 2,
|
2018-05-18 03:25:45 +00:00
|
|
|
cursor: 'move',
|
|
|
|
style: 'pointer-events:all'
|
|
|
|
};
|
|
|
|
if ('pathNodeTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
|
|
|
|
atts['xlink:title'] = uiStrings.pathNodeTooltip;
|
|
|
|
}
|
|
|
|
assignAttributes(pointGrip, atts);
|
2018-05-18 04:02:30 +00:00
|
|
|
pointGrip = pointGripContainer.appendChild(pointGrip);
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const grip = $('#pathpointgrip_' + index);
|
2018-05-18 04:02:30 +00:00
|
|
|
grip.dblclick(function () {
|
2018-05-18 03:25:45 +00:00
|
|
|
if (pathModule.path) {
|
|
|
|
pathModule.path.setSegType();
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (x && y) {
|
|
|
|
// set up the point grip element and display it
|
2018-05-18 03:25:45 +00:00
|
|
|
assignAttributes(pointGrip, {
|
|
|
|
cx: x,
|
|
|
|
cy: y,
|
|
|
|
display: 'inline'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return pointGrip;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getGripContainer = function () {
|
|
|
|
let c = getElem('pathpointgrip_container');
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!c) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const parent = getElem('selectorParentGroup');
|
2018-05-18 04:02:30 +00:00
|
|
|
c = parent.appendChild(document.createElementNS(NS.SVG, 'g'));
|
|
|
|
c.id = 'pathpointgrip_container';
|
|
|
|
}
|
|
|
|
return c;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
/**
|
|
|
|
* Requires prior call to `setUiStrings` if `xlink:title`
|
|
|
|
* to be set on the grip
|
|
|
|
*/
|
|
|
|
export const addCtrlGrip = function (id) {
|
|
|
|
let pointGrip = getElem('ctrlpointgrip_' + id);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (pointGrip) { return pointGrip; }
|
|
|
|
|
|
|
|
pointGrip = document.createElementNS(NS.SVG, 'circle');
|
2018-05-18 03:25:45 +00:00
|
|
|
const atts = {
|
|
|
|
id: 'ctrlpointgrip_' + id,
|
|
|
|
display: 'none',
|
|
|
|
r: 4,
|
|
|
|
fill: '#0FF',
|
|
|
|
stroke: '#55F',
|
2018-05-18 04:02:30 +00:00
|
|
|
'stroke-width': 1,
|
2018-05-18 03:25:45 +00:00
|
|
|
cursor: 'move',
|
|
|
|
style: 'pointer-events:all'
|
|
|
|
};
|
|
|
|
if ('pathCtrlPtTooltip' in uiStrings) { // May be empty if running path.js without svg-editor
|
|
|
|
atts['xlink:title'] = uiStrings.pathCtrlPtTooltip;
|
|
|
|
}
|
|
|
|
assignAttributes(pointGrip, atts);
|
|
|
|
getGripContainer().appendChild(pointGrip);
|
2018-05-18 04:02:30 +00:00
|
|
|
return pointGrip;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getCtrlLine = function (id) {
|
|
|
|
let ctrlLine = getElem('ctrlLine_' + id);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (ctrlLine) { return ctrlLine; }
|
2015-07-06 10:42:25 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
ctrlLine = document.createElementNS(NS.SVG, 'line');
|
2018-05-18 03:25:45 +00:00
|
|
|
assignAttributes(ctrlLine, {
|
|
|
|
id: 'ctrlLine_' + id,
|
|
|
|
stroke: '#555',
|
2018-05-18 04:02:30 +00:00
|
|
|
'stroke-width': 1,
|
2018-05-18 03:25:45 +00:00
|
|
|
style: 'pointer-events:none'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
2018-05-18 03:25:45 +00:00
|
|
|
getGripContainer().appendChild(ctrlLine);
|
2018-05-18 04:02:30 +00:00
|
|
|
return ctrlLine;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getPointGrip = function (seg, update) {
|
|
|
|
const {index} = seg;
|
|
|
|
const pointGrip = addPointGrip(index);
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
if (update) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const pt = getGripPt(seg);
|
|
|
|
assignAttributes(pointGrip, {
|
|
|
|
cx: pt.x,
|
|
|
|
cy: pt.y,
|
|
|
|
display: 'inline'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
return pointGrip;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getControlPoints = function (seg) {
|
|
|
|
const {item, index} = seg;
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!('x1' in item) || !('x2' in item)) { return null; }
|
2018-05-18 03:25:45 +00:00
|
|
|
const cpt = {};
|
|
|
|
/* const pointGripContainer = */ getGripContainer();
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// Note that this is intentionally not seg.prev.item
|
2018-05-18 03:25:45 +00:00
|
|
|
const prev = pathModule.path.segs[index - 1].item;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const segItems = [prev, item];
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 1; i < 3; i++) {
|
|
|
|
const id = index + 'c' + i;
|
2015-07-06 10:42:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const ctrlLine = cpt['c' + i + '_line'] = getCtrlLine(id);
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const pt = getGripPt(seg, {x: item['x' + i], y: item['y' + i]});
|
|
|
|
const gpt = getGripPt(seg, {x: segItems[i - 1].x, y: segItems[i - 1].y});
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
assignAttributes(ctrlLine, {
|
|
|
|
x1: pt.x,
|
|
|
|
y1: pt.y,
|
|
|
|
x2: gpt.x,
|
|
|
|
y2: gpt.y,
|
|
|
|
display: 'inline'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
cpt['c' + i + '_line'] = ctrlLine;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// create it
|
2018-05-18 03:25:45 +00:00
|
|
|
const pointGrip = cpt['c' + i] = addCtrlGrip(id);
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
assignAttributes(pointGrip, {
|
|
|
|
cx: pt.x,
|
|
|
|
cy: pt.y,
|
|
|
|
display: 'inline'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
cpt['c' + i] = pointGrip;
|
|
|
|
}
|
|
|
|
return cpt;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// This replaces the segment at the given index. Type is given as number.
|
2018-05-18 03:25:45 +00:00
|
|
|
export const replacePathSeg = function (type, index, pts, elem) {
|
|
|
|
const path = elem || pathModule.path.elem;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const func = 'createSVGPathSeg' + pathFuncs[type];
|
|
|
|
const seg = path[func].apply(path, pts);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (supportsPathReplaceItem()) {
|
2018-05-18 04:02:30 +00:00
|
|
|
path.pathSegList.replaceItem(seg, index);
|
|
|
|
} else {
|
2018-05-18 03:25:45 +00:00
|
|
|
const segList = path.pathSegList;
|
|
|
|
const len = segList.numberOfItems;
|
|
|
|
const arr = [];
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
const curSeg = segList.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
arr.push(curSeg);
|
|
|
|
}
|
|
|
|
segList.clear();
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 0; i < len; i++) {
|
2018-05-18 04:02:30 +00:00
|
|
|
if (i === index) {
|
|
|
|
segList.appendItem(seg);
|
|
|
|
} else {
|
|
|
|
segList.appendItem(arr[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getSegSelector = function (seg, update) {
|
|
|
|
const {index} = seg;
|
|
|
|
let segLine = getElem('segline_' + index);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!segLine) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const pointGripContainer = getGripContainer();
|
2018-05-18 04:02:30 +00:00
|
|
|
// create segline
|
|
|
|
segLine = document.createElementNS(NS.SVG, 'path');
|
2018-05-18 03:25:45 +00:00
|
|
|
assignAttributes(segLine, {
|
|
|
|
id: 'segline_' + index,
|
|
|
|
display: 'none',
|
|
|
|
fill: 'none',
|
|
|
|
stroke: '#0FF',
|
2018-05-18 04:02:30 +00:00
|
|
|
'stroke-width': 2,
|
2018-05-18 03:25:45 +00:00
|
|
|
style: 'pointer-events:none',
|
|
|
|
d: 'M0,0 0,0'
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
pointGripContainer.appendChild(segLine);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (update) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const {prev} = seg;
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!prev) {
|
|
|
|
segLine.setAttribute('display', 'none');
|
|
|
|
return segLine;
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const pt = getGripPt(prev);
|
2018-05-18 04:02:30 +00:00
|
|
|
// Set start point
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(2, 0, [pt.x, pt.y], segLine);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const pts = ptObjToArr(seg.type, seg.item, true);
|
|
|
|
for (let i = 0; i < pts.length; i += 2) {
|
|
|
|
const pt = getGripPt(seg, {x: pts[i], y: pts[i + 1]});
|
2018-05-18 04:02:30 +00:00
|
|
|
pts[i] = pt.x;
|
|
|
|
pts[i + 1] = pt.y;
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(seg.type, 1, pts, segLine);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
return segLine;
|
2011-02-04 08:02:46 +00:00
|
|
|
};
|
|
|
|
|
2011-02-09 06:14:47 +00:00
|
|
|
// Function: smoothControlPoints
|
|
|
|
// Takes three points and creates a smoother line based on them
|
2018-05-16 00:53:27 +00:00
|
|
|
//
|
|
|
|
// Parameters:
|
2011-02-09 06:14:47 +00:00
|
|
|
// ct1 - Object with x and y values (first control point)
|
|
|
|
// ct2 - Object with x and y values (second control point)
|
|
|
|
// pt - Object with x and y values (third point)
|
|
|
|
//
|
2018-05-16 00:53:27 +00:00
|
|
|
// Returns:
|
2011-02-09 06:14:47 +00:00
|
|
|
// Array of two "smoothed" point objects
|
2018-05-18 03:25:45 +00:00
|
|
|
export const smoothControlPoints = function (ct1, ct2, pt) {
|
2018-05-18 04:02:30 +00:00
|
|
|
// each point must not be the origin
|
2018-05-18 03:25:45 +00:00
|
|
|
const x1 = ct1.x - pt.x,
|
2018-05-18 04:02:30 +00:00
|
|
|
y1 = ct1.y - pt.y,
|
|
|
|
x2 = ct2.x - pt.x,
|
|
|
|
y2 = ct2.y - pt.y;
|
|
|
|
|
|
|
|
if ((x1 !== 0 || y1 !== 0) && (x2 !== 0 || y2 !== 0)) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const
|
2018-05-18 04:02:30 +00:00
|
|
|
r1 = Math.sqrt(x1 * x1 + y1 * y1),
|
|
|
|
r2 = Math.sqrt(x2 * x2 + y2 * y2),
|
|
|
|
nct1 = editorContext_.getSVGRoot().createSVGPoint(),
|
|
|
|
nct2 = editorContext_.getSVGRoot().createSVGPoint();
|
2018-05-18 03:25:45 +00:00
|
|
|
let anglea = Math.atan2(y1, x1),
|
|
|
|
angleb = Math.atan2(y2, x2);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (anglea < 0) { anglea += 2 * Math.PI; }
|
|
|
|
if (angleb < 0) { angleb += 2 * Math.PI; }
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const angleBetween = Math.abs(anglea - angleb),
|
2018-05-18 04:02:30 +00:00
|
|
|
angleDiff = Math.abs(Math.PI - angleBetween) / 2;
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let newAnglea, newAngleb;
|
2018-05-18 04:02:30 +00:00
|
|
|
if (anglea - angleb > 0) {
|
|
|
|
newAnglea = angleBetween < Math.PI ? (anglea + angleDiff) : (anglea - angleDiff);
|
|
|
|
newAngleb = angleBetween < Math.PI ? (angleb - angleDiff) : (angleb + angleDiff);
|
|
|
|
} else {
|
|
|
|
newAnglea = angleBetween < Math.PI ? (anglea - angleDiff) : (anglea + angleDiff);
|
|
|
|
newAngleb = angleBetween < Math.PI ? (angleb + angleDiff) : (angleb - angleDiff);
|
|
|
|
}
|
|
|
|
|
|
|
|
// rotate the points
|
|
|
|
nct1.x = r1 * Math.cos(newAnglea) + pt.x;
|
|
|
|
nct1.y = r1 * Math.sin(newAnglea) + pt.y;
|
|
|
|
nct2.x = r2 * Math.cos(newAngleb) + pt.x;
|
|
|
|
nct2.y = r2 * Math.sin(newAngleb) + pt.y;
|
|
|
|
|
|
|
|
return [nct1, nct2];
|
|
|
|
}
|
|
|
|
return undefined;
|
2011-02-09 06:14:47 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export class Segment {
|
|
|
|
constructor (index, item) {
|
|
|
|
this.selected = false;
|
|
|
|
this.index = index;
|
|
|
|
this.item = item;
|
|
|
|
this.type = item.pathSegType;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
this.ctrlpts = [];
|
|
|
|
this.ptgrip = null;
|
|
|
|
this.segsel = null;
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
showCtrlPts (y) {
|
|
|
|
for (const i in this.ctrlpts) {
|
|
|
|
if (this.ctrlpts.hasOwnProperty(i)) {
|
|
|
|
this.ctrlpts[i].setAttribute('display', y ? 'inline' : 'none');
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
selectCtrls (y) {
|
|
|
|
$('#ctrlpointgrip_' + this.index + 'c1, #ctrlpointgrip_' + this.index + 'c2')
|
|
|
|
.attr('fill', y ? '#0FF' : '#EEE');
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
show (y) {
|
|
|
|
if (this.ptgrip) {
|
|
|
|
this.ptgrip.setAttribute('display', y ? 'inline' : 'none');
|
|
|
|
this.segsel.setAttribute('display', y ? 'inline' : 'none');
|
|
|
|
// Show/hide all control points if available
|
|
|
|
this.showCtrlPts(y);
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
select (y) {
|
|
|
|
if (this.ptgrip) {
|
|
|
|
this.ptgrip.setAttribute('stroke', y ? '#0FF' : '#00F');
|
|
|
|
this.segsel.setAttribute('display', y ? 'inline' : 'none');
|
|
|
|
if (this.ctrlpts) {
|
|
|
|
this.selectCtrls(y);
|
|
|
|
}
|
|
|
|
this.selected = y;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
addGrip () {
|
|
|
|
this.ptgrip = getPointGrip(this, true);
|
|
|
|
this.ctrlpts = getControlPoints(this, true);
|
|
|
|
this.segsel = getSegSelector(this, true);
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
update (full) {
|
|
|
|
if (this.ptgrip) {
|
|
|
|
const pt = getGripPt(this);
|
|
|
|
assignAttributes(this.ptgrip, {
|
|
|
|
cx: pt.x,
|
|
|
|
cy: pt.y
|
|
|
|
});
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
getSegSelector(this, true);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (this.ctrlpts) {
|
|
|
|
if (full) {
|
|
|
|
this.item = pathModule.path.elem.pathSegList.getItem(this.index);
|
|
|
|
this.type = this.item.pathSegType;
|
|
|
|
}
|
|
|
|
getControlPoints(this);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
// this.segsel.setAttribute('display', y?'inline':'none');
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
move (dx, dy) {
|
|
|
|
const {item} = this;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const curPts = this.ctrlpts
|
|
|
|
? [item.x += dx, item.y += dy,
|
|
|
|
item.x1, item.y1, item.x2 += dx, item.y2 += dy
|
|
|
|
]
|
|
|
|
: [item.x += dx, item.y += dy];
|
2015-07-06 10:21:37 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(this.type, this.index, curPts);
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (this.next && this.next.ctrlpts) {
|
|
|
|
const next = this.next.item;
|
|
|
|
const nextPts = [next.x, next.y,
|
|
|
|
next.x1 += dx, next.y1 += dy, next.x2, next.y2];
|
|
|
|
replacePathSeg(this.next.type, this.next.index, nextPts);
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (this.mate) {
|
|
|
|
// The last point of a closed subpath has a 'mate',
|
|
|
|
// which is the 'M' segment of the subpath
|
|
|
|
const {item} = this.mate;
|
|
|
|
const pts = [item.x += dx, item.y += dy];
|
|
|
|
replacePathSeg(this.mate.type, this.mate.index, pts);
|
|
|
|
// Has no grip, so does not need 'updating'?
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
this.update(true);
|
|
|
|
if (this.next) { this.next.update(true); }
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
setLinked (num) {
|
|
|
|
let seg, anum, pt;
|
|
|
|
if (num === 2) {
|
|
|
|
anum = 1;
|
|
|
|
seg = this.next;
|
|
|
|
if (!seg) { return; }
|
|
|
|
pt = this.item;
|
|
|
|
} else {
|
|
|
|
anum = 2;
|
|
|
|
seg = this.prev;
|
|
|
|
if (!seg) { return; }
|
|
|
|
pt = seg.item;
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const {item} = seg;
|
|
|
|
item['x' + anum] = pt.x + (pt.x - this.item['x' + num]);
|
|
|
|
item['y' + anum] = pt.y + (pt.y - this.item['y' + num]);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const pts = [item.x, item.y,
|
|
|
|
item.x1, item.y1,
|
|
|
|
item.x2, item.y2];
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(seg.type, seg.index, pts);
|
|
|
|
seg.update(true);
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
moveCtrl (num, dx, dy) {
|
|
|
|
const {item} = this;
|
|
|
|
item['x' + num] += dx;
|
|
|
|
item['y' + num] += dy;
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const pts = [item.x, item.y,
|
|
|
|
item.x1, item.y1, item.x2, item.y2];
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(this.type, this.index, pts);
|
|
|
|
this.update(true);
|
|
|
|
}
|
2011-02-04 08:02:46 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
setType (newType, pts) {
|
|
|
|
replacePathSeg(newType, this.index, pts);
|
|
|
|
this.type = newType;
|
|
|
|
this.item = pathModule.path.elem.pathSegList.getItem(this.index);
|
|
|
|
this.showCtrlPts(newType === 6);
|
|
|
|
this.ctrlpts = getControlPoints(this);
|
|
|
|
this.update(true);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export class Path {
|
|
|
|
constructor (elem) {
|
|
|
|
if (!elem || elem.tagName !== 'path') {
|
|
|
|
throw new Error('svgedit.path.Path constructed without a <path> element');
|
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
this.elem = elem;
|
|
|
|
this.segs = [];
|
|
|
|
this.selected_pts = [];
|
|
|
|
pathModule.path = this;
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
this.init();
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// Reset path data
|
|
|
|
init () {
|
|
|
|
// Hide all grips, etc
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// fixed, needed to work on all found elements, not just first
|
|
|
|
$(getGripContainer()).find('*').each(function () {
|
|
|
|
$(this).attr('display', 'none');
|
|
|
|
});
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const segList = this.elem.pathSegList;
|
|
|
|
const len = segList.numberOfItems;
|
|
|
|
this.segs = [];
|
|
|
|
this.selected_pts = [];
|
|
|
|
this.first_seg = null;
|
|
|
|
|
|
|
|
// Set up segs array
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
const item = segList.getItem(i);
|
|
|
|
const segment = new Segment(i, item);
|
|
|
|
segment.path = this;
|
|
|
|
this.segs.push(segment);
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const {segs} = this;
|
|
|
|
|
|
|
|
let startI = null;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
const seg = segs[i];
|
|
|
|
const nextSeg = (i + 1) >= len ? null : segs[i + 1];
|
|
|
|
const prevSeg = (i - 1) < 0 ? null : segs[i - 1];
|
|
|
|
if (seg.type === 2) {
|
|
|
|
if (prevSeg && prevSeg.type !== 1) {
|
|
|
|
// New sub-path, last one is open,
|
|
|
|
// so add a grip to last sub-path's first point
|
|
|
|
const startSeg = segs[startI];
|
|
|
|
startSeg.next = segs[startI + 1];
|
|
|
|
startSeg.next.prev = startSeg;
|
|
|
|
startSeg.addGrip();
|
|
|
|
}
|
|
|
|
// Remember that this is a starter seg
|
|
|
|
startI = i;
|
|
|
|
} else if (nextSeg && nextSeg.type === 1) {
|
|
|
|
// This is the last real segment of a closed sub-path
|
|
|
|
// Next is first seg after "M"
|
|
|
|
seg.next = segs[startI + 1];
|
|
|
|
|
|
|
|
// First seg after "M"'s prev is this
|
2018-05-18 04:02:30 +00:00
|
|
|
seg.next.prev = seg;
|
2018-05-18 03:25:45 +00:00
|
|
|
seg.mate = segs[startI];
|
|
|
|
seg.addGrip();
|
|
|
|
if (this.first_seg == null) {
|
|
|
|
this.first_seg = seg;
|
|
|
|
}
|
|
|
|
} else if (!nextSeg) {
|
|
|
|
if (seg.type !== 1) {
|
|
|
|
// Last seg, doesn't close so add a grip
|
|
|
|
// to last sub-path's first point
|
|
|
|
const startSeg = segs[startI];
|
|
|
|
startSeg.next = segs[startI + 1];
|
|
|
|
startSeg.next.prev = startSeg;
|
|
|
|
startSeg.addGrip();
|
|
|
|
seg.addGrip();
|
|
|
|
|
|
|
|
if (!this.first_seg) {
|
|
|
|
// Open path, so set first as real first and add grip
|
|
|
|
this.first_seg = segs[startI];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (seg.type !== 1) {
|
|
|
|
// Regular segment, so add grip and its "next"
|
|
|
|
seg.addGrip();
|
|
|
|
|
|
|
|
// Don't set its "next" if it's an "M"
|
|
|
|
if (nextSeg && nextSeg.type !== 2) {
|
|
|
|
seg.next = nextSeg;
|
|
|
|
seg.next.prev = seg;
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
return this;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
eachSeg (fn) {
|
|
|
|
const len = this.segs.length;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
const ret = fn.call(this.segs[i], i);
|
|
|
|
if (ret === false) { break; }
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
addSeg (index) {
|
|
|
|
// Adds a new segment
|
|
|
|
const seg = this.segs[index];
|
|
|
|
if (!seg.prev) { return; }
|
|
|
|
|
|
|
|
const {prev} = seg;
|
|
|
|
let newseg, newX, newY;
|
|
|
|
switch (seg.item.pathSegType) {
|
|
|
|
case 4: {
|
|
|
|
newX = (seg.item.x + prev.item.x) / 2;
|
|
|
|
newY = (seg.item.y + prev.item.y) / 2;
|
|
|
|
newseg = this.elem.createSVGPathSegLinetoAbs(newX, newY);
|
|
|
|
break;
|
|
|
|
} case 6: { // make it a curved segment to preserve the shape (WRS)
|
|
|
|
// https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm#Geometric_interpretation
|
|
|
|
const p0x = (prev.item.x + seg.item.x1) / 2;
|
|
|
|
const p1x = (seg.item.x1 + seg.item.x2) / 2;
|
|
|
|
const p2x = (seg.item.x2 + seg.item.x) / 2;
|
|
|
|
const p01x = (p0x + p1x) / 2;
|
|
|
|
const p12x = (p1x + p2x) / 2;
|
|
|
|
newX = (p01x + p12x) / 2;
|
|
|
|
const p0y = (prev.item.y + seg.item.y1) / 2;
|
|
|
|
const p1y = (seg.item.y1 + seg.item.y2) / 2;
|
|
|
|
const p2y = (seg.item.y2 + seg.item.y) / 2;
|
|
|
|
const p01y = (p0y + p1y) / 2;
|
|
|
|
const p12y = (p1y + p2y) / 2;
|
|
|
|
newY = (p01y + p12y) / 2;
|
|
|
|
newseg = this.elem.createSVGPathSegCurvetoCubicAbs(newX, newY, p0x, p0y, p01x, p01y);
|
|
|
|
const pts = [seg.item.x, seg.item.y, p12x, p12y, p2x, p2y];
|
|
|
|
replacePathSeg(seg.type, index, pts);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
insertItemBefore(this.elem, newseg, index);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
deleteSeg (index) {
|
|
|
|
const seg = this.segs[index];
|
|
|
|
const list = this.elem.pathSegList;
|
|
|
|
|
|
|
|
seg.show(false);
|
|
|
|
const {next} = seg;
|
|
|
|
if (seg.mate) {
|
|
|
|
// Make the next point be the "M" point
|
|
|
|
const pt = [next.item.x, next.item.y];
|
|
|
|
replacePathSeg(2, next.index, pt);
|
|
|
|
|
|
|
|
// Reposition last node
|
|
|
|
replacePathSeg(4, seg.index, pt);
|
|
|
|
|
|
|
|
list.removeItem(seg.mate.index);
|
|
|
|
} else if (!seg.prev) {
|
|
|
|
// First node of open path, make next point the M
|
|
|
|
// const {item} = seg;
|
|
|
|
const pt = [next.item.x, next.item.y];
|
|
|
|
replacePathSeg(2, seg.next.index, pt);
|
|
|
|
list.removeItem(index);
|
|
|
|
} else {
|
|
|
|
list.removeItem(index);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
subpathIsClosed (index) {
|
|
|
|
let closed = false;
|
|
|
|
// Check if subpath is already open
|
|
|
|
pathModule.path.eachSeg(function (i) {
|
|
|
|
if (i <= index) { return true; }
|
|
|
|
if (this.type === 2) {
|
|
|
|
// Found M first, so open
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (this.type === 1) {
|
|
|
|
// Found Z first, so closed
|
|
|
|
closed = true;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
return closed;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
removePtFromSelection (index) {
|
|
|
|
const pos = this.selected_pts.indexOf(index);
|
|
|
|
if (pos === -1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.segs[index].select(false);
|
|
|
|
this.selected_pts.splice(pos, 1);
|
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
clearSelection () {
|
|
|
|
this.eachSeg(function () {
|
|
|
|
// 'this' is the segment here
|
|
|
|
this.select(false);
|
|
|
|
});
|
|
|
|
this.selected_pts = [];
|
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
storeD () {
|
|
|
|
this.last_d = this.elem.getAttribute('d');
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
show (y) {
|
|
|
|
// Shows this path's segment grips
|
|
|
|
this.eachSeg(function () {
|
|
|
|
// 'this' is the segment here
|
|
|
|
this.show(y);
|
|
|
|
});
|
|
|
|
if (y) {
|
|
|
|
this.selectPt(this.first_seg.index);
|
|
|
|
}
|
|
|
|
return this;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// Move selected points
|
|
|
|
movePts (dx, dy) {
|
|
|
|
let i = this.selected_pts.length;
|
|
|
|
while (i--) {
|
|
|
|
const seg = this.segs[this.selected_pts[i]];
|
|
|
|
seg.move(dx, dy);
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
moveCtrl (dx, dy) {
|
|
|
|
const seg = this.segs[this.selected_pts[0]];
|
|
|
|
seg.moveCtrl(this.dragctrl, dx, dy);
|
|
|
|
if (linkControlPts) {
|
|
|
|
seg.setLinked(this.dragctrl);
|
|
|
|
}
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
setSegType (newType) {
|
|
|
|
this.storeD();
|
|
|
|
let i = this.selected_pts.length;
|
|
|
|
let text;
|
|
|
|
while (i--) {
|
|
|
|
const selPt = this.selected_pts[i];
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// Selected seg
|
|
|
|
const cur = this.segs[selPt];
|
|
|
|
const {prev} = cur;
|
|
|
|
if (!prev) { continue; }
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
if (!newType) { // double-click, so just toggle
|
|
|
|
text = 'Toggle Path Segment Type';
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// Toggle segment to curve/straight line
|
|
|
|
const oldType = cur.type;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
newType = (oldType === 6) ? 4 : 6;
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
newType = Number(newType);
|
|
|
|
|
|
|
|
const curX = cur.item.x;
|
|
|
|
const curY = cur.item.y;
|
|
|
|
const prevX = prev.item.x;
|
|
|
|
const prevY = prev.item.y;
|
|
|
|
let points;
|
|
|
|
switch (newType) {
|
|
|
|
case 6: {
|
|
|
|
if (cur.olditem) {
|
|
|
|
const old = cur.olditem;
|
|
|
|
points = [curX, curY, old.x1, old.y1, old.x2, old.y2];
|
|
|
|
} else {
|
|
|
|
const diffX = curX - prevX;
|
|
|
|
const diffY = curY - prevY;
|
|
|
|
// get control points from straight line segment
|
|
|
|
/*
|
|
|
|
const ct1x = (prevX + (diffY/2));
|
|
|
|
const ct1y = (prevY - (diffX/2));
|
|
|
|
const ct2x = (curX + (diffY/2));
|
|
|
|
const ct2y = (curY - (diffX/2));
|
|
|
|
*/
|
|
|
|
// create control points on the line to preserve the shape (WRS)
|
|
|
|
const ct1x = (prevX + (diffX / 3));
|
|
|
|
const ct1y = (prevY + (diffY / 3));
|
|
|
|
const ct2x = (curX - (diffX / 3));
|
|
|
|
const ct2y = (curY - (diffY / 3));
|
|
|
|
points = [curX, curY, ct1x, ct1y, ct2x, ct2y];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
} case 4: {
|
|
|
|
points = [curX, curY];
|
|
|
|
|
|
|
|
// Store original prevve segment nums
|
|
|
|
cur.olditem = cur.item;
|
|
|
|
break;
|
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
cur.setType(newType, points);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
pathModule.path.endChanges(text);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
selectPt (pt, ctrlNum) {
|
|
|
|
this.clearSelection();
|
|
|
|
if (pt == null) {
|
|
|
|
this.eachSeg(function (i) {
|
|
|
|
// 'this' is the segment here.
|
|
|
|
if (this.prev) {
|
|
|
|
pt = i;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.addPtsToSelection(pt);
|
|
|
|
if (ctrlNum) {
|
|
|
|
this.dragctrl = ctrlNum;
|
|
|
|
|
|
|
|
if (linkControlPts) {
|
|
|
|
this.segs[pt].setLinked(ctrlNum);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
}
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
// Update position of all points
|
|
|
|
update () {
|
|
|
|
const {elem} = this;
|
|
|
|
if (getRotationAngle(elem)) {
|
|
|
|
this.matrix = getMatrix(elem);
|
|
|
|
this.imatrix = this.matrix.inverse();
|
|
|
|
} else {
|
|
|
|
this.matrix = null;
|
|
|
|
this.imatrix = null;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
|
|
|
|
this.eachSeg(function (i) {
|
|
|
|
this.item = elem.pathSegList.getItem(i);
|
|
|
|
this.update();
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-04 16:06:25 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
endChanges (text) {
|
|
|
|
if (isWebkit()) { editorContext_.resetD(this.elem); }
|
|
|
|
const cmd = new ChangeElementCommand(this.elem, {d: this.last_d}, text);
|
|
|
|
editorContext_.endChanges({cmd, elem: this.elem});
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2011-02-09 06:14:47 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
addPtsToSelection (indexes) {
|
|
|
|
if (!Array.isArray(indexes)) { indexes = [indexes]; }
|
|
|
|
for (let i = 0; i < indexes.length; i++) {
|
|
|
|
const index = indexes[i];
|
|
|
|
const seg = this.segs[index];
|
|
|
|
if (seg.ptgrip) {
|
|
|
|
if (!this.selected_pts.includes(index) && index >= 0) {
|
|
|
|
this.selected_pts.push(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.selected_pts.sort();
|
|
|
|
let i = this.selected_pts.length;
|
|
|
|
const grips = [];
|
|
|
|
grips.length = i;
|
|
|
|
// Loop through points to be selected and highlight each
|
|
|
|
while (i--) {
|
|
|
|
const pt = this.selected_pts[i];
|
|
|
|
const seg = this.segs[pt];
|
|
|
|
seg.select(true);
|
|
|
|
grips[i] = seg.ptgrip;
|
|
|
|
}
|
2011-02-09 06:14:47 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const closedSubpath = this.subpathIsClosed(this.selected_pts[0]);
|
|
|
|
editorContext_.addPtsToSelection({grips, closedSubpath});
|
|
|
|
}
|
|
|
|
}
|
2011-02-09 06:14:47 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const getPath_ = function (elem) {
|
|
|
|
let p = pathData[elem.id];
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!p) {
|
2018-05-18 03:25:45 +00:00
|
|
|
p = pathData[elem.id] = new Path(elem);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
return p;
|
2011-03-07 18:26:12 +00:00
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const removePath_ = function (id) {
|
2018-05-18 04:02:30 +00:00
|
|
|
if (id in pathData) { delete pathData[id]; }
|
2011-03-07 18:26:12 +00:00
|
|
|
};
|
2018-05-18 03:25:45 +00:00
|
|
|
|
|
|
|
let newcx, newcy, oldcx, oldcy, angle;
|
|
|
|
|
|
|
|
const getRotVals = function (x, y) {
|
|
|
|
let dx = x - oldcx;
|
|
|
|
let dy = y - oldcy;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// rotate the point around the old center
|
2018-05-18 03:25:45 +00:00
|
|
|
let r = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
let theta = Math.atan2(dy, dx) + angle;
|
2018-05-18 04:02:30 +00:00
|
|
|
dx = r * Math.cos(theta) + oldcx;
|
|
|
|
dy = r * Math.sin(theta) + oldcy;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// dx,dy should now hold the actual coordinates of each
|
|
|
|
// point after being rotated
|
2011-02-24 16:13:26 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// now we want to rotate them around the new center in the reverse direction
|
|
|
|
dx -= newcx;
|
|
|
|
dy -= newcy;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
r = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
theta = Math.atan2(dy, dx) - angle;
|
2013-02-14 13:11:01 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
return {x: r * Math.cos(theta) + newcx,
|
|
|
|
y: r * Math.sin(theta) + newcy};
|
2011-02-24 16:13:26 +00:00
|
|
|
};
|
|
|
|
|
2011-03-07 18:26:12 +00:00
|
|
|
// If the path was rotated, we must now pay the piper:
|
2018-05-16 00:53:27 +00:00
|
|
|
// Every path point must be rotated into the rotated coordinate system of
|
2011-03-07 18:26:12 +00:00
|
|
|
// its old center, then determine the new center, then rotate it back
|
|
|
|
// This is because we want the path to remember its rotation
|
|
|
|
|
|
|
|
// TODO: This is still using ye olde transform methods, can probably
|
2018-05-18 03:25:45 +00:00
|
|
|
// be optimized or even taken care of by `recalculateDimensions`
|
|
|
|
export const recalcRotatedPath = function () {
|
|
|
|
const currentPath = pathModule.path.elem;
|
|
|
|
angle = getRotationAngle(currentPath, true);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (!angle) { return; }
|
2018-05-18 03:25:45 +00:00
|
|
|
// selectedBBoxes[0] = pathModule.path.oldbbox;
|
|
|
|
const oldbox = pathModule.path.oldbbox; // selectedBBoxes[0],
|
2018-05-18 04:02:30 +00:00
|
|
|
oldcx = oldbox.x + oldbox.width / 2;
|
|
|
|
oldcy = oldbox.y + oldbox.height / 2;
|
2018-05-18 03:25:45 +00:00
|
|
|
let box = getBBox(currentPath);
|
2018-05-18 04:02:30 +00:00
|
|
|
newcx = box.x + box.width / 2;
|
|
|
|
newcy = box.y + box.height / 2;
|
|
|
|
|
|
|
|
// un-rotate the new center to the proper position
|
2018-05-18 03:25:45 +00:00
|
|
|
const dx = newcx - oldcx,
|
2018-05-18 04:02:30 +00:00
|
|
|
dy = newcy - oldcy,
|
|
|
|
r = Math.sqrt(dx * dx + dy * dy),
|
|
|
|
theta = Math.atan2(dy, dx) + angle;
|
|
|
|
|
|
|
|
newcx = r * Math.cos(theta) + oldcx;
|
|
|
|
newcy = r * Math.sin(theta) + oldcy;
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const list = currentPath.pathSegList;
|
|
|
|
|
|
|
|
let i = list.numberOfItems;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (i) {
|
|
|
|
i -= 1;
|
2018-05-18 03:25:45 +00:00
|
|
|
const seg = list.getItem(i),
|
2018-05-18 04:02:30 +00:00
|
|
|
type = seg.pathSegType;
|
|
|
|
if (type === 1) { continue; }
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const rvals = getRotVals(seg.x, seg.y),
|
2018-05-18 04:02:30 +00:00
|
|
|
points = [rvals.x, rvals.y];
|
|
|
|
if (seg.x1 != null && seg.x2 != null) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const cVals1 = getRotVals(seg.x1, seg.y1);
|
|
|
|
const cVals2 = getRotVals(seg.x2, seg.y2);
|
2018-05-18 04:02:30 +00:00
|
|
|
points.splice(points.length, 0, cVals1.x, cVals1.y, cVals2.x, cVals2.y);
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
replacePathSeg(type, i, points);
|
2018-05-18 04:02:30 +00:00
|
|
|
} // loop for each point
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
box = getBBox(currentPath);
|
2018-05-18 04:02:30 +00:00
|
|
|
// selectedBBoxes[0].x = box.x; selectedBBoxes[0].y = box.y;
|
|
|
|
// selectedBBoxes[0].width = box.width; selectedBBoxes[0].height = box.height;
|
|
|
|
|
|
|
|
// now we must set the new transform to be rotated around the new center
|
2018-05-18 03:25:45 +00:00
|
|
|
const Rnc = editorContext_.getSVGRoot().createSVGTransform(),
|
|
|
|
tlist = getTransformList(currentPath);
|
2018-05-18 04:02:30 +00:00
|
|
|
Rnc.setRotate((angle * 180.0 / Math.PI), newcx, newcy);
|
|
|
|
tlist.replaceItem(Rnc, 0);
|
2011-02-23 16:24:07 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// ====================================
|
|
|
|
// Public API starts here
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
export const clearData = function () {
|
2018-05-18 04:02:30 +00:00
|
|
|
pathData = {};
|
2011-02-23 16:24:07 +00:00
|
|
|
};
|