import { cubicSubdivide } from '../core/curve'; 
 | 
import PathProxy from '../core/PathProxy'; 
 | 
  
 | 
const CMD = PathProxy.CMD; 
 | 
  
 | 
function aroundEqual(a: number, b: number) { 
 | 
    return Math.abs(a - b) < 1e-5; 
 | 
} 
 | 
  
 | 
export function pathToBezierCurves(path: PathProxy) { 
 | 
  
 | 
    const data = path.data; 
 | 
    const len = path.len(); 
 | 
  
 | 
    const bezierArrayGroups: number[][] = []; 
 | 
    let currentSubpath: number[]; 
 | 
  
 | 
    let xi = 0; 
 | 
    let yi = 0; 
 | 
    let x0 = 0; 
 | 
    let y0 = 0; 
 | 
  
 | 
    function createNewSubpath(x: number, y: number) { 
 | 
        // More than one M command 
 | 
        if (currentSubpath && currentSubpath.length > 2) { 
 | 
            bezierArrayGroups.push(currentSubpath); 
 | 
        } 
 | 
        currentSubpath = [x, y]; 
 | 
    } 
 | 
  
 | 
    function addLine(x0: number, y0: number, x1: number, y1: number) { 
 | 
        if (!(aroundEqual(x0, x1) && aroundEqual(y0, y1))) { 
 | 
            currentSubpath.push(x0, y0, x1, y1, x1, y1); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    function addArc(startAngle: number, endAngle: number, cx: number, cy: number, rx: number, ry: number) { 
 | 
        // https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves 
 | 
        const delta = Math.abs(endAngle - startAngle); 
 | 
        const len = Math.tan(delta / 4) * 4 / 3; 
 | 
        const dir = endAngle < startAngle ? -1 : 1; 
 | 
  
 | 
        const c1 = Math.cos(startAngle); 
 | 
        const s1 = Math.sin(startAngle); 
 | 
        const c2 = Math.cos(endAngle); 
 | 
        const s2 = Math.sin(endAngle); 
 | 
  
 | 
        const x1 = c1 * rx + cx; 
 | 
        const y1 = s1 * ry + cy; 
 | 
  
 | 
        const x4 = c2 * rx + cx; 
 | 
        const y4 = s2 * ry + cy; 
 | 
  
 | 
        const hx = rx * len * dir; 
 | 
        const hy = ry * len * dir; 
 | 
        currentSubpath.push( 
 | 
            // Move control points on tangent. 
 | 
            x1 - hx * s1, y1 + hy * c1, 
 | 
            x4 + hx * s2, y4 - hy * c2, 
 | 
            x4, y4 
 | 
        ); 
 | 
    } 
 | 
  
 | 
    let x1; 
 | 
    let y1; 
 | 
    let x2; 
 | 
    let y2; 
 | 
  
 | 
    for (let i = 0; i < len;) { 
 | 
        const cmd = data[i++]; 
 | 
        const isFirst = i === 1; 
 | 
  
 | 
        if (isFirst) { 
 | 
            // 如果第一个命令是 L, C, Q 
 | 
            // 则 previous point 同绘制命令的第一个 point 
 | 
            // 第一个命令为 Arc 的情况下会在后面特殊处理 
 | 
            xi = data[i]; 
 | 
            yi = data[i + 1]; 
 | 
  
 | 
            x0 = xi; 
 | 
            y0 = yi; 
 | 
  
 | 
            if (cmd === CMD.L || cmd === CMD.C || cmd === CMD.Q) { 
 | 
                // Start point 
 | 
                currentSubpath = [x0, y0]; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        switch (cmd) { 
 | 
            case CMD.M: 
 | 
                // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 
 | 
                // 在 closePath 的时候使用 
 | 
                xi = x0 = data[i++]; 
 | 
                yi = y0 = data[i++]; 
 | 
  
 | 
                createNewSubpath(x0, y0); 
 | 
                break; 
 | 
            case CMD.L: 
 | 
                x1 = data[i++]; 
 | 
                y1 = data[i++]; 
 | 
                addLine(xi, yi, x1, y1); 
 | 
                xi = x1; 
 | 
                yi = y1; 
 | 
                break; 
 | 
            case CMD.C: 
 | 
                currentSubpath.push( 
 | 
                    data[i++], data[i++], data[i++], data[i++], 
 | 
                    xi = data[i++], yi = data[i++] 
 | 
                ); 
 | 
                break; 
 | 
            case CMD.Q: 
 | 
                x1 = data[i++]; 
 | 
                y1 = data[i++]; 
 | 
                x2 = data[i++]; 
 | 
                y2 = data[i++]; 
 | 
                currentSubpath.push( 
 | 
                    // Convert quadratic to cubic 
 | 
                    xi + 2 / 3 * (x1 - xi), yi + 2 / 3 * (y1 - yi), 
 | 
                    x2 + 2 / 3 * (x1 - x2), y2 + 2 / 3 * (y1 - y2), 
 | 
                    x2, y2 
 | 
                ); 
 | 
                xi = x2; 
 | 
                yi = y2; 
 | 
                break; 
 | 
            case CMD.A: 
 | 
                const cx = data[i++]; 
 | 
                const cy = data[i++]; 
 | 
                const rx = data[i++]; 
 | 
                const ry = data[i++]; 
 | 
                const startAngle = data[i++]; 
 | 
                const endAngle = data[i++] + startAngle; 
 | 
  
 | 
                // TODO Arc rotation 
 | 
                i += 1; 
 | 
                const anticlockwise = !data[i++]; 
 | 
  
 | 
                x1 = Math.cos(startAngle) * rx + cx; 
 | 
                y1 = Math.sin(startAngle) * ry + cy; 
 | 
                if (isFirst) { 
 | 
                    // 直接使用 arc 命令 
 | 
                    // 第一个命令起点还未定义 
 | 
                    x0 = x1; 
 | 
                    y0 = y1; 
 | 
                    createNewSubpath(x0, y0); 
 | 
                } 
 | 
                else { 
 | 
                    // Connect a line between current point to arc start point. 
 | 
                    addLine(xi, yi, x1, y1); 
 | 
                } 
 | 
  
 | 
                xi = Math.cos(endAngle) * rx + cx; 
 | 
                yi = Math.sin(endAngle) * ry + cy; 
 | 
  
 | 
                const step = (anticlockwise ? -1 : 1) * Math.PI / 2; 
 | 
  
 | 
                for (let angle = startAngle; anticlockwise ? angle > endAngle : angle < endAngle; angle += step) { 
 | 
                    const nextAngle = anticlockwise ? Math.max(angle + step, endAngle) 
 | 
                        : Math.min(angle + step, endAngle); 
 | 
                    addArc(angle, nextAngle, cx, cy, rx, ry); 
 | 
                } 
 | 
  
 | 
                break; 
 | 
            case CMD.R: 
 | 
                x0 = xi = data[i++]; 
 | 
                y0 = yi = data[i++]; 
 | 
                x1 = x0 + data[i++]; 
 | 
                y1 = y0 + data[i++]; 
 | 
  
 | 
                // rect is an individual path. 
 | 
                createNewSubpath(x1, y0); 
 | 
                addLine(x1, y0, x1, y1); 
 | 
                addLine(x1, y1, x0, y1); 
 | 
                addLine(x0, y1, x0, y0); 
 | 
                addLine(x0, y0, x1, y0); 
 | 
                break; 
 | 
            case CMD.Z: 
 | 
                currentSubpath && addLine(xi, yi, x0, y0); 
 | 
                xi = x0; 
 | 
                yi = y0; 
 | 
                break; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    if (currentSubpath && currentSubpath.length > 2) { 
 | 
        bezierArrayGroups.push(currentSubpath); 
 | 
    } 
 | 
  
 | 
    return bezierArrayGroups; 
 | 
} 
 | 
  
 | 
function adpativeBezier( 
 | 
    x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, 
 | 
    out: number[], scale: number 
 | 
) { 
 | 
    // This bezier is used to simulates a line when converting path to beziers. 
 | 
    if (aroundEqual(x0, x1) && aroundEqual(y0, y1) && aroundEqual(x2, x3) && aroundEqual(y2, y3)) { 
 | 
        out.push(x3, y3); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    const PIXEL_DISTANCE = 2 / scale; 
 | 
    const PIXEL_DISTANCE_SQR = PIXEL_DISTANCE * PIXEL_DISTANCE; 
 | 
  
 | 
    // Determine if curve is straight enough 
 | 
    let dx = x3 - x0; 
 | 
    let dy = y3 - y0; 
 | 
    const d = Math.sqrt(dx * dx + dy * dy); 
 | 
    dx /= d; 
 | 
    dy /= d; 
 | 
  
 | 
    const dx1 = x1 - x0; 
 | 
    const dy1 = y1 - y0; 
 | 
    const dx2 = x2 - x3; 
 | 
    const dy2 = y2 - y3; 
 | 
  
 | 
    const cp1LenSqr = dx1 * dx1 + dy1 * dy1; 
 | 
    const cp2LenSqr = dx2 * dx2 + dy2 * dy2; 
 | 
  
 | 
    if (cp1LenSqr < PIXEL_DISTANCE_SQR && cp2LenSqr < PIXEL_DISTANCE_SQR) { 
 | 
        // Add small segment 
 | 
        out.push(x3, y3); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    // Project length of cp1 
 | 
    const projLen1 = dx * dx1 + dy * dy1; 
 | 
    // Project length of cp2 
 | 
    const projLen2 = -dx * dx2 - dy * dy2; 
 | 
  
 | 
    // Distance from cp1 to start-end line. 
 | 
    const d1Sqr = cp1LenSqr - projLen1 * projLen1; 
 | 
    // Distance from cp2 to start-end line. 
 | 
    const d2Sqr = cp2LenSqr - projLen2 * projLen2; 
 | 
  
 | 
    // IF the cp1 and cp2 is near to the start-line enough 
 | 
    // We treat it straight enough 
 | 
    if (d1Sqr < PIXEL_DISTANCE_SQR && projLen1 >= 0 
 | 
        && d2Sqr < PIXEL_DISTANCE_SQR && projLen2 >= 0 
 | 
    ) { 
 | 
        out.push(x3, y3); 
 | 
        return; 
 | 
    } 
 | 
  
 | 
  
 | 
    const tmpSegX: number[] = []; 
 | 
    const tmpSegY: number[] = []; 
 | 
    // Subdivide 
 | 
    cubicSubdivide(x0, x1, x2, x3, 0.5, tmpSegX); 
 | 
    cubicSubdivide(y0, y1, y2, y3, 0.5, tmpSegY); 
 | 
  
 | 
    adpativeBezier( 
 | 
        tmpSegX[0], tmpSegY[0], tmpSegX[1], tmpSegY[1], tmpSegX[2], tmpSegY[2], tmpSegX[3], tmpSegY[3], 
 | 
        out, scale 
 | 
    ); 
 | 
    adpativeBezier( 
 | 
        tmpSegX[4], tmpSegY[4], tmpSegX[5], tmpSegY[5], tmpSegX[6], tmpSegY[6], tmpSegX[7], tmpSegY[7], 
 | 
        out, scale 
 | 
    ); 
 | 
} 
 | 
  
 | 
export function pathToPolygons(path: PathProxy, scale?: number) { 
 | 
    // TODO Optimize simple case like path is polygon and rect? 
 | 
    const bezierArrayGroups = pathToBezierCurves(path); 
 | 
  
 | 
    const polygons: number[][] = []; 
 | 
  
 | 
    scale = scale || 1; 
 | 
  
 | 
    for (let i = 0; i < bezierArrayGroups.length; i++) { 
 | 
        const beziers = bezierArrayGroups[i]; 
 | 
        const polygon: number[] = []; 
 | 
        let x0 = beziers[0]; 
 | 
        let y0 = beziers[1]; 
 | 
  
 | 
        polygon.push(x0, y0); 
 | 
  
 | 
        for (let k = 2; k < beziers.length;) { 
 | 
  
 | 
            const x1 = beziers[k++]; 
 | 
            const y1 = beziers[k++]; 
 | 
            const x2 = beziers[k++]; 
 | 
            const y2 = beziers[k++]; 
 | 
            const x3 = beziers[k++]; 
 | 
            const y3 = beziers[k++]; 
 | 
  
 | 
            adpativeBezier(x0, y0, x1, y1, x2, y2, x3, y3, polygon, scale); 
 | 
  
 | 
            x0 = x3; 
 | 
            y0 = y3; 
 | 
        } 
 | 
  
 | 
        polygons.push(polygon); 
 | 
    } 
 | 
    return polygons; 
 | 
} 
 |