| 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; | 
| } |