liusuyi
2023-04-24 4737f1e038743ced243c9e52423404d9034d6107
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { PathRebuilder } from '../core/PathProxy';
import { isAroundZero } from './helper';
 
const mathSin = Math.sin;
const mathCos = Math.cos;
const PI = Math.PI;
const PI2 = Math.PI * 2;
const degree = 180 / PI;
 
 
export default class SVGPathRebuilder implements PathRebuilder {
    private _d: (string | number)[]
    private _str: string
    private _invalid: boolean
 
    // If is start of subpath
    private _start: boolean
    private _p: number
 
    reset(precision?: number) {
        this._start = true;
        this._d = [];
        this._str = '';
 
        this._p = Math.pow(10, precision || 4);
    }
    moveTo(x: number, y: number) {
        this._add('M', x, y);
    }
    lineTo(x: number, y: number) {
        this._add('L', x, y);
    }
    bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number) {
        this._add('C', x, y, x2, y2, x3, y3);
    }
    quadraticCurveTo(x: number, y: number, x2: number, y2: number) {
        this._add('Q', x, y, x2, y2);
    }
    arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean) {
        this.ellipse(cx, cy, r, r, 0, startAngle, endAngle, anticlockwise);
    }
    ellipse(
        cx: number, cy: number,
        rx: number, ry: number,
        psi: number,
        startAngle: number,
        endAngle: number,
        anticlockwise: boolean
    ) {
        let dTheta = endAngle - startAngle;
        const clockwise = !anticlockwise;
 
        const dThetaPositive = Math.abs(dTheta);
        const isCircle = isAroundZero(dThetaPositive - PI2)
            || (clockwise ? dTheta >= PI2 : -dTheta >= PI2);
 
        // Mapping to 0~2PI
        const unifiedTheta = dTheta > 0 ? dTheta % PI2 : (dTheta % PI2 + PI2);
 
        let large = false;
        if (isCircle) {
            large = true;
        }
        else if (isAroundZero(dThetaPositive)) {
            large = false;
        }
        else {
            large = (unifiedTheta >= PI) === !!clockwise;
        }
 
        const x0 = cx + rx * mathCos(startAngle);
        const y0 = cy + ry * mathSin(startAngle);
 
        if (this._start) {
            // Move to (x0, y0) only when CMD.A comes at the
            // first position of a shape.
            // For instance, when drawing a ring, CMD.A comes
            // after CMD.M, so it's unnecessary to move to
            // (x0, y0).
            this._add('M', x0, y0);
        }
 
        const xRot = Math.round(psi * degree);
        // It will not draw if start point and end point are exactly the same
        // We need to add two arcs
        if (isCircle) {
            const p = 1 / this._p;
            const dTheta = (clockwise ? 1 : -1) * (PI2 - p);
            this._add(
                'A', rx, ry, xRot, 1, +clockwise,
                cx + rx * mathCos(startAngle + dTheta),
                cy + ry * mathSin(startAngle + dTheta)
            );
            // TODO.
            // Usually we can simply divide the circle into two halfs arcs.
            // But it will cause slightly diff with previous screenshot.
            // We can't tell it but visual regression test can. To avoid too much breaks.
            // We keep the logic on the browser as before.
            // But in SSR mode wich has lower precision. We close the circle by adding another arc.
            if (p > 1e-2) {
                this._add('A', rx, ry, xRot, 0, +clockwise, x0, y0);
            }
        }
        else {
            const x = cx + rx * mathCos(endAngle);
            const y = cy + ry * mathSin(endAngle);
 
            // FIXME Ellipse
            this._add('A', rx, ry, xRot, +large, +clockwise, x, y);
        }
 
    }
    rect(x: number, y: number, w: number, h: number) {
        this._add('M', x, y);
        // Use relative coordinates to reduce the size.
        this._add('l', w, 0);
        this._add('l', 0, h);
        this._add('l', -w, 0);
        // this._add('L', x, y);
        this._add('Z');
    }
    closePath() {
        // Not use Z as first command
        if (this._d.length > 0) {
            this._add('Z');
        }
    }
 
    _add(cmd: string, a?: number, b?: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number) {
        const vals = [];
        const p = this._p;
        for (let i = 1; i < arguments.length; i++) {
            const val = arguments[i];
            if (isNaN(val)) {
                this._invalid = true;
                return;
            }
            vals.push(Math.round(val * p) / p);
        }
        this._d.push(cmd + vals.join(' '));
        this._start = cmd === 'Z';
    }
 
    generateStr() {
        this._str = this._invalid ? '' : this._d.join('');
        this._d = [];
    }
    getStr() {
        return this._str;
    }
}