pimath 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/pimath.js +856 -1882
  2. package/dist/pimath.js.map +1 -1
  3. package/package.json +2 -2
  4. package/src/algebra/equation.ts +562 -558
  5. package/src/algebra/equationSolver.ts +489 -489
  6. package/src/algebra/factor.ts +396 -392
  7. package/src/algebra/linearSystem.ts +446 -442
  8. package/src/algebra/matrix.ts +515 -511
  9. package/src/algebra/monom.ts +932 -928
  10. package/src/algebra/polyFactor.ts +724 -720
  11. package/src/algebra/polynom.ts +1233 -1229
  12. package/src/coefficients/fraction.ts +907 -748
  13. package/src/coefficients/root.ts +500 -359
  14. package/src/geometry/TupleN.ts +129 -129
  15. package/src/geometry/circle.ts +455 -455
  16. package/src/geometry/line.ts +658 -654
  17. package/src/geometry/point.ts +105 -105
  18. package/src/geometry/sphere3.ts +2 -2
  19. package/src/geometry/triangle.ts +488 -488
  20. package/src/geometry/vector.ts +230 -226
  21. package/src/numeric.ts +136 -51
  22. package/src/pimath.interface.ts +2 -0
  23. package/types/algebra/equation.d.ts +1 -0
  24. package/types/algebra/equation.d.ts.map +1 -1
  25. package/types/algebra/equationSolver.d.ts.map +1 -1
  26. package/types/algebra/factor.d.ts +1 -0
  27. package/types/algebra/factor.d.ts.map +1 -1
  28. package/types/algebra/linearSystem.d.ts +1 -0
  29. package/types/algebra/linearSystem.d.ts.map +1 -1
  30. package/types/algebra/matrix.d.ts +6 -5
  31. package/types/algebra/matrix.d.ts.map +1 -1
  32. package/types/algebra/monom.d.ts +1 -0
  33. package/types/algebra/monom.d.ts.map +1 -1
  34. package/types/algebra/polyFactor.d.ts +1 -0
  35. package/types/algebra/polyFactor.d.ts.map +1 -1
  36. package/types/algebra/polynom.d.ts +1 -0
  37. package/types/algebra/polynom.d.ts.map +1 -1
  38. package/types/coefficients/fraction.d.ts +62 -53
  39. package/types/coefficients/fraction.d.ts.map +1 -1
  40. package/types/coefficients/root.d.ts +3 -0
  41. package/types/coefficients/root.d.ts.map +1 -1
  42. package/types/geometry/TupleN.d.ts +1 -1
  43. package/types/geometry/TupleN.d.ts.map +1 -1
  44. package/types/geometry/circle.d.ts.map +1 -1
  45. package/types/geometry/line.d.ts +1 -0
  46. package/types/geometry/line.d.ts.map +1 -1
  47. package/types/geometry/point.d.ts.map +1 -1
  48. package/types/geometry/sphere3.d.ts +2 -2
  49. package/types/geometry/sphere3.d.ts.map +1 -1
  50. package/types/geometry/triangle.d.ts.map +1 -1
  51. package/types/geometry/vector.d.ts +1 -0
  52. package/types/geometry/vector.d.ts.map +1 -1
  53. package/types/index.d.ts +3 -3
  54. package/types/numeric.d.ts +4 -4
  55. package/types/numeric.d.ts.map +1 -1
  56. package/types/pimath.interface.d.ts +1 -0
  57. package/types/pimath.interface.d.ts.map +1 -1
  58. package/types/primes.data.d.ts +2 -0
  59. package/types/primes.data.d.ts.map +1 -0
@@ -1,456 +1,456 @@
1
- import {Line} from "./line"
2
- import {Vector} from "./vector"
3
- import {Numeric} from "../numeric"
4
- import {Fraction, Root} from "../coefficients"
5
- import {Equation, Monom, Polynom} from "../algebra"
6
- import {type IPiMathObject} from "../pimath.interface"
7
- import {Point} from "./point"
8
- import {Triangle} from "./triangle"
9
- import {Solution} from "../analyze"
10
-
11
- enum CIRCLE_DISPLAY {
12
- CANONICAL,
13
- CENTER_RADIUS,
14
- }
15
-
16
- export class Circle
17
- implements IPiMathObject<Circle> {
18
-
19
- #center: Point | null = null
20
- #equation: Equation | null = null
21
- #output_style: CIRCLE_DISPLAY = CIRCLE_DISPLAY.CENTER_RADIUS
22
- #squareRadius: Fraction | null = null
23
-
24
- constructor()
25
- constructor(equation: string | Equation)
26
- constructor(circle: Circle)
27
- constructor(center: Point, radius: Fraction | number, square?: boolean)
28
- constructor(center: Point, pointThrough: Point)
29
- constructor(A: Point, B: Point, C: Point)
30
- constructor(...values: unknown[]) {
31
- if (values.length > 0) {
32
- this.parse(...values)
33
- }
34
- }
35
-
36
- parse(...values: unknown[]): this {
37
- // Data can be given in these formats:
38
- // one value, a string -> make it an Equation
39
- // one value, an Equation
40
- // one value, a circle -> clone it
41
- // two values: two points (center and pointThrough)
42
- // two values: point and Fraction (center and radius)
43
- // three values: Vector2D, Fraction, Boolean (center, square radius, true)
44
-
45
- if (typeof values[0] === 'string') {
46
- return this.fromString(values[0])
47
- }
48
-
49
- if (values[0] instanceof Equation) {
50
- return this.fromEquation(values[0])
51
- }
52
-
53
- if (values[0] instanceof Circle) {
54
- return this.copy(values[0])
55
- }
56
-
57
- if (values.length === 2 &&
58
- values[0] instanceof Point && values[1] instanceof Point
59
- ) {
60
- // Circle center through one point
61
- return this.fromCenterPoint(values[0], values[1])
62
- }
63
-
64
- if (values.length >= 2 && values[0]
65
- instanceof Point &&
66
- (values[1] instanceof Fraction || typeof values[1] === 'number')
67
- ) {
68
- // Circle center through one point
69
- return this.fromCenterRadius(
70
- values[0], values[1],
71
- (typeof values[2] === "boolean") ? values[2] : false
72
- )
73
- }
74
-
75
- return this
76
- }
77
-
78
- clone(): Circle {
79
- return new Circle().fromCenterRadius(
80
- this.center.clone(),
81
- this.squareRadius.clone(),
82
- true
83
- )
84
- }
85
-
86
- copy(circle: Circle): this {
87
- this.#center = circle.center.clone()
88
- this.#squareRadius = circle.squareRadius.clone()
89
-
90
- this.#calculateCartesian()
91
-
92
- return this
93
- }
94
-
95
- get tex(): string {
96
- if (this.#output_style === CIRCLE_DISPLAY.CANONICAL) {
97
- return this.equation.moveLeft().reduce().tex
98
- }
99
-
100
- let cx, cy
101
- if (this.center.x.isZero()) {
102
- cx = 'x^2'
103
- } else {
104
- cx = `\\left(x${this.center.x.isNegative() ? '+' : '-'}${this.center.x.clone().abs().tex}\\right)^2`
105
- }
106
- if (this.center.y.isZero()) {
107
- cy = 'y^2'
108
- } else {
109
- cy = `\\left(y${this.center.y.isNegative() ? '+' : '-'}${this.center.y.clone().abs().tex}\\right)^2`
110
- }
111
- return `${cx}+${cy}=${this.squareRadius.tex}`
112
- }
113
-
114
- get display(): string {
115
- if (this.#output_style === CIRCLE_DISPLAY.CANONICAL) {
116
- return this.equation.moveLeft().reduce().display
117
- }
118
-
119
- let cx, cy
120
- if (this.center.x.isZero()) {
121
- cx = 'x^2'
122
- } else {
123
- cx = `(x${this.center.x.isNegative() ? '+' : '-'}${this.center.x.clone().abs().tex})^2`
124
- }
125
- if (this.center.y.isZero()) {
126
- cy = 'y^2'
127
- } else {
128
- cy = `(y${this.center.y.isNegative() ? '+' : '-'}${this.center.y.clone().abs().tex})^2`
129
- }
130
- return `${cx}+${cy}=${this.squareRadius.display}`
131
- }
132
-
133
- get asCanonical(): this {
134
- this.#output_style = CIRCLE_DISPLAY.CANONICAL
135
- return this
136
- }
137
-
138
- get asCenterRadius(): this {
139
- this.#output_style = CIRCLE_DISPLAY.CENTER_RADIUS
140
- return this
141
- }
142
-
143
- get center(): Point {
144
- return this.#center ?? new Point()
145
- }
146
-
147
- get equation(): Equation {
148
- return this.#equation?.clone() ?? new Equation('0=0')
149
- }
150
-
151
- fromCenterPoint(center: Point, pointThrough: Point): this {
152
- this.#center = center.clone()
153
- this.#squareRadius = new Vector(this.#center, pointThrough).normSquare
154
-
155
- this.#calculateCartesian()
156
-
157
- return this
158
- }
159
-
160
- fromCenterRadius(center: Point, radius: Fraction | number, square?: boolean): this {
161
- this.#center = center.clone()
162
- if (square) {
163
- this.#squareRadius = (new Fraction(radius))
164
- } else {
165
- this.#squareRadius = new Fraction(radius).pow(2)
166
- }
167
-
168
- this.#calculateCartesian()
169
-
170
- return this
171
- }
172
-
173
- fromEquation(equ: Equation): this {
174
- // Move everything to the left.
175
- equ.moveLeft()
176
-
177
- if (equ.degree('x').value === 2 && equ.degree('y').value === 2) {
178
- // Both must be of degree 2.
179
- const x2 = equ.left.monomByDegree(2, 'x'), y2 = equ.left.monomByDegree(2, 'y')
180
- let x1: Monom, y1: Monom, c: Monom
181
-
182
- // Both square monoms must have the same coefficient.
183
- if (x2.coefficient.isEqual(y2.coefficient)) {
184
- equ.divide(x2.coefficient)
185
-
186
- x1 = equ.left.monomByDegree(1, 'x')
187
- y1 = equ.left.monomByDegree(1, 'y')
188
-
189
- c = equ.left.monomByDegree(0)
190
-
191
- this.#center = new Point(x1.coefficient.clone().divide(2).opposite(), y1.coefficient.clone().divide(2).opposite())
192
-
193
- this.#squareRadius = c.coefficient.clone().opposite()
194
- .add(this.#center.x.clone().pow(2))
195
- .add(this.#center.y.clone().pow(2))
196
-
197
- this.#calculateCartesian()
198
-
199
- return this
200
- } else {
201
- // The circle is not a valid circle
202
- this.#reset()
203
- }
204
- }
205
-
206
- return this
207
- }
208
-
209
- fromPoints(A: Point, B: Point, C: Point): this {
210
- const T = new Triangle(A, B, C)
211
-
212
- if (!T.isValid || !T.remarquables) {
213
- this.#reset()
214
- return this
215
- }
216
-
217
- const mAB = T.getMediators().c.clone()
218
- const mAC = T.getMediators().b.clone()
219
-
220
- return this.fromCenterPoint(
221
- mAB.intersection(mAC).point,
222
- A
223
- )
224
- }
225
-
226
- fromString(str: string): this {
227
- return this.fromEquation(new Equation(str))
228
- }
229
-
230
- getPointsOnCircle(): Point[] {
231
- // It means searching for pythagorician triples that make a perfect square.
232
- // (x-4)^2 + (y+3)^2 = 15
233
- const triplets = Numeric.pythagoreanTripletsWithTarget(this.squareRadius.value, true)
234
-
235
- const points: Point[] = []
236
-
237
- triplets.forEach(triplet => {
238
- // Allow positive / negative values
239
- // x-a = t => x = a + t
240
- // x-a = -t => x = a - t
241
- for (const k of [[1, 1], [-1, 1], [-1, -1], [1, -1]]) {
242
- // check if the point is not already here.
243
- const x = this.center.x.clone().add(k[0] * triplet[0])
244
- const y = this.center.y.clone().add(k[1] * triplet[1])
245
-
246
- if (points.every(pt => !pt.isEqualXY(x, y))) {
247
- points.push(new Point(x, y))
248
- }
249
-
250
- }
251
- })
252
- return points
253
- }
254
-
255
- isPointOnCircle = (P: Point): boolean => {
256
- return this.#equation?.test({x: P.x, y: P.y}) ?? false
257
- }
258
-
259
- public isSame(circ: Circle): boolean {
260
- return circ.center.isEqual(this.center)
261
- && circ.radius.isEqual(this.radius)
262
- }
263
-
264
- /**
265
- * Find the intersection points between the circle and a line. It can be 0, 1 or 2 points.
266
- * The points are sorted depending on the direction vector of the line.
267
- * @param L
268
- */
269
- lineIntersection(L: Line): Point[] {
270
- if (this.#equation === null) return []
271
-
272
- const center = this.center
273
- const d = L.d
274
- const OP = L.OA
275
-
276
- // A = dx^2+dy^2
277
- const A = d.normSquare
278
-
279
- // B = 2 ( dx (x0-cx) + dy(y0-cy) )
280
- const B = OP.x.clone().subtract(center.x).multiply(d.x)
281
- .add(OP.y.clone().subtract(center.y).multiply(d.y))
282
- .multiply(2)
283
-
284
- // C = (x0-cx)^2 + (y0-cy)^2 - r^2
285
- const C = OP.x.clone().subtract(center.x).pow(2)
286
- .add(OP.y.clone().subtract(center.y).pow(2))
287
- .subtract(this.squareRadius)
288
-
289
- const sol = Solution.fromQuadratic(A, B, C)
290
-
291
- if (sol.length === 0) return []
292
-
293
- // One intersection point
294
- if (sol.length === 1) { // means exact answer
295
- const OX = OP.add(d.clone().multiplyByScalar(sol[0].fraction))
296
- return [
297
- new Point(OX.x, OX.y)
298
- ]
299
- }
300
-
301
- // Two intersection points
302
- const OX1 = sol[0].exact
303
- ? OP.clone().add(d.clone().multiplyByScalar(sol[0].fraction))
304
- : OP.clone().add(d.clone().multiplyByScalar(sol[0].value))
305
-
306
- const OX2 = sol[1].exact
307
- ? OP.clone().add(d.clone().multiplyByScalar(sol[1].fraction))
308
- : OP.clone().add(d.clone().multiplyByScalar(sol[1].value))
309
-
310
- // sort the points depending on the direction vector.
311
- return [
312
- new Point(OX1.x, OX1.y),
313
- new Point(OX2.x, OX2.y),
314
- ].sort((a, b) => {
315
- // <0 => a before
316
- if (d.x.isZero()) {
317
- return d.y.isPositive()
318
- ? a.y.value - b.y.value
319
- : b.y.value - a.y.value
320
- }
321
-
322
- return d.x.isPositive()
323
- ? a.x.value - b.x.value
324
- : b.x.value - a.x.value
325
- })
326
- }
327
-
328
- get radius(): Root {
329
- return new Root().from(2, this.#squareRadius ?? 0)
330
- }
331
-
332
- /**
333
- * Get the relative position between circle and line. It corresponds to the number of intersection.
334
- * @param {Line} L
335
- * @returns {number}
336
- */
337
- public relativePosition(L: Line): number {
338
- if (this.#center === null || this.#squareRadius === null) {
339
- return -1
340
- }
341
-
342
- const distance = L.distanceTo(this.#center).pow(2).value
343
- const radius = this.#squareRadius.value
344
-
345
- if (distance - radius > 0.0000000001) {
346
- return 0 // external
347
- }
348
-
349
- if (Math.abs(distance - radius) < 0.0000000001) {
350
- return 1 // tangent
351
- }
352
-
353
- return 2 // secant
354
- }
355
-
356
- setRadius(radius: Fraction | number, square?: boolean): this {
357
- if (square) {
358
- this.#squareRadius = new Fraction(radius)
359
- } else {
360
- this.#squareRadius = new Fraction(radius).pow(2)
361
- }
362
-
363
- this.#calculateCartesian()
364
- return this
365
- }
366
-
367
- get squareRadius(): Fraction {
368
- return this.#squareRadius?.clone() ?? new Fraction(-1)
369
- }
370
-
371
- tangents = (P: Point | Fraction): Line[] => {
372
- if (P instanceof Fraction) {
373
- return this.#tangentsWithSlope(P)
374
- }
375
-
376
- if (this.isPointOnCircle(P)) {
377
- return this.#tangentsThroughOnePointOnTheCircle(P)
378
- }
379
-
380
- if (this.#center !== null && this.#center.distanceTo(P).value > this.radius.value) {
381
- return this.#tangentsThroughOnePointOutsideTheCircle(P)
382
- }
383
-
384
- return []
385
- }
386
-
387
- #calculateCartesian(): void {
388
- this.#equation = (
389
- new Equation(
390
- new Polynom(`(x-(${this.center.x.display}))^2+(y-(${this.center.y.display}))^2`),
391
- new Polynom(this.squareRadius.display))
392
- ).moveLeft()
393
- }
394
-
395
- #reset() {
396
- this.#center = null
397
- this.#squareRadius = null
398
- this.#equation = null
399
- }
400
-
401
- #tangentsThroughOnePointOnTheCircle = (P: Point): Line[] => {
402
- const CT = new Vector(this.center, P)
403
- return [new Line().fromPointAndNormal(P, CT)]
404
- }
405
-
406
- #tangentsThroughOnePointOutsideTheCircle = (P: Point): Line[] => {
407
- // y = mx + h
408
- // px, py => h = -m px + py => mx - y -m.px + py = 0 =>
409
- // Centre: cx, cy, radius: r
410
- // (m.cx - cy -m.px + py)^2 = r^2 * (m^2 + 1)
411
- // (m(cx-py) - (cy - py))^2 = r^2 * (m^2 + 1)
412
-
413
- const cx_px = this.center.x.clone().subtract(P.x)
414
- const cy_py = this.center.y.clone().subtract(P.y)
415
- const polyLeft = new Polynom('x')
416
- const polyRight = new Polynom('x^2+1')
417
-
418
- polyLeft.multiply(cx_px).subtract(cy_py).pow(2)
419
- polyRight.multiply(this.squareRadius)
420
-
421
- const equ = new Equation(polyLeft, polyRight)
422
-
423
- const solutions = equ.solve()
424
-
425
- return solutions.map(sol => {
426
- // h = -m px + py
427
- let h: Fraction
428
- const equ = new Equation('y', 'x')
429
-
430
- if (sol.exact) {
431
- h = P.x.clone().opposite().multiply(sol.fraction).add(P.y)
432
- equ.right.multiply(sol.fraction).add(h)
433
- } else {
434
- h = P.x.clone().opposite().multiply(sol.value).add(P.y)
435
- equ.right.multiply(sol.value).add(h)
436
- }
437
-
438
- return new Line(equ)
439
- })
440
-
441
- }
442
-
443
- #tangentsWithSlope = (slope: Fraction): Line[] => {
444
- // d(C;t)=r => ac1+bc2 + x = +- sqrt(a^2 + b^2)*r
445
- // x = -ac1-bc2 +- sqrt(a^2 + b^2)*r
446
- // y = a/bx + h => ax-by + H = 0
447
-
448
- const a = slope.numerator, b = -slope.denominator, c1 = this.center.x.clone(), c2 = this.center.y.clone()
449
-
450
- const sq = this.squareRadius.clone().multiply(slope.numerator ** 2 + slope.denominator ** 2),
451
- x1 = c1.clone().multiply(a).opposite().subtract(c2.clone().multiply(b)).add(sq.clone().sqrt()),
452
- x2 = c1.clone().multiply(a).opposite().subtract(c2.clone().multiply(b)).subtract(sq.clone().sqrt())
453
-
454
- return [new Line(a, b, x1), new Line(a, b, x2)]
455
- }
1
+ import {Line} from "./line"
2
+ import {Vector} from "./vector"
3
+ import {Numeric} from "../numeric"
4
+ import {Fraction, Root} from "../coefficients"
5
+ import {Equation, Monom, Polynom} from "../algebra"
6
+ import {type IPiMathObject} from "../pimath.interface"
7
+ import {Point} from "./point"
8
+ import {Triangle} from "./triangle"
9
+ import {Solution} from "../analyze"
10
+
11
+ enum CIRCLE_DISPLAY {
12
+ CANONICAL,
13
+ CENTER_RADIUS,
14
+ }
15
+
16
+ export class Circle
17
+ implements IPiMathObject<Circle> {
18
+
19
+ #center: Point | null = null
20
+ #equation: Equation | null = null
21
+ #output_style: CIRCLE_DISPLAY = CIRCLE_DISPLAY.CENTER_RADIUS
22
+ #squareRadius: Fraction | null = null
23
+
24
+ constructor()
25
+ constructor(equation: string | Equation)
26
+ constructor(circle: Circle)
27
+ constructor(center: Point, radius: Fraction | number, square?: boolean)
28
+ constructor(center: Point, pointThrough: Point)
29
+ constructor(A: Point, B: Point, C: Point)
30
+ constructor(...values: unknown[]) {
31
+ if (values.length > 0) {
32
+ this.parse(...values)
33
+ }
34
+ }
35
+
36
+ parse(...values: unknown[]): this {
37
+ // Data can be given in these formats:
38
+ // one value, a string -> make it an Equation
39
+ // one value, an Equation
40
+ // one value, a circle -> clone it
41
+ // two values: two points (center and pointThrough)
42
+ // two values: point and Fraction (center and radius)
43
+ // three values: Vector2D, Fraction, Boolean (center, square radius, true)
44
+
45
+ if (typeof values[0] === 'string') {
46
+ return this.fromString(values[0])
47
+ }
48
+
49
+ if (values[0] instanceof Equation) {
50
+ return this.fromEquation(values[0])
51
+ }
52
+
53
+ if (values[0] instanceof Circle) {
54
+ return this.copy(values[0])
55
+ }
56
+
57
+ if (values.length === 2 &&
58
+ values[0] instanceof Point && values[1] instanceof Point
59
+ ) {
60
+ // Circle center through one point
61
+ return this.fromCenterPoint(values[0], values[1])
62
+ }
63
+
64
+ if (values.length >= 2 && values[0]
65
+ instanceof Point &&
66
+ (values[1] instanceof Fraction || typeof values[1] === 'number')
67
+ ) {
68
+ // Circle center through one point
69
+ return this.fromCenterRadius(
70
+ values[0], values[1],
71
+ (typeof values[2] === "boolean") ? values[2] : false
72
+ )
73
+ }
74
+
75
+ return this
76
+ }
77
+
78
+ clone(): Circle {
79
+ return new Circle().fromCenterRadius(
80
+ this.center.clone(),
81
+ this.squareRadius.clone(),
82
+ true
83
+ )
84
+ }
85
+
86
+ copy(circle: Circle): this {
87
+ this.#center = circle.center.clone()
88
+ this.#squareRadius = circle.squareRadius.clone()
89
+
90
+ this.#calculateCartesian()
91
+
92
+ return this
93
+ }
94
+
95
+ get tex(): string {
96
+ if (this.#output_style === CIRCLE_DISPLAY.CANONICAL) {
97
+ return this.equation.moveLeft().reduce().tex
98
+ }
99
+
100
+ let cx, cy
101
+ if (this.center.x.isZero()) {
102
+ cx = 'x^2'
103
+ } else {
104
+ cx = `\\left(x${this.center.x.isNegative() ? '+' : '-'}${this.center.x.clone().abs().tex}\\right)^2`
105
+ }
106
+ if (this.center.y.isZero()) {
107
+ cy = 'y^2'
108
+ } else {
109
+ cy = `\\left(y${this.center.y.isNegative() ? '+' : '-'}${this.center.y.clone().abs().tex}\\right)^2`
110
+ }
111
+ return `${cx}+${cy}=${this.squareRadius.tex}`
112
+ }
113
+
114
+ get display(): string {
115
+ if (this.#output_style === CIRCLE_DISPLAY.CANONICAL) {
116
+ return this.equation.moveLeft().reduce().display
117
+ }
118
+
119
+ let cx, cy
120
+ if (this.center.x.isZero()) {
121
+ cx = 'x^2'
122
+ } else {
123
+ cx = `(x${this.center.x.isNegative() ? '+' : '-'}${this.center.x.clone().abs().tex})^2`
124
+ }
125
+ if (this.center.y.isZero()) {
126
+ cy = 'y^2'
127
+ } else {
128
+ cy = `(y${this.center.y.isNegative() ? '+' : '-'}${this.center.y.clone().abs().tex})^2`
129
+ }
130
+ return `${cx}+${cy}=${this.squareRadius.display}`
131
+ }
132
+
133
+ get asCanonical(): this {
134
+ this.#output_style = CIRCLE_DISPLAY.CANONICAL
135
+ return this
136
+ }
137
+
138
+ get asCenterRadius(): this {
139
+ this.#output_style = CIRCLE_DISPLAY.CENTER_RADIUS
140
+ return this
141
+ }
142
+
143
+ get center(): Point {
144
+ return this.#center ?? new Point()
145
+ }
146
+
147
+ get equation(): Equation {
148
+ return this.#equation?.clone() ?? new Equation('0=0')
149
+ }
150
+
151
+ fromCenterPoint(center: Point, pointThrough: Point): this {
152
+ this.#center = center.clone()
153
+ this.#squareRadius = new Vector(this.#center, pointThrough).normSquare
154
+
155
+ this.#calculateCartesian()
156
+
157
+ return this
158
+ }
159
+
160
+ fromCenterRadius(center: Point, radius: Fraction | number, square?: boolean): this {
161
+ this.#center = center.clone()
162
+ if (square) {
163
+ this.#squareRadius = (new Fraction(radius))
164
+ } else {
165
+ this.#squareRadius = new Fraction(radius).pow(2)
166
+ }
167
+
168
+ this.#calculateCartesian()
169
+
170
+ return this
171
+ }
172
+
173
+ fromEquation(equ: Equation): this {
174
+ // Move everything to the left.
175
+ equ.moveLeft()
176
+
177
+ if (equ.degree('x').value === 2 && equ.degree('y').value === 2) {
178
+ // Both must be of degree 2.
179
+ const x2 = equ.left.monomByDegree(2, 'x'), y2 = equ.left.monomByDegree(2, 'y')
180
+ let x1: Monom, y1: Monom, c: Monom
181
+
182
+ // Both square monoms must have the same coefficient.
183
+ if (x2.coefficient.isEqual(y2.coefficient)) {
184
+ equ.divide(x2.coefficient)
185
+
186
+ x1 = equ.left.monomByDegree(1, 'x')
187
+ y1 = equ.left.monomByDegree(1, 'y')
188
+
189
+ c = equ.left.monomByDegree(0)
190
+
191
+ this.#center = new Point(x1.coefficient.clone().divide(2).opposite(), y1.coefficient.clone().divide(2).opposite())
192
+
193
+ this.#squareRadius = c.coefficient.clone().opposite()
194
+ .add(this.#center.x.clone().pow(2))
195
+ .add(this.#center.y.clone().pow(2))
196
+
197
+ this.#calculateCartesian()
198
+
199
+ return this
200
+ } else {
201
+ // The circle is not a valid circle
202
+ this.#reset()
203
+ }
204
+ }
205
+
206
+ return this
207
+ }
208
+
209
+ fromPoints(A: Point, B: Point, C: Point): this {
210
+ const T = new Triangle(A, B, C)
211
+
212
+ if (!T.isValid || !T.remarquables) {
213
+ this.#reset()
214
+ return this
215
+ }
216
+
217
+ const mAB = T.getMediators().c.clone()
218
+ const mAC = T.getMediators().b.clone()
219
+
220
+ return this.fromCenterPoint(
221
+ mAB.intersection(mAC).point,
222
+ A
223
+ )
224
+ }
225
+
226
+ fromString(str: string): this {
227
+ return this.fromEquation(new Equation(str))
228
+ }
229
+
230
+ getPointsOnCircle(): Point[] {
231
+ // It means searching for pythagorician triples that make a perfect square.
232
+ // (x-4)^2 + (y+3)^2 = 15
233
+ const triplets = Numeric.pythagoreanTripletsWithTarget(this.squareRadius.value, true)
234
+
235
+ const points: Point[] = []
236
+
237
+ triplets.forEach(triplet => {
238
+ // Allow positive / negative values
239
+ // x-a = t => x = a + t
240
+ // x-a = -t => x = a - t
241
+ for (const k of [[1, 1], [-1, 1], [-1, -1], [1, -1]]) {
242
+ // check if the point is not already here.
243
+ const x = this.center.x.clone().add(k[0] * triplet[0])
244
+ const y = this.center.y.clone().add(k[1] * triplet[1])
245
+
246
+ if (points.every(pt => !pt.isEqualXY(x, y))) {
247
+ points.push(new Point(x, y))
248
+ }
249
+
250
+ }
251
+ })
252
+ return points
253
+ }
254
+
255
+ isPointOnCircle = (P: Point): boolean => {
256
+ return this.#equation?.test({x: P.x, y: P.y}) ?? false
257
+ }
258
+
259
+ isSame(circ: Circle): boolean {
260
+ return circ.center.isEqual(this.center)
261
+ && circ.radius.isEqual(this.radius)
262
+ }
263
+
264
+ /**
265
+ * Find the intersection points between the circle and a line. It can be 0, 1 or 2 points.
266
+ * The points are sorted depending on the direction vector of the line.
267
+ * @param L
268
+ */
269
+ lineIntersection(L: Line): Point[] {
270
+ if (this.#equation === null) return []
271
+
272
+ const center = this.center
273
+ const d = L.d
274
+ const OP = L.OA
275
+
276
+ // A = dx^2+dy^2
277
+ const A = d.normSquare
278
+
279
+ // B = 2 ( dx (x0-cx) + dy(y0-cy) )
280
+ const B = OP.x.clone().subtract(center.x).multiply(d.x)
281
+ .add(OP.y.clone().subtract(center.y).multiply(d.y))
282
+ .multiply(2)
283
+
284
+ // C = (x0-cx)^2 + (y0-cy)^2 - r^2
285
+ const C = OP.x.clone().subtract(center.x).pow(2)
286
+ .add(OP.y.clone().subtract(center.y).pow(2))
287
+ .subtract(this.squareRadius)
288
+
289
+ const sol = Solution.fromQuadratic(A, B, C)
290
+
291
+ if (sol.length === 0) return []
292
+
293
+ // One intersection point
294
+ if (sol.length === 1) { // means exact answer
295
+ const OX = OP.add(d.clone().multiplyByScalar(sol[0].fraction))
296
+ return [
297
+ new Point(OX.x, OX.y)
298
+ ]
299
+ }
300
+
301
+ // Two intersection points
302
+ const OX1 = sol[0].exact
303
+ ? OP.clone().add(d.clone().multiplyByScalar(sol[0].fraction))
304
+ : OP.clone().add(d.clone().multiplyByScalar(sol[0].value))
305
+
306
+ const OX2 = sol[1].exact
307
+ ? OP.clone().add(d.clone().multiplyByScalar(sol[1].fraction))
308
+ : OP.clone().add(d.clone().multiplyByScalar(sol[1].value))
309
+
310
+ // sort the points depending on the direction vector.
311
+ return [
312
+ new Point(OX1.x, OX1.y),
313
+ new Point(OX2.x, OX2.y),
314
+ ].sort((a, b) => {
315
+ // <0 => a before
316
+ if (d.x.isZero()) {
317
+ return d.y.isPositive()
318
+ ? a.y.value - b.y.value
319
+ : b.y.value - a.y.value
320
+ }
321
+
322
+ return d.x.isPositive()
323
+ ? a.x.value - b.x.value
324
+ : b.x.value - a.x.value
325
+ })
326
+ }
327
+
328
+ get radius(): Root {
329
+ return new Root().from(2, this.#squareRadius ?? 0)
330
+ }
331
+
332
+ /**
333
+ * Get the relative position between circle and line. It corresponds to the number of intersection.
334
+ * @param {Line} L
335
+ * @returns {number}
336
+ */
337
+ relativePosition(L: Line): number {
338
+ if (this.#center === null || this.#squareRadius === null) {
339
+ return -1
340
+ }
341
+
342
+ const distance = L.distanceTo(this.#center).pow(2).value
343
+ const radius = this.#squareRadius.value
344
+
345
+ if (distance - radius > 0.0000000001) {
346
+ return 0 // external
347
+ }
348
+
349
+ if (Math.abs(distance - radius) < 0.0000000001) {
350
+ return 1 // tangent
351
+ }
352
+
353
+ return 2 // secant
354
+ }
355
+
356
+ setRadius(radius: Fraction | number, square?: boolean): this {
357
+ if (square) {
358
+ this.#squareRadius = new Fraction(radius)
359
+ } else {
360
+ this.#squareRadius = new Fraction(radius).pow(2)
361
+ }
362
+
363
+ this.#calculateCartesian()
364
+ return this
365
+ }
366
+
367
+ get squareRadius(): Fraction {
368
+ return this.#squareRadius?.clone() ?? new Fraction(-1)
369
+ }
370
+
371
+ tangents = (P: Point | Fraction): Line[] => {
372
+ if (P instanceof Fraction) {
373
+ return this.#tangentsWithSlope(P)
374
+ }
375
+
376
+ if (this.isPointOnCircle(P)) {
377
+ return this.#tangentsThroughOnePointOnTheCircle(P)
378
+ }
379
+
380
+ if (this.#center !== null && this.#center.distanceTo(P).value > this.radius.value) {
381
+ return this.#tangentsThroughOnePointOutsideTheCircle(P)
382
+ }
383
+
384
+ return []
385
+ }
386
+
387
+ #calculateCartesian(): void {
388
+ this.#equation = (
389
+ new Equation(
390
+ new Polynom(`(x-(${this.center.x.display}))^2+(y-(${this.center.y.display}))^2`),
391
+ new Polynom(this.squareRadius.display))
392
+ ).moveLeft()
393
+ }
394
+
395
+ #reset() {
396
+ this.#center = null
397
+ this.#squareRadius = null
398
+ this.#equation = null
399
+ }
400
+
401
+ #tangentsThroughOnePointOnTheCircle = (P: Point): Line[] => {
402
+ const CT = new Vector(this.center, P)
403
+ return [new Line().fromPointAndNormal(P, CT)]
404
+ }
405
+
406
+ #tangentsThroughOnePointOutsideTheCircle = (P: Point): Line[] => {
407
+ // y = mx + h
408
+ // px, py => h = -m px + py => mx - y -m.px + py = 0 =>
409
+ // Centre: cx, cy, radius: r
410
+ // (m.cx - cy -m.px + py)^2 = r^2 * (m^2 + 1)
411
+ // (m(cx-py) - (cy - py))^2 = r^2 * (m^2 + 1)
412
+
413
+ const cx_px = this.center.x.clone().subtract(P.x)
414
+ const cy_py = this.center.y.clone().subtract(P.y)
415
+ const polyLeft = new Polynom('x')
416
+ const polyRight = new Polynom('x^2+1')
417
+
418
+ polyLeft.multiply(cx_px).subtract(cy_py).pow(2)
419
+ polyRight.multiply(this.squareRadius)
420
+
421
+ const equ = new Equation(polyLeft, polyRight)
422
+
423
+ const solutions = equ.solve()
424
+
425
+ return solutions.map(sol => {
426
+ // h = -m px + py
427
+ let h: Fraction
428
+ const equ = new Equation('y', 'x')
429
+
430
+ if (sol.exact) {
431
+ h = P.x.clone().opposite().multiply(sol.fraction).add(P.y)
432
+ equ.right.multiply(sol.fraction).add(h)
433
+ } else {
434
+ h = P.x.clone().opposite().multiply(sol.value).add(P.y)
435
+ equ.right.multiply(sol.value).add(h)
436
+ }
437
+
438
+ return new Line(equ)
439
+ })
440
+
441
+ }
442
+
443
+ #tangentsWithSlope = (slope: Fraction): Line[] => {
444
+ // d(C;t)=r => ac1+bc2 + x = +- sqrt(a^2 + b^2)*r
445
+ // x = -ac1-bc2 +- sqrt(a^2 + b^2)*r
446
+ // y = a/bx + h => ax-by + H = 0
447
+
448
+ const a = slope.numerator, b = -slope.denominator, c1 = this.center.x.clone(), c2 = this.center.y.clone()
449
+
450
+ const sq = this.squareRadius.clone().multiply(slope.numerator ** 2 + slope.denominator ** 2),
451
+ x1 = c1.clone().multiply(a).opposite().subtract(c2.clone().multiply(b)).add(sq.clone().sqrt()),
452
+ x2 = c1.clone().multiply(a).opposite().subtract(c2.clone().multiply(b)).subtract(sq.clone().sqrt())
453
+
454
+ return [new Line(a, b, x1), new Line(a, b, x2)]
455
+ }
456
456
  }