import Path, { PathProps } from '../graphic/Path';
|
import PathProxy from '../core/PathProxy';
|
import transformPath from './transformPath';
|
import { VectorArray } from '../core/vector';
|
import { MatrixArray } from '../core/matrix';
|
import { extend } from '../core/util';
|
|
// command chars
|
// const cc = [
|
// 'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z',
|
// 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A'
|
// ];
|
|
const mathSqrt = Math.sqrt;
|
const mathSin = Math.sin;
|
const mathCos = Math.cos;
|
const PI = Math.PI;
|
|
function vMag(v: VectorArray): number {
|
return Math.sqrt(v[0] * v[0] + v[1] * v[1]);
|
};
|
function vRatio(u: VectorArray, v: VectorArray): number {
|
return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v));
|
};
|
function vAngle(u: VectorArray, v: VectorArray): number {
|
return (u[0] * v[1] < u[1] * v[0] ? -1 : 1)
|
* Math.acos(vRatio(u, v));
|
};
|
|
function processArc(
|
x1: number, y1: number, x2: number, y2: number, fa: number, fs: number,
|
rx: number, ry: number, psiDeg: number, cmd: number, path: PathProxy
|
) {
|
// https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
const psi = psiDeg * (PI / 180.0);
|
const xp = mathCos(psi) * (x1 - x2) / 2.0
|
+ mathSin(psi) * (y1 - y2) / 2.0;
|
const yp = -1 * mathSin(psi) * (x1 - x2) / 2.0
|
+ mathCos(psi) * (y1 - y2) / 2.0;
|
|
const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);
|
|
if (lambda > 1) {
|
rx *= mathSqrt(lambda);
|
ry *= mathSqrt(lambda);
|
}
|
|
const f = (fa === fs ? -1 : 1)
|
* mathSqrt((((rx * rx) * (ry * ry))
|
- ((rx * rx) * (yp * yp))
|
- ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp)
|
+ (ry * ry) * (xp * xp))
|
) || 0;
|
|
const cxp = f * rx * yp / ry;
|
const cyp = f * -ry * xp / rx;
|
|
const cx = (x1 + x2) / 2.0
|
+ mathCos(psi) * cxp
|
- mathSin(psi) * cyp;
|
const cy = (y1 + y2) / 2.0
|
+ mathSin(psi) * cxp
|
+ mathCos(psi) * cyp;
|
|
const theta = vAngle([ 1, 0 ], [ (xp - cxp) / rx, (yp - cyp) / ry ]);
|
const u = [ (xp - cxp) / rx, (yp - cyp) / ry ];
|
const v = [ (-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry ];
|
let dTheta = vAngle(u, v);
|
|
if (vRatio(u, v) <= -1) {
|
dTheta = PI;
|
}
|
if (vRatio(u, v) >= 1) {
|
dTheta = 0;
|
}
|
|
if (dTheta < 0) {
|
const n = Math.round(dTheta / PI * 1e6) / 1e6;
|
// Convert to positive
|
dTheta = PI * 2 + (n % 2) * PI;
|
}
|
|
path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs);
|
}
|
|
|
const commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig;
|
// Consider case:
|
// (1) delimiter can be comma or space, where continuous commas
|
// or spaces should be seen as one comma.
|
// (2) value can be like:
|
// '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983',
|
// 'l-.5E1,54', '121-23-44-11' (no delimiter)
|
const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;
|
// const valueSplitReg = /[\s,]+/;
|
|
function createPathProxyFromString(data: string) {
|
const path = new PathProxy();
|
|
if (!data) {
|
return path;
|
}
|
|
// const data = data.replace(/-/g, ' -')
|
// .replace(/ /g, ' ')
|
// .replace(/ /g, ',')
|
// .replace(/,,/g, ',');
|
|
// const n;
|
// create pipes so that we can split the data
|
// for (n = 0; n < cc.length; n++) {
|
// cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]);
|
// }
|
|
// data = data.replace(/-/g, ',-');
|
|
// create array
|
// const arr = cs.split('|');
|
// init context point
|
let cpx = 0;
|
let cpy = 0;
|
let subpathX = cpx;
|
let subpathY = cpy;
|
let prevCmd;
|
|
const CMD = PathProxy.CMD;
|
|
// commandReg.lastIndex = 0;
|
// const cmdResult;
|
// while ((cmdResult = commandReg.exec(data)) != null) {
|
// const cmdStr = cmdResult[1];
|
// const cmdContent = cmdResult[2];
|
|
const cmdList = data.match(commandReg);
|
if (!cmdList) {
|
// Invalid svg path.
|
return path;
|
}
|
|
for (let l = 0; l < cmdList.length; l++) {
|
const cmdText = cmdList[l];
|
let cmdStr = cmdText.charAt(0);
|
|
let cmd;
|
|
// String#split is faster a little bit than String#replace or RegExp#exec.
|
// const p = cmdContent.split(valueSplitReg);
|
// const pLen = 0;
|
// for (let i = 0; i < p.length; i++) {
|
// // '' and other invalid str => NaN
|
// const val = parseFloat(p[i]);
|
// !isNaN(val) && (p[pLen++] = val);
|
// }
|
|
|
// Following code will convert string to number. So convert type to number here
|
const p = cmdText.match(numberReg) as any[] as number[] || [];
|
const pLen = p.length;
|
for (let i = 0; i < pLen; i++) {
|
p[i] = parseFloat(p[i] as any as string);
|
}
|
|
let off = 0;
|
while (off < pLen) {
|
let ctlPtx;
|
let ctlPty;
|
|
let rx;
|
let ry;
|
let psi;
|
let fa;
|
let fs;
|
|
let x1 = cpx;
|
let y1 = cpy;
|
|
let len: number;
|
let pathData: number[] | Float32Array;
|
// convert l, H, h, V, and v to L
|
switch (cmdStr) {
|
case 'l':
|
cpx += p[off++];
|
cpy += p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'L':
|
cpx = p[off++];
|
cpy = p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'm':
|
cpx += p[off++];
|
cpy += p[off++];
|
cmd = CMD.M;
|
path.addData(cmd, cpx, cpy);
|
subpathX = cpx;
|
subpathY = cpy;
|
cmdStr = 'l';
|
break;
|
case 'M':
|
cpx = p[off++];
|
cpy = p[off++];
|
cmd = CMD.M;
|
path.addData(cmd, cpx, cpy);
|
subpathX = cpx;
|
subpathY = cpy;
|
cmdStr = 'L';
|
break;
|
case 'h':
|
cpx += p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'H':
|
cpx = p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'v':
|
cpy += p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'V':
|
cpy = p[off++];
|
cmd = CMD.L;
|
path.addData(cmd, cpx, cpy);
|
break;
|
case 'C':
|
cmd = CMD.C;
|
path.addData(
|
cmd, p[off++], p[off++], p[off++], p[off++], p[off++], p[off++]
|
);
|
cpx = p[off - 2];
|
cpy = p[off - 1];
|
break;
|
case 'c':
|
cmd = CMD.C;
|
path.addData(
|
cmd,
|
p[off++] + cpx, p[off++] + cpy,
|
p[off++] + cpx, p[off++] + cpy,
|
p[off++] + cpx, p[off++] + cpy
|
);
|
cpx += p[off - 2];
|
cpy += p[off - 1];
|
break;
|
case 'S':
|
ctlPtx = cpx;
|
ctlPty = cpy;
|
len = path.len();
|
pathData = path.data;
|
if (prevCmd === CMD.C) {
|
ctlPtx += cpx - pathData[len - 4];
|
ctlPty += cpy - pathData[len - 3];
|
}
|
cmd = CMD.C;
|
x1 = p[off++];
|
y1 = p[off++];
|
cpx = p[off++];
|
cpy = p[off++];
|
path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
|
break;
|
case 's':
|
ctlPtx = cpx;
|
ctlPty = cpy;
|
len = path.len();
|
pathData = path.data;
|
if (prevCmd === CMD.C) {
|
ctlPtx += cpx - pathData[len - 4];
|
ctlPty += cpy - pathData[len - 3];
|
}
|
cmd = CMD.C;
|
x1 = cpx + p[off++];
|
y1 = cpy + p[off++];
|
cpx += p[off++];
|
cpy += p[off++];
|
path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy);
|
break;
|
case 'Q':
|
x1 = p[off++];
|
y1 = p[off++];
|
cpx = p[off++];
|
cpy = p[off++];
|
cmd = CMD.Q;
|
path.addData(cmd, x1, y1, cpx, cpy);
|
break;
|
case 'q':
|
x1 = p[off++] + cpx;
|
y1 = p[off++] + cpy;
|
cpx += p[off++];
|
cpy += p[off++];
|
cmd = CMD.Q;
|
path.addData(cmd, x1, y1, cpx, cpy);
|
break;
|
case 'T':
|
ctlPtx = cpx;
|
ctlPty = cpy;
|
len = path.len();
|
pathData = path.data;
|
if (prevCmd === CMD.Q) {
|
ctlPtx += cpx - pathData[len - 4];
|
ctlPty += cpy - pathData[len - 3];
|
}
|
cpx = p[off++];
|
cpy = p[off++];
|
cmd = CMD.Q;
|
path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
|
break;
|
case 't':
|
ctlPtx = cpx;
|
ctlPty = cpy;
|
len = path.len();
|
pathData = path.data;
|
if (prevCmd === CMD.Q) {
|
ctlPtx += cpx - pathData[len - 4];
|
ctlPty += cpy - pathData[len - 3];
|
}
|
cpx += p[off++];
|
cpy += p[off++];
|
cmd = CMD.Q;
|
path.addData(cmd, ctlPtx, ctlPty, cpx, cpy);
|
break;
|
case 'A':
|
rx = p[off++];
|
ry = p[off++];
|
psi = p[off++];
|
fa = p[off++];
|
fs = p[off++];
|
|
x1 = cpx, y1 = cpy;
|
cpx = p[off++];
|
cpy = p[off++];
|
cmd = CMD.A;
|
processArc(
|
x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
|
);
|
break;
|
case 'a':
|
rx = p[off++];
|
ry = p[off++];
|
psi = p[off++];
|
fa = p[off++];
|
fs = p[off++];
|
|
x1 = cpx, y1 = cpy;
|
cpx += p[off++];
|
cpy += p[off++];
|
cmd = CMD.A;
|
processArc(
|
x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path
|
);
|
break;
|
}
|
}
|
|
if (cmdStr === 'z' || cmdStr === 'Z') {
|
cmd = CMD.Z;
|
path.addData(cmd);
|
// z may be in the middle of the path.
|
cpx = subpathX;
|
cpy = subpathY;
|
}
|
|
prevCmd = cmd;
|
}
|
|
path.toStatic();
|
|
return path;
|
}
|
|
type SVGPathOption = Omit<PathProps, 'shape' | 'buildPath'>
|
interface InnerSVGPathOption extends PathProps {
|
applyTransform?: (m: MatrixArray) => void
|
}
|
class SVGPath extends Path {
|
applyTransform(m: MatrixArray) {}
|
}
|
|
function isPathProxy(path: PathProxy | CanvasRenderingContext2D): path is PathProxy {
|
return (path as PathProxy).setData != null;
|
}
|
// TODO Optimize double memory cost problem
|
function createPathOptions(str: string, opts: SVGPathOption): InnerSVGPathOption {
|
const pathProxy = createPathProxyFromString(str);
|
const innerOpts: InnerSVGPathOption = extend({}, opts);
|
innerOpts.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
|
if (isPathProxy(path)) {
|
path.setData(pathProxy.data);
|
// Svg and vml renderer don't have context
|
const ctx = path.getContext();
|
if (ctx) {
|
path.rebuildPath(ctx, 1);
|
}
|
}
|
else {
|
const ctx = path;
|
pathProxy.rebuildPath(ctx, 1);
|
}
|
};
|
|
innerOpts.applyTransform = function (this: SVGPath, m: MatrixArray) {
|
transformPath(pathProxy, m);
|
this.dirtyShape();
|
};
|
|
return innerOpts;
|
}
|
|
/**
|
* Create a Path object from path string data
|
* http://www.w3.org/TR/SVG/paths.html#PathData
|
* @param opts Other options
|
*/
|
export function createFromString(str: string, opts?: SVGPathOption): SVGPath {
|
// PENDING
|
return new SVGPath(createPathOptions(str, opts));
|
}
|
|
/**
|
* Create a Path class from path string data
|
* @param str
|
* @param opts Other options
|
*/
|
export function extendFromString(str: string, defaultOpts?: SVGPathOption): typeof SVGPath {
|
const innerOpts = createPathOptions(str, defaultOpts);
|
class Sub extends SVGPath {
|
constructor(opts: InnerSVGPathOption) {
|
super(opts);
|
this.applyTransform = innerOpts.applyTransform;
|
this.buildPath = innerOpts.buildPath;
|
}
|
}
|
return Sub;
|
}
|
|
/**
|
* Merge multiple paths
|
*/
|
// TODO Apply transform
|
// TODO stroke dash
|
// TODO Optimize double memory cost problem
|
export function mergePath(pathEls: Path[], opts: PathProps) {
|
const pathList: PathProxy[] = [];
|
const len = pathEls.length;
|
for (let i = 0; i < len; i++) {
|
const pathEl = pathEls[i];
|
pathList.push(pathEl.getUpdatedPathProxy(true));
|
}
|
|
const pathBundle = new Path(opts);
|
// Need path proxy.
|
pathBundle.createPathProxy();
|
pathBundle.buildPath = function (path: PathProxy | CanvasRenderingContext2D) {
|
if (isPathProxy(path)) {
|
path.appendPath(pathList);
|
// Svg and vml renderer don't have context
|
const ctx = path.getContext();
|
if (ctx) {
|
// Path bundle not support percent draw.
|
path.rebuildPath(ctx, 1);
|
}
|
}
|
};
|
|
return pathBundle;
|
}
|
|
/**
|
* Clone a path.
|
*/
|
export function clonePath(sourcePath: Path, opts?: {
|
/**
|
* If bake global transform to path.
|
*/
|
bakeTransform?: boolean
|
/**
|
* Convert global transform to local.
|
*/
|
toLocal?: boolean
|
}) {
|
opts = opts || {};
|
const path = new Path();
|
if (sourcePath.shape) {
|
path.setShape(sourcePath.shape);
|
}
|
path.setStyle(sourcePath.style);
|
|
if (opts.bakeTransform) {
|
transformPath(path.path, sourcePath.getComputedTransform());
|
}
|
else {
|
// TODO Copy getLocalTransform, updateTransform since they can be changed.
|
if (opts.toLocal) {
|
path.setLocalTransform(sourcePath.getComputedTransform());
|
}
|
else {
|
path.copyTransform(sourcePath);
|
}
|
}
|
|
// These methods may be overridden
|
path.buildPath = sourcePath.buildPath;
|
(path as SVGPath).applyTransform = (path as SVGPath).applyTransform;
|
|
path.z = sourcePath.z;
|
path.z2 = sourcePath.z2;
|
path.zlevel = sourcePath.zlevel;
|
|
return path;
|
}
|