File size: 6,367 Bytes
04f98c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { SVGPathData } from "./SVGPathData";
import { CommandA, CommandC } from "./types";

export function rotate([x, y]: [number, number], rad: number) {
  return [
    x * Math.cos(rad) - y * Math.sin(rad),
    x * Math.sin(rad) + y * Math.cos(rad),
  ];
}

const DEBUG_CHECK_NUMBERS = true;
export function assertNumbers(...numbers: number[]) {
  if (DEBUG_CHECK_NUMBERS) {
    for (let i = 0; i < numbers.length; i++) {
      if ("number" !== typeof numbers[i]) {
        throw new Error(
          `assertNumbers arguments[${i}] is not a number. ${typeof numbers[i]} == typeof ${numbers[i]}`);
      }
    }
  }
  return true;
}

const PI = Math.PI;

/**
 * https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
 * Fixes rX and rY.
 * Ensures lArcFlag and sweepFlag are 0 or 1
 * Adds center coordinates: command.cX, command.cY (relative or absolute, depending on command.relative)
 * Adds start and end arc parameters (in degrees): command.phi1, command.phi2; phi1 < phi2 iff. c.sweepFlag == true
 */
export function annotateArcCommand(c: CommandA, x1: number, y1: number) {
  c.lArcFlag = (0 === c.lArcFlag) ? 0 : 1;
  c.sweepFlag = (0 === c.sweepFlag) ? 0 : 1;
  // tslint:disable-next-line
  let {rX, rY, x, y} = c;

  rX = Math.abs(c.rX);
  rY = Math.abs(c.rY);
  const [x1_, y1_] = rotate([(x1 - x) / 2, (y1 - y) / 2], -c.xRot / 180 * PI);
  const testValue = Math.pow(x1_, 2) / Math.pow(rX, 2) + Math.pow(y1_, 2) / Math.pow(rY, 2);

  if (1 < testValue) {
    rX *= Math.sqrt(testValue);
    rY *= Math.sqrt(testValue);
  }
  c.rX = rX;
  c.rY = rY;
  const c_ScaleTemp = (Math.pow(rX, 2) * Math.pow(y1_, 2) + Math.pow(rY, 2) * Math.pow(x1_, 2));
  const c_Scale = (c.lArcFlag !== c.sweepFlag ? 1 : -1) *
    Math.sqrt(Math.max(0, (Math.pow(rX, 2) * Math.pow(rY, 2) - c_ScaleTemp) / c_ScaleTemp));
  const cx_ = rX * y1_ / rY * c_Scale;
  const cy_ = -rY * x1_ / rX * c_Scale;
  const cRot = rotate([cx_, cy_], c.xRot / 180 * PI);

  c.cX = cRot[0] + (x1 + x) / 2;
  c.cY = cRot[1] + (y1 + y) / 2;
  c.phi1 = Math.atan2((y1_ - cy_) / rY, (x1_ - cx_) / rX);
  c.phi2 = Math.atan2((-y1_ - cy_) / rY, (-x1_ - cx_) / rX);
  if (0 === c.sweepFlag && c.phi2 > c.phi1) {
    c.phi2 -= 2 * PI;
  }
  if (1 === c.sweepFlag && c.phi2 < c.phi1) {
    c.phi2 += 2 * PI;
  }
  c.phi1 *= 180 / PI;
  c.phi2 *= 180 / PI;
}

/**
 * Solves a quadratic system of equations of the form
 *      a * x + b * y = c
 *      x² + y² = 1
 * This can be understood as the intersection of the unit circle with a line.
 *      => y = (c - a x) / b
 *      => x² + (c - a x)² / b² = 1
 *      => x² b² + c² - 2 c a x + a² x² = b²
 *      => (a² + b²) x² - 2 a c x + (c² - b²) = 0
 */
export function intersectionUnitCircleLine(a: number, b: number, c: number): [number, number][] {
  assertNumbers(a, b, c);
  // cf. pqFormula
  const termSqr = a * a + b * b - c * c;

  if (0 > termSqr) {
    return [];
  } else if (0 === termSqr) {
    return [
      [
        (a * c) / (a * a + b * b),
        (b * c) / (a * a + b * b)]];
  }
  const term = Math.sqrt(termSqr);

  return [
    [
      (a * c + b * term) / (a * a + b * b),
      (b * c - a * term) / (a * a + b * b)],
    [
      (a * c - b * term) / (a * a + b * b),
      (b * c + a * term) / (a * a + b * b)]];

}

export const DEG = Math.PI / 180;

export function lerp(a: number, b: number, t: number) {
  return (1 - t) * a + t * b;
}

export function arcAt(c: number, x1: number, x2: number, phiDeg: number) {
  return c + Math.cos(phiDeg / 180 * PI) * x1 + Math.sin(phiDeg / 180 * PI) * x2;
}

export function bezierRoot(x0: number, x1: number, x2: number, x3: number) {
  const EPS = 1e-6;
  const x01 = x1 - x0;
  const x12 = x2 - x1;
  const x23 = x3 - x2;
  const a = 3 * x01 + 3 * x23 - 6 * x12;
  const b = (x12 - x01) * 6;
  const c = 3 * x01;
  // solve a * t² + b * t + c = 0

  if (Math.abs(a) < EPS) {
    // equivalent to b * t + c =>
    return [-c / b];
  }
  return pqFormula(b / a, c / a, EPS);

}

export function bezierAt(x0: number, x1: number, x2: number, x3: number, t: number) {
  // console.log(x0, y0, x1, y1, x2, y2, x3, y3, t)
  const s = 1 - t;
  const c0 = s * s * s;
  const c1 = 3 * s * s * t;
  const c2 = 3 * s * t * t;
  const c3 = t * t * t;

  return x0 * c0 + x1 * c1 + x2 * c2 + x3 * c3;
}

function pqFormula(p: number, q: number, PRECISION = 1e-6) {
  // 4 times the discriminant:in
  const discriminantX4 = p * p / 4 - q;

  if (discriminantX4 < -PRECISION) {
    return [];
  } else if (discriminantX4 <= PRECISION) {
    return [-p / 2];
  }
  const root = Math.sqrt(discriminantX4);

  return [-(p / 2) - root, -(p / 2) + root];

}

export function a2c(arc: CommandA, x0: number, y0: number): CommandC[] {
  if (!arc.cX) {
    annotateArcCommand(arc, x0, y0);
  }

  const phiMin = Math.min(arc.phi1!, arc.phi2!), phiMax = Math.max(arc.phi1!, arc.phi2!), deltaPhi = phiMax - phiMin;
  const partCount = Math.ceil(deltaPhi / 90 );

  const result: CommandC[] = new Array(partCount);
  let prevX = x0, prevY = y0;
  for (let i = 0; i < partCount; i++) {
    const phiStart = lerp(arc.phi1!, arc.phi2!, i / partCount);
    const phiEnd = lerp(arc.phi1!, arc.phi2!, (i + 1) / partCount);
    const deltaPhi = phiEnd - phiStart;
    const f = 4 / 3 * Math.tan(deltaPhi * DEG / 4);
    // x1/y1, x2/y2 and x/y coordinates on the unit circle for phiStart/phiEnd
    const [x1, y1] = [
      Math.cos(phiStart * DEG) - f * Math.sin(phiStart * DEG),
      Math.sin(phiStart * DEG) + f * Math.cos(phiStart * DEG)];
    const [x, y] = [Math.cos(phiEnd * DEG), Math.sin(phiEnd * DEG)];
    const [x2, y2] = [x + f * Math.sin(phiEnd * DEG), y - f * Math.cos(phiEnd * DEG)];
    result[i] = {relative: arc.relative, type: SVGPathData.CURVE_TO } as any;
    const transform = (x: number, y: number) => {
      const [xTemp, yTemp] = rotate([x * arc.rX, y * arc.rY], arc.xRot);
      return [arc.cX! + xTemp, arc.cY! + yTemp];
    };
    [result[i].x1, result[i].y1] = transform(x1, y1);
    [result[i].x2, result[i].y2] = transform(x2, y2);
    [result[i].x, result[i].y] = transform(x, y);
    if (arc.relative) {
      result[i].x1 -= prevX;
      result[i].y1 -= prevY;
      result[i].x2 -= prevX;
      result[i].y2 -= prevY;
      result[i].x -= prevX;
      result[i].y -= prevY;
    }
    [prevX, prevY] = [result[i].x, result[i].y];
  }
  return result;
}