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,655 +1,659 @@
1
- /**
2
- * This class works for 2d line in a plane.
3
- */
4
-
5
- import {Numeric} from "../numeric"
6
- import {Fraction} from "../coefficients"
7
- import {Equation, Monom, Polynom} from "../algebra"
8
- import {Vector} from "./vector"
9
- import {type InputValue, type IPiMathObject, LinePropriety} from "../pimath.interface"
10
- import {randomIntSym} from "../randomization/rndHelpers"
11
- import {Point} from "./point"
12
- import {Root} from "../coefficients/root"
13
- import {ParseError} from "../errors"
14
-
15
- enum LINE_DISPLAY {
16
- CARTESIAN,
17
- CANONICAL,
18
- MXH,
19
- PARAMETRIC,
20
- SYSTEM
21
- }
22
-
23
- enum LINE_DIRECTION {
24
- LEFT_TO_RIGHT = 'lr',
25
- RIGHT_TO_LEFT = 'rl'
26
- }
27
-
28
- export class Line implements IPiMathObject<Line> {
29
- static PARALLEL = LinePropriety.Parallel
30
- static PERPENDICULAR = LinePropriety.Perpendicular
31
- #OA: Vector
32
- // ax + by + c = 0
33
- #a: Fraction
34
-
35
- // A line is defined as the canonical form
36
- #b: Fraction
37
- #c: Fraction
38
- #orientation: LINE_DIRECTION
39
- // output mode.
40
- #outputMode: LINE_DISPLAY = LINE_DISPLAY.CANONICAL
41
-
42
- /**
43
- * Value can be a mix of:
44
- *
45
- * @param values
46
- */
47
- constructor(...values: unknown[]) {
48
- this.#a = new Fraction().zero()
49
- this.#b = new Fraction().zero()
50
- this.#c = new Fraction().zero()
51
- this.#OA = new Vector()
52
- this.#orientation = LINE_DIRECTION.LEFT_TO_RIGHT
53
-
54
- if (values.length > 0) {
55
- this.parse(...values)
56
- }
57
-
58
- return this
59
- }
60
-
61
- // ------------------------------------------
62
- /**
63
- * Parse data to a line
64
- * @param {any} values
65
- * @returns {Line}
66
- */
67
- parse = (...values: unknown[]): this => {
68
- // Nothing is given...
69
- if (values.length === 0) {
70
- return this
71
- }
72
-
73
- // One value only: already a line (clone it), an Equation, a string (as Equation)
74
- if (values.length === 1) {
75
- if (values[0] instanceof Line) {
76
- // Already a Line
77
- return this.fromCoefficient(values[0].a, values[0].b, values[0].c)
78
- }
79
-
80
- if (values[0] instanceof Equation) {
81
- // It's an Equation
82
- return this.fromEquation(values[0])
83
- }
84
-
85
- if (typeof values[0] === "string") {
86
- // It's a string - create an Equation from it.
87
- // A malformed string throws (ParseError) instead of silently
88
- // returning a half-built Line.
89
- try {
90
- const E = new Equation(values[0])
91
- return this.parse(E)
92
- } catch (e) {
93
- throw new ParseError(`Cannot parse "${values[0]}" as a Line`, {cause: e})
94
- }
95
- }
96
- }
97
-
98
- if (values.length === 2) {
99
- if (values[0] instanceof Point && values[1] instanceof Point) {
100
- return this.fromPoints(values[0], values[1])
101
- }
102
-
103
- if (values[0] instanceof Point && values[1] instanceof Vector) {
104
- return this.fromPointAndDirection(values[0], values[1])
105
- }
106
- }
107
-
108
- if (values.length === 3 && values.every(x => Fraction.isFraction(x as InputValue<Fraction>))) {
109
- return this.fromCoefficient(
110
- values[0] as InputValue<Fraction>,
111
- values[1] as InputValue<Fraction>,
112
- values[2] as InputValue<Fraction>
113
- )
114
- }
115
-
116
- console.warn('Something wrong happened while creating the line')
117
- console.warn(values)
118
- return this
119
- }
120
-
121
- // ------------------------------------------
122
- // Getter and setter
123
-
124
- clone = (): Line => {
125
- return new Line().fromCoefficient(this.#a, this.#b, this.#c)
126
- }
127
-
128
- get tex(): string {
129
- // canonical => ax + by + c = 0
130
- // mxh => y = -a/b x - c/b
131
- // parametric => (xy) = OA + k*d
132
- // equation => ax + by = -c
133
- // system => \begin{}...
134
-
135
- const output = this.#outputMode
136
- this.#outputMode = LINE_DISPLAY.CANONICAL
137
-
138
- switch (output) {
139
- case LINE_DISPLAY.CARTESIAN:
140
- return this.getEquation().reorder().tex
141
- case LINE_DISPLAY.MXH:
142
- return this.slope.isInfinity() ?
143
- 'x=' + this.OA.x.tex :
144
- 'y=' + new Polynom().parse('x', this.slope, this.height).tex
145
- case LINE_DISPLAY.PARAMETRIC:
146
- case LINE_DISPLAY.SYSTEM: {
147
- const d = this.d.clone().simplify()
148
-
149
- if (output === LINE_DISPLAY.PARAMETRIC) {
150
- return `${Vector.asTex('x', 'y')} = ${Vector.asTex(this.#OA.x.tex, this.#OA.y.tex)} + k\\cdot ${Vector.asTex(d.x.tex, d.y.tex)}`
151
- } else {
152
- return `\\left\\{\\begin{aligned}\n\tx &= ${(new Polynom(this.#OA.x)
153
- .add(new Monom(d.x).multiply(new Monom('k'))))
154
- .reorder('k', true)
155
- .tex}\\\\\n\ty &= ${(new Polynom(this.#OA.y)
156
- .add(new Monom(d.y).multiply(new Monom('k'))))
157
- .reorder('k', true)
158
- .tex}\n\\end{aligned}\\right.`
159
- }
160
- }
161
- default: {
162
- const canonical = this.getEquation()
163
- if (canonical.left.monoms[0].coefficient.isNegative()) {
164
- canonical.multiply(-1)
165
- }
166
- return canonical.tex
167
- }
168
- }
169
-
170
- }
171
-
172
- get display(): string {
173
- // canonical => ax + by + c = 0
174
- // mxh => y = -a/b x - c/b
175
- // parametric => (xy) = OA + k*d // not relevant in display mode.
176
- const output = this.#outputMode
177
- this.#outputMode = LINE_DISPLAY.CANONICAL
178
-
179
- switch (output) {
180
- case LINE_DISPLAY.CARTESIAN:
181
- return this.getEquation().reorder().display
182
- case LINE_DISPLAY.MXH:
183
- return this.slope.isInfinity() ?
184
- 'x=' + this.OA.x.display :
185
- 'y=' + new Polynom().parse('x', this.slope, this.height).display
186
- case LINE_DISPLAY.PARAMETRIC: {
187
- const d = this.d.clone().simplify()
188
- return `((x,y))=((${this.#OA.x.display},${this.#OA.y.display}))+k((${d.x.display},${d.y.display}))`
189
- }
190
- case LINE_DISPLAY.SYSTEM: {
191
- const d = this.d.clone().simplify()
192
- // {(2x,+,17y,=,23),(x,-,y,=,5):}
193
- const px = (new Polynom(this.#OA.x).add(new Monom(d.x).multiply(new Monom('k'))))
194
- .reorder('k', true)
195
- const py = (new Polynom(this.#OA.y).add(new Monom(d.y).multiply(new Monom('k'))))
196
- .reorder('k', true)
197
-
198
- return `{(x,=,${px.display}),(y,=,${py.display}):}`
199
- }
200
- default: {
201
- const canonical = this.getEquation()
202
- // Make sur the first item is positive.
203
- if (canonical.left.monoms[0].coefficient.isNegative()) {
204
- canonical.multiply(-1)
205
- }
206
- return canonical.display
207
- }
208
-
209
- }
210
- }
211
-
212
- get OA(): Vector {
213
- return this.#OA
214
- }
215
-
216
- set OA(value: Vector | Point) {
217
- this.fromPointAndNormal(value, this.n)
218
- }
219
-
220
- get a(): Fraction {
221
- return this.#a
222
- }
223
-
224
- set a(value: Fraction) {
225
- this.#a = value
226
- }
227
-
228
- get asCanonical(): this {
229
- this.#outputMode = LINE_DISPLAY.CANONICAL
230
- return this
231
- }
232
-
233
- get asCartesian(): this {
234
- this.#outputMode = LINE_DISPLAY.CARTESIAN
235
- return this
236
- }
237
-
238
- get asMxh(): this {
239
- this.#outputMode = LINE_DISPLAY.MXH
240
- return this
241
- }
242
-
243
- get asParametric(): this {
244
- this.#outputMode = LINE_DISPLAY.PARAMETRIC
245
- return this
246
- }
247
-
248
- get asSystem(): this {
249
- this.#outputMode = LINE_DISPLAY.SYSTEM
250
- return this
251
- }
252
-
253
- get b(): Fraction {
254
- return this.#b
255
- }
256
-
257
- set b(value: Fraction) {
258
- this.#b = value
259
- }
260
-
261
- get c(): Fraction {
262
- return this.#c
263
- }
264
-
265
- set c(value: Fraction) {
266
- this.#c = value
267
- }
268
-
269
- // ------------------------------------------
270
- canonicalAsFloatCoefficient(decimals = 2): string {
271
- let canonical = ''
272
-
273
- if (!this.#a.isZero()) {
274
- if (this.#a.isOne()) {
275
- canonical = 'x'
276
- } else if (this.#a.clone().opposite().isOne()) {
277
- canonical = '-x'
278
- } else {
279
- canonical = this.#a.value.toFixed(decimals) + 'x'
280
- }
281
- }
282
-
283
- if (!this.#b.isZero()) {
284
- if (this.#b.isPositive()) {
285
- canonical += '+'
286
- }
287
- canonical += this.#b.value.toFixed(decimals) + 'y'
288
- }
289
-
290
- if (!this.#c.isZero()) {
291
- if (this.#c.isPositive()) {
292
- canonical += '+'
293
- }
294
- canonical += this.#c.value.toFixed(decimals)
295
- }
296
-
297
-
298
- return canonical + '=0'
299
- }
300
-
301
- get d(): Vector {
302
- const director = new Vector(this.#b.clone(), this.#a.clone().opposite())
303
-
304
- if (
305
- this.#orientation === LINE_DIRECTION.LEFT_TO_RIGHT && director.x.isNegative() ||
306
- this.#orientation === LINE_DIRECTION.RIGHT_TO_LEFT && director.x.isPositive()
307
- ) {
308
- director.opposite()
309
- }
310
-
311
- return director
312
- }
313
-
314
- set d(value: Vector) {
315
- this.fromPointAndDirection(this.OA, value)
316
- }
317
-
318
- get director(): Vector {
319
- return this.d
320
- }
321
-
322
- distanceTo(pt: Point): Root {
323
- const numerator = pt.x.clone().multiply(this.#a)
324
- .add(pt.y.clone().multiply(this.#b))
325
- .add(this.#c).abs()
326
- const d2 = this.normal.normSquare
327
-
328
- // The denominator is null - shouldn't be possible
329
- if (d2.isZero()) {
330
- return new Root(0)
331
- }
332
-
333
- return new Root().from(2, d2.inverse(), numerator).reduce()
334
-
335
- // The denominator is a perfect square - simplify the tex result
336
- // const value = numerator.value / Math.sqrt(d2.value)
337
- // const F = numerator.clone().divide(d2.clone().sqrt())
338
-
339
- //
340
- // if (d2.isSquare()) {
341
- //
342
- // return {
343
- // value,
344
- // tex: F.tex,
345
- // fraction: F
346
- // }
347
- // }
348
- // // Complete answer...
349
- // return {
350
- // value,
351
- // tex: `\\frac{${numerator.tex}}{\\sqrt{${d2.tex}}}`,
352
- // fraction: F
353
- // }
354
- }
355
-
356
- fromCoefficient(a: InputValue<Fraction>, b: InputValue<Fraction>, c: InputValue<Fraction>): this {
357
- this.#a = new Fraction(a)
358
- this.#b = new Fraction(b)
359
- this.#c = new Fraction(c)
360
-
361
- // make sure the coefficients are relative...
362
- const lcm = [this.#a, this.#b, this.#c].every(k => k.exact)
363
- ? Numeric.lcm(this.#a.denominator, this.#b.denominator, this.#c.denominator)
364
- : 1
365
-
366
- if (lcm > 1) {
367
- this.#a.multiply(lcm).reduce()
368
- this.#b.multiply(lcm).reduce()
369
- this.#c.multiply(lcm).reduce()
370
- }
371
-
372
- if (this.#b.isZero()) {
373
- // ax+c=0 => x = -c/a
374
- this.#OA = new Vector(this.#c.clone().divide(this.#a).opposite(), 0)
375
- return this
376
- }
377
-
378
- // ax+by+c=0 => x=0 => y = a/b x - c/b = (ax-c)/b
379
- for (let x = 0; x < this.#b.value; x++) {
380
- const y = this.#a.clone().divide(this.#b)
381
- .multiply(x)
382
- .subtract(this.#c.clone().divide(this.#b))
383
- .reduce()
384
-
385
- this.#OA = new Vector(x, y)
386
-
387
- if (y.isRelative()) {
388
- return this
389
- }
390
- }
391
-
392
- // no "nice" point... do it with 'x=0'
393
- const y = this.#c.clone().divide(this.#b).opposite().reduce()
394
- this.#OA = new Vector(0, y)
395
-
396
- return this
397
- }
398
-
399
- fromEquation(equ: Equation): this {
400
- // Reorder the equation
401
- equ.reorder(true)
402
-
403
- // It must contain either x, y or both.
404
- const letters = new Set(equ.letters())
405
-
406
- // No 'x', no 'y' in the equations
407
- if (!(letters.has('x') || letters.has('y'))) {
408
- return this
409
- }
410
-
411
- // Another letter in the equation ?
412
- for (const elem of ['x', 'y']) {
413
- if (letters.has(elem)) {
414
- letters.delete(elem)
415
- }
416
- }
417
-
418
- if (letters.size > 0) {
419
- return this
420
- }
421
-
422
- // Everything should be ok now...
423
- return this.fromCoefficient(
424
- equ.left.monomByLetter('x').coefficient,
425
- equ.left.monomByLetter('y').coefficient,
426
- equ.left.monomByDegree(0).coefficient
427
- )
428
- }
429
-
430
- fromParallel(parallel: Line, point: Point): this {
431
- return this.fromPointAndNormal(point, parallel.n)
432
- }
433
-
434
- fromPerpendicular(perpendicular: Line, point: Point): this {
435
- return this.fromPointAndNormal(point, perpendicular.d)
436
- }
437
-
438
- fromPointAndDirection(P: Point | Vector, d: Vector): this {
439
- this.#orientation = d.x.isPositive()
440
- ? LINE_DIRECTION.LEFT_TO_RIGHT
441
- : LINE_DIRECTION.RIGHT_TO_LEFT
442
-
443
- return this.fromPointAndNormal(P, d.clone().normal())
444
- }
445
-
446
- fromPointAndLine(P: Vector, L: Line, orientation: LinePropriety = LinePropriety.Parallel): this {
447
-
448
- if (orientation === LinePropriety.Perpendicular) {
449
- return this.fromPointAndNormal(P, L.director)
450
- }
451
-
452
- return this.fromPointAndNormal(P, L.normal)
453
- }
454
-
455
- fromPointAndNormal = (P: Point | Vector, n: Vector): this => {
456
- if (n.isZero()) {
457
- console.warn('Normal vector is null.')
458
- return this
459
- }
460
-
461
- this.fromCoefficient(
462
- n.x,
463
- n.y,
464
- P.x.clone().multiply(n.x)
465
- .add(P.y.clone().multiply(n.y)).opposite()
466
- )
467
-
468
- this.#OA = new Vector(P.clone())
469
-
470
- return this
471
- }
472
-
473
- // ------------------------------------------
474
- // Creation / parsing functions
475
-
476
- fromPoints(A: Point, B: Point) {
477
- return this.fromPointAndNormal(A, new Vector(A, B).normal())
478
- }
479
-
480
- // ------------------------------------------
481
- getEquation(): Equation {
482
- const equ = new Equation(new Polynom().parse('xy', this.#a, this.#b, this.#c), new Polynom('0'))
483
- return equ.simplify()
484
- }
485
-
486
- getValueAtX = (value: Fraction | number): Fraction => {
487
- const equ = this.getEquation().isolate('y'),
488
- F = new Fraction(value)
489
-
490
- if (equ instanceof Equation) {
491
- return equ.right.evaluate({x: F}) as Fraction
492
- }
493
- return new Fraction().invalid()
494
- }
495
-
496
- getValueAtY = (value: Fraction | number): Fraction => {
497
- const equ = this.getEquation().isolate('x'),
498
- F = new Fraction(value)
499
-
500
- if (equ instanceof Equation) {
501
- return equ.right.evaluate({y: F}) as Fraction
502
- }
503
-
504
- return new Fraction().invalid()
505
- }
506
-
507
- get height(): Fraction {
508
- return this.#c.clone().opposite().divide(this.#b)
509
- }
510
-
511
- hitSegment(A: Point, B: Point): boolean {
512
- const iPt = this.intersection(
513
- new Line().fromPoints(A, B)
514
- )
515
-
516
- // There is an intersection point
517
- if (iPt.hasIntersection) {
518
- return iPt.point.x.value >= Math.min(A.x.value, B.x.value)
519
- && iPt.point.x.value <= Math.max(A.x.value, B.x.value)
520
- && iPt.point.y.value >= Math.min(A.y.value, B.y.value)
521
- && iPt.point.y.value <= Math.max(A.y.value, B.y.value)
522
- }
523
- return false
524
- }
525
-
526
- intersection = (line: Line): { point: Point, hasIntersection: boolean, isParallel: boolean, isSame: boolean } => {
527
- // TODO: rework Line.intersection
528
- const Pt = new Point()
529
- let isParallel = false, isSame = false
530
-
531
- // this => ax+by+c = 0
532
- // line => dx+ey+f = 0
533
- //
534
- // aex + bey + ce = 0
535
- // dbx + bey + bf = 0
536
- // (ae-db)x + ce-bf = 0
537
- //
538
- // adx + bdy + cd = 0
539
- // adx + aey + af = 0
540
- // (bd-ae)y + (cd-af)
541
- //
542
- // x = (bf-ce)/(ae-db)
543
- // y = (af-cd)/(bd-ae)
544
-
545
-
546
- // Theres is no 'y'
547
- if (this.#b.isZero() || line.b.isZero()) {
548
- // TODO : handle no y in the line canonical form
549
- }
550
-
551
- if (this.isParallelTo(line)) {
552
- Pt.x = new Fraction().invalid()
553
- Pt.y = new Fraction().invalid()
554
- isParallel = true
555
- } else if (this.isSameAs(line)) {
556
- Pt.x = new Fraction().invalid()
557
- Pt.y = new Fraction().invalid()
558
- isSame = true
559
- } else {
560
- Pt.x = this.#b.clone().multiply(line.c).subtract(this.#c.clone().multiply(line.b))
561
- .divide(this.#a.clone().multiply(line.b).subtract(this.#b.clone().multiply(line.a)))
562
- Pt.y = this.#a.clone().multiply(line.c).subtract(this.#c.clone().multiply(line.a))
563
- .divide(this.#b.clone().multiply(line.a).subtract(this.#a.clone().multiply(line.b)))
564
- }
565
-
566
- return {
567
- point: Pt,
568
- hasIntersection: !(isParallel || isSame),
569
- isParallel,
570
- isSame
571
- }
572
- }
573
-
574
- isHorizontal(): boolean {
575
- return this.slope.value === 0
576
- }
577
-
578
- // ------------------------------------------
579
- isOnLine(pt: Point): boolean {
580
- return this.#a.clone()
581
- .multiply(pt.x)
582
- .add(
583
- this.#b.clone()
584
- .multiply(pt.y)
585
- )
586
- .add(this.#c)
587
- .isZero()
588
- }
589
-
590
- isParallelTo = (line: Line): boolean => {
591
- // Do they have the isSame direction ?
592
- return this.slope.isEqual(line.slope) && this.height.isNotEqual(line.height)
593
- }
594
-
595
- isPerpendicularTo = (line: Line): boolean => {
596
- return this.d.isNormalTo(line.d)
597
- }
598
-
599
- isSameAs = (line: Line): boolean => {
600
- return this.slope.isEqual(line.slope) && this.height.isEqual(line.height)
601
- }
602
-
603
- isVertical = (): boolean => {
604
- return this.slope.isInfinity()
605
- }
606
-
607
- get n(): Vector {
608
- return this.d.normal()
609
- }
610
-
611
- get normal(): Vector {
612
- return new Vector(this.#a, this.#b)
613
- }
614
-
615
- randomNearPoint = (k?: number): Point => {
616
- const pt = this.randomPoint(k)
617
-
618
- let maxIterationTest = 10
619
- while (this.isOnLine(pt) && maxIterationTest > 0) {
620
- pt.x.add(randomIntSym(1, false))
621
- pt.y.add(randomIntSym(1, false))
622
- maxIterationTest--
623
-
624
- }
625
-
626
- return pt
627
- }
628
-
629
- randomPoint = (k?: number): Point => {
630
- // Return a random point on the line.
631
- const pt = this.d
632
- .clone()
633
- .multiplyByScalar(randomIntSym((k === undefined || k <= 1) ? 3 : k, false))
634
- .add(this.#OA)
635
-
636
- return new Point(pt)
637
- }
638
-
639
- simplify = (): this => {
640
- const lcm = Numeric.lcm(this.#a.denominator, this.#b.denominator, this.#c.denominator),
641
- gcd = Numeric.gcd(this.#a.numerator, this.#b.numerator, this.#c.numerator)
642
-
643
- this.fromCoefficient(
644
- this.#a.clone().multiply(lcm).divide(gcd),
645
- this.#b.clone().multiply(lcm).divide(gcd),
646
- this.#c.clone().multiply(lcm).divide(gcd),
647
- )
648
-
649
- return this
650
- }
651
-
652
- get slope(): Fraction {
653
- return this.#a.clone().opposite().divide(this.#b)
654
- }
1
+ /**
2
+ * This class works for 2d line in a plane.
3
+ */
4
+
5
+ import {Numeric} from "../numeric"
6
+ import {Fraction} from "../coefficients"
7
+ import {Equation, Monom, Polynom} from "../algebra"
8
+ import {Vector} from "./vector"
9
+ import {type InputValue, type IPiMathObject, LinePropriety} from "../pimath.interface"
10
+ import {randomIntSym} from "../randomization/rndHelpers"
11
+ import {Point} from "./point"
12
+ import {Root} from "../coefficients/root"
13
+ import {NotImplementedError, ParseError} from "../errors"
14
+
15
+ enum LINE_DISPLAY {
16
+ CARTESIAN,
17
+ CANONICAL,
18
+ MXH,
19
+ PARAMETRIC,
20
+ SYSTEM
21
+ }
22
+
23
+ enum LINE_DIRECTION {
24
+ LEFT_TO_RIGHT = 'lr',
25
+ RIGHT_TO_LEFT = 'rl'
26
+ }
27
+
28
+ export class Line implements IPiMathObject<Line> {
29
+ static PARALLEL = LinePropriety.Parallel
30
+ static PERPENDICULAR = LinePropriety.Perpendicular
31
+ #OA: Vector
32
+ // ax + by + c = 0
33
+ #a: Fraction
34
+
35
+ // A line is defined as the canonical form
36
+ #b: Fraction
37
+ #c: Fraction
38
+ #orientation: LINE_DIRECTION
39
+ // output mode.
40
+ #outputMode: LINE_DISPLAY = LINE_DISPLAY.CANONICAL
41
+
42
+ /**
43
+ * Value can be a mix of:
44
+ *
45
+ * @param values
46
+ */
47
+ constructor(...values: unknown[]) {
48
+ this.#a = new Fraction().zero()
49
+ this.#b = new Fraction().zero()
50
+ this.#c = new Fraction().zero()
51
+ this.#OA = new Vector()
52
+ this.#orientation = LINE_DIRECTION.LEFT_TO_RIGHT
53
+
54
+ if (values.length > 0) {
55
+ this.parse(...values)
56
+ }
57
+
58
+ return this
59
+ }
60
+
61
+ // ------------------------------------------
62
+ /**
63
+ * Parse data to a line
64
+ * @param {any} values
65
+ * @returns {Line}
66
+ */
67
+ parse = (...values: unknown[]): this => {
68
+ // Nothing is given...
69
+ if (values.length === 0) {
70
+ return this
71
+ }
72
+
73
+ // One value only: already a line (clone it), an Equation, a string (as Equation)
74
+ if (values.length === 1) {
75
+ if (values[0] instanceof Line) {
76
+ // Already a Line
77
+ return this.fromCoefficient(values[0].a, values[0].b, values[0].c)
78
+ }
79
+
80
+ if (values[0] instanceof Equation) {
81
+ // It's an Equation
82
+ return this.fromEquation(values[0])
83
+ }
84
+
85
+ if (typeof values[0] === "string") {
86
+ // It's a string - create an Equation from it.
87
+ // A malformed string throws (ParseError) instead of silently
88
+ // returning a half-built Line.
89
+ try {
90
+ const E = new Equation(values[0])
91
+ return this.parse(E)
92
+ } catch (e) {
93
+ throw new ParseError(`Cannot parse "${values[0]}" as a Line`, {cause: e})
94
+ }
95
+ }
96
+ }
97
+
98
+ if (values.length === 2) {
99
+ if (values[0] instanceof Point && values[1] instanceof Point) {
100
+ return this.fromPoints(values[0], values[1])
101
+ }
102
+
103
+ if (values[0] instanceof Point && values[1] instanceof Vector) {
104
+ return this.fromPointAndDirection(values[0], values[1])
105
+ }
106
+ }
107
+
108
+ if (values.length === 3 && values.every(x => Fraction.isFraction(x as InputValue<Fraction>))) {
109
+ return this.fromCoefficient(
110
+ values[0] as InputValue<Fraction>,
111
+ values[1] as InputValue<Fraction>,
112
+ values[2] as InputValue<Fraction>
113
+ )
114
+ }
115
+
116
+ console.warn('Something wrong happened while creating the line')
117
+ console.warn(values)
118
+ return this
119
+ }
120
+
121
+ // ------------------------------------------
122
+ // Getter and setter
123
+
124
+ copy(): this {
125
+ throw new NotImplementedError("copy() is not yet implemented for Line")
126
+ }
127
+
128
+ clone = (): Line => {
129
+ return new Line().fromCoefficient(this.#a, this.#b, this.#c)
130
+ }
131
+
132
+ get tex(): string {
133
+ // canonical => ax + by + c = 0
134
+ // mxh => y = -a/b x - c/b
135
+ // parametric => (xy) = OA + k*d
136
+ // equation => ax + by = -c
137
+ // system => \begin{}...
138
+
139
+ const output = this.#outputMode
140
+ this.#outputMode = LINE_DISPLAY.CANONICAL
141
+
142
+ switch (output) {
143
+ case LINE_DISPLAY.CARTESIAN:
144
+ return this.getEquation().reorder().tex
145
+ case LINE_DISPLAY.MXH:
146
+ return this.slope.isInfinity() ?
147
+ 'x=' + this.OA.x.tex :
148
+ 'y=' + new Polynom().parse('x', this.slope, this.height).tex
149
+ case LINE_DISPLAY.PARAMETRIC:
150
+ case LINE_DISPLAY.SYSTEM: {
151
+ const d = this.d.clone().simplify()
152
+
153
+ if (output === LINE_DISPLAY.PARAMETRIC) {
154
+ return `${Vector.asTex('x', 'y')} = ${Vector.asTex(this.#OA.x.tex, this.#OA.y.tex)} + k\\cdot ${Vector.asTex(d.x.tex, d.y.tex)}`
155
+ } else {
156
+ return `\\left\\{\\begin{aligned}\n\tx &= ${(new Polynom(this.#OA.x)
157
+ .add(new Monom(d.x).multiply(new Monom('k'))))
158
+ .reorder('k', true)
159
+ .tex}\\\\\n\ty &= ${(new Polynom(this.#OA.y)
160
+ .add(new Monom(d.y).multiply(new Monom('k'))))
161
+ .reorder('k', true)
162
+ .tex}\n\\end{aligned}\\right.`
163
+ }
164
+ }
165
+ default: {
166
+ const canonical = this.getEquation()
167
+ if (canonical.left.monoms[0].coefficient.isNegative()) {
168
+ canonical.multiply(-1)
169
+ }
170
+ return canonical.tex
171
+ }
172
+ }
173
+
174
+ }
175
+
176
+ get display(): string {
177
+ // canonical => ax + by + c = 0
178
+ // mxh => y = -a/b x - c/b
179
+ // parametric => (xy) = OA + k*d // not relevant in display mode.
180
+ const output = this.#outputMode
181
+ this.#outputMode = LINE_DISPLAY.CANONICAL
182
+
183
+ switch (output) {
184
+ case LINE_DISPLAY.CARTESIAN:
185
+ return this.getEquation().reorder().display
186
+ case LINE_DISPLAY.MXH:
187
+ return this.slope.isInfinity() ?
188
+ 'x=' + this.OA.x.display :
189
+ 'y=' + new Polynom().parse('x', this.slope, this.height).display
190
+ case LINE_DISPLAY.PARAMETRIC: {
191
+ const d = this.d.clone().simplify()
192
+ return `((x,y))=((${this.#OA.x.display},${this.#OA.y.display}))+k((${d.x.display},${d.y.display}))`
193
+ }
194
+ case LINE_DISPLAY.SYSTEM: {
195
+ const d = this.d.clone().simplify()
196
+ // {(2x,+,17y,=,23),(x,-,y,=,5):}
197
+ const px = (new Polynom(this.#OA.x).add(new Monom(d.x).multiply(new Monom('k'))))
198
+ .reorder('k', true)
199
+ const py = (new Polynom(this.#OA.y).add(new Monom(d.y).multiply(new Monom('k'))))
200
+ .reorder('k', true)
201
+
202
+ return `{(x,=,${px.display}),(y,=,${py.display}):}`
203
+ }
204
+ default: {
205
+ const canonical = this.getEquation()
206
+ // Make sur the first item is positive.
207
+ if (canonical.left.monoms[0].coefficient.isNegative()) {
208
+ canonical.multiply(-1)
209
+ }
210
+ return canonical.display
211
+ }
212
+
213
+ }
214
+ }
215
+
216
+ get OA(): Vector {
217
+ return this.#OA
218
+ }
219
+
220
+ set OA(value: Vector | Point) {
221
+ this.fromPointAndNormal(value, this.n)
222
+ }
223
+
224
+ get a(): Fraction {
225
+ return this.#a
226
+ }
227
+
228
+ set a(value: Fraction) {
229
+ this.#a = value
230
+ }
231
+
232
+ get asCanonical(): this {
233
+ this.#outputMode = LINE_DISPLAY.CANONICAL
234
+ return this
235
+ }
236
+
237
+ get asCartesian(): this {
238
+ this.#outputMode = LINE_DISPLAY.CARTESIAN
239
+ return this
240
+ }
241
+
242
+ get asMxh(): this {
243
+ this.#outputMode = LINE_DISPLAY.MXH
244
+ return this
245
+ }
246
+
247
+ get asParametric(): this {
248
+ this.#outputMode = LINE_DISPLAY.PARAMETRIC
249
+ return this
250
+ }
251
+
252
+ get asSystem(): this {
253
+ this.#outputMode = LINE_DISPLAY.SYSTEM
254
+ return this
255
+ }
256
+
257
+ get b(): Fraction {
258
+ return this.#b
259
+ }
260
+
261
+ set b(value: Fraction) {
262
+ this.#b = value
263
+ }
264
+
265
+ get c(): Fraction {
266
+ return this.#c
267
+ }
268
+
269
+ set c(value: Fraction) {
270
+ this.#c = value
271
+ }
272
+
273
+ // ------------------------------------------
274
+ canonicalAsFloatCoefficient(decimals = 2): string {
275
+ let canonical = ''
276
+
277
+ if (!this.#a.isZero()) {
278
+ if (this.#a.isOne()) {
279
+ canonical = 'x'
280
+ } else if (this.#a.clone().opposite().isOne()) {
281
+ canonical = '-x'
282
+ } else {
283
+ canonical = this.#a.value.toFixed(decimals) + 'x'
284
+ }
285
+ }
286
+
287
+ if (!this.#b.isZero()) {
288
+ if (this.#b.isPositive()) {
289
+ canonical += '+'
290
+ }
291
+ canonical += this.#b.value.toFixed(decimals) + 'y'
292
+ }
293
+
294
+ if (!this.#c.isZero()) {
295
+ if (this.#c.isPositive()) {
296
+ canonical += '+'
297
+ }
298
+ canonical += this.#c.value.toFixed(decimals)
299
+ }
300
+
301
+
302
+ return canonical + '=0'
303
+ }
304
+
305
+ get d(): Vector {
306
+ const director = new Vector(this.#b.clone(), this.#a.clone().opposite())
307
+
308
+ if (
309
+ this.#orientation === LINE_DIRECTION.LEFT_TO_RIGHT && director.x.isNegative() ||
310
+ this.#orientation === LINE_DIRECTION.RIGHT_TO_LEFT && director.x.isPositive()
311
+ ) {
312
+ director.opposite()
313
+ }
314
+
315
+ return director
316
+ }
317
+
318
+ set d(value: Vector) {
319
+ this.fromPointAndDirection(this.OA, value)
320
+ }
321
+
322
+ get director(): Vector {
323
+ return this.d
324
+ }
325
+
326
+ distanceTo(pt: Point): Root {
327
+ const numerator = pt.x.clone().multiply(this.#a)
328
+ .add(pt.y.clone().multiply(this.#b))
329
+ .add(this.#c).abs()
330
+ const d2 = this.normal.normSquare
331
+
332
+ // The denominator is null - shouldn't be possible
333
+ if (d2.isZero()) {
334
+ return new Root(0)
335
+ }
336
+
337
+ return new Root().from(2, d2.inverse(), numerator).reduce()
338
+
339
+ // The denominator is a perfect square - simplify the tex result
340
+ // const value = numerator.value / Math.sqrt(d2.value)
341
+ // const F = numerator.clone().divide(d2.clone().sqrt())
342
+
343
+ //
344
+ // if (d2.isSquare()) {
345
+ //
346
+ // return {
347
+ // value,
348
+ // tex: F.tex,
349
+ // fraction: F
350
+ // }
351
+ // }
352
+ // // Complete answer...
353
+ // return {
354
+ // value,
355
+ // tex: `\\frac{${numerator.tex}}{\\sqrt{${d2.tex}}}`,
356
+ // fraction: F
357
+ // }
358
+ }
359
+
360
+ fromCoefficient(a: InputValue<Fraction>, b: InputValue<Fraction>, c: InputValue<Fraction>): this {
361
+ this.#a = new Fraction(a)
362
+ this.#b = new Fraction(b)
363
+ this.#c = new Fraction(c)
364
+
365
+ // make sure the coefficients are relative...
366
+ const lcm = [this.#a, this.#b, this.#c].every(k => k.exact)
367
+ ? Numeric.lcm(this.#a.denominator, this.#b.denominator, this.#c.denominator)
368
+ : 1
369
+
370
+ if (lcm > 1) {
371
+ this.#a.multiply(lcm).reduce()
372
+ this.#b.multiply(lcm).reduce()
373
+ this.#c.multiply(lcm).reduce()
374
+ }
375
+
376
+ if (this.#b.isZero()) {
377
+ // ax+c=0 => x = -c/a
378
+ this.#OA = new Vector(this.#c.clone().divide(this.#a).opposite(), 0)
379
+ return this
380
+ }
381
+
382
+ // ax+by+c=0 => x=0 => y = a/b x - c/b = (ax-c)/b
383
+ for (let x = 0; x < this.#b.value; x++) {
384
+ const y = this.#a.clone().divide(this.#b)
385
+ .multiply(x)
386
+ .subtract(this.#c.clone().divide(this.#b))
387
+ .reduce()
388
+
389
+ this.#OA = new Vector(x, y)
390
+
391
+ if (y.isRelative()) {
392
+ return this
393
+ }
394
+ }
395
+
396
+ // no "nice" point... do it with 'x=0'
397
+ const y = this.#c.clone().divide(this.#b).opposite().reduce()
398
+ this.#OA = new Vector(0, y)
399
+
400
+ return this
401
+ }
402
+
403
+ fromEquation(equ: Equation): this {
404
+ // Reorder the equation
405
+ equ.reorder(true)
406
+
407
+ // It must contain either x, y or both.
408
+ const letters = new Set(equ.letters())
409
+
410
+ // No 'x', no 'y' in the equations
411
+ if (!(letters.has('x') || letters.has('y'))) {
412
+ return this
413
+ }
414
+
415
+ // Another letter in the equation ?
416
+ for (const elem of ['x', 'y']) {
417
+ if (letters.has(elem)) {
418
+ letters.delete(elem)
419
+ }
420
+ }
421
+
422
+ if (letters.size > 0) {
423
+ return this
424
+ }
425
+
426
+ // Everything should be ok now...
427
+ return this.fromCoefficient(
428
+ equ.left.monomByLetter('x').coefficient,
429
+ equ.left.monomByLetter('y').coefficient,
430
+ equ.left.monomByDegree(0).coefficient
431
+ )
432
+ }
433
+
434
+ fromParallel(parallel: Line, point: Point): this {
435
+ return this.fromPointAndNormal(point, parallel.n)
436
+ }
437
+
438
+ fromPerpendicular(perpendicular: Line, point: Point): this {
439
+ return this.fromPointAndNormal(point, perpendicular.d)
440
+ }
441
+
442
+ fromPointAndDirection(P: Point | Vector, d: Vector): this {
443
+ this.#orientation = d.x.isPositive()
444
+ ? LINE_DIRECTION.LEFT_TO_RIGHT
445
+ : LINE_DIRECTION.RIGHT_TO_LEFT
446
+
447
+ return this.fromPointAndNormal(P, d.clone().normal())
448
+ }
449
+
450
+ fromPointAndLine(P: Vector, L: Line, orientation: LinePropriety = LinePropriety.Parallel): this {
451
+
452
+ if (orientation === LinePropriety.Perpendicular) {
453
+ return this.fromPointAndNormal(P, L.director)
454
+ }
455
+
456
+ return this.fromPointAndNormal(P, L.normal)
457
+ }
458
+
459
+ fromPointAndNormal = (P: Point | Vector, n: Vector): this => {
460
+ if (n.isZero()) {
461
+ console.warn('Normal vector is null.')
462
+ return this
463
+ }
464
+
465
+ this.fromCoefficient(
466
+ n.x,
467
+ n.y,
468
+ P.x.clone().multiply(n.x)
469
+ .add(P.y.clone().multiply(n.y)).opposite()
470
+ )
471
+
472
+ this.#OA = new Vector(P.clone())
473
+
474
+ return this
475
+ }
476
+
477
+ // ------------------------------------------
478
+ // Creation / parsing functions
479
+
480
+ fromPoints(A: Point, B: Point) {
481
+ return this.fromPointAndNormal(A, new Vector(A, B).normal())
482
+ }
483
+
484
+ // ------------------------------------------
485
+ getEquation(): Equation {
486
+ const equ = new Equation(new Polynom().parse('xy', this.#a, this.#b, this.#c), new Polynom('0'))
487
+ return equ.simplify()
488
+ }
489
+
490
+ getValueAtX = (value: Fraction | number): Fraction => {
491
+ const equ = this.getEquation().isolate('y'),
492
+ F = new Fraction(value)
493
+
494
+ if (equ instanceof Equation) {
495
+ return equ.right.evaluate({x: F}) as Fraction
496
+ }
497
+ return new Fraction().invalid()
498
+ }
499
+
500
+ getValueAtY = (value: Fraction | number): Fraction => {
501
+ const equ = this.getEquation().isolate('x'),
502
+ F = new Fraction(value)
503
+
504
+ if (equ instanceof Equation) {
505
+ return equ.right.evaluate({y: F}) as Fraction
506
+ }
507
+
508
+ return new Fraction().invalid()
509
+ }
510
+
511
+ get height(): Fraction {
512
+ return this.#c.clone().opposite().divide(this.#b)
513
+ }
514
+
515
+ hitSegment(A: Point, B: Point): boolean {
516
+ const iPt = this.intersection(
517
+ new Line().fromPoints(A, B)
518
+ )
519
+
520
+ // There is an intersection point
521
+ if (iPt.hasIntersection) {
522
+ return iPt.point.x.value >= Math.min(A.x.value, B.x.value)
523
+ && iPt.point.x.value <= Math.max(A.x.value, B.x.value)
524
+ && iPt.point.y.value >= Math.min(A.y.value, B.y.value)
525
+ && iPt.point.y.value <= Math.max(A.y.value, B.y.value)
526
+ }
527
+ return false
528
+ }
529
+
530
+ intersection = (line: Line): { point: Point, hasIntersection: boolean, isParallel: boolean, isSame: boolean } => {
531
+ // TODO: rework Line.intersection
532
+ const Pt = new Point()
533
+ let isParallel = false, isSame = false
534
+
535
+ // this => ax+by+c = 0
536
+ // line => dx+ey+f = 0
537
+ //
538
+ // aex + bey + ce = 0
539
+ // dbx + bey + bf = 0
540
+ // (ae-db)x + ce-bf = 0
541
+ //
542
+ // adx + bdy + cd = 0
543
+ // adx + aey + af = 0
544
+ // (bd-ae)y + (cd-af)
545
+ //
546
+ // x = (bf-ce)/(ae-db)
547
+ // y = (af-cd)/(bd-ae)
548
+
549
+
550
+ // Theres is no 'y'
551
+ if (this.#b.isZero() || line.b.isZero()) {
552
+ // TODO : handle no y in the line canonical form
553
+ }
554
+
555
+ if (this.isParallelTo(line)) {
556
+ Pt.x = new Fraction().invalid()
557
+ Pt.y = new Fraction().invalid()
558
+ isParallel = true
559
+ } else if (this.isSameAs(line)) {
560
+ Pt.x = new Fraction().invalid()
561
+ Pt.y = new Fraction().invalid()
562
+ isSame = true
563
+ } else {
564
+ Pt.x = this.#b.clone().multiply(line.c).subtract(this.#c.clone().multiply(line.b))
565
+ .divide(this.#a.clone().multiply(line.b).subtract(this.#b.clone().multiply(line.a)))
566
+ Pt.y = this.#a.clone().multiply(line.c).subtract(this.#c.clone().multiply(line.a))
567
+ .divide(this.#b.clone().multiply(line.a).subtract(this.#a.clone().multiply(line.b)))
568
+ }
569
+
570
+ return {
571
+ point: Pt,
572
+ hasIntersection: !(isParallel || isSame),
573
+ isParallel,
574
+ isSame
575
+ }
576
+ }
577
+
578
+ isHorizontal(): boolean {
579
+ return this.slope.value === 0
580
+ }
581
+
582
+ // ------------------------------------------
583
+ isOnLine(pt: Point): boolean {
584
+ return this.#a.clone()
585
+ .multiply(pt.x)
586
+ .add(
587
+ this.#b.clone()
588
+ .multiply(pt.y)
589
+ )
590
+ .add(this.#c)
591
+ .isZero()
592
+ }
593
+
594
+ isParallelTo = (line: Line): boolean => {
595
+ // Do they have the isSame direction ?
596
+ return this.slope.isEqual(line.slope) && this.height.isNotEqual(line.height)
597
+ }
598
+
599
+ isPerpendicularTo = (line: Line): boolean => {
600
+ return this.d.isNormalTo(line.d)
601
+ }
602
+
603
+ isSameAs = (line: Line): boolean => {
604
+ return this.slope.isEqual(line.slope) && this.height.isEqual(line.height)
605
+ }
606
+
607
+ isVertical = (): boolean => {
608
+ return this.slope.isInfinity()
609
+ }
610
+
611
+ get n(): Vector {
612
+ return this.d.normal()
613
+ }
614
+
615
+ get normal(): Vector {
616
+ return new Vector(this.#a, this.#b)
617
+ }
618
+
619
+ randomNearPoint = (k?: number): Point => {
620
+ const pt = this.randomPoint(k)
621
+
622
+ let maxIterationTest = 10
623
+ while (this.isOnLine(pt) && maxIterationTest > 0) {
624
+ pt.x.add(randomIntSym(1, false))
625
+ pt.y.add(randomIntSym(1, false))
626
+ maxIterationTest--
627
+
628
+ }
629
+
630
+ return pt
631
+ }
632
+
633
+ randomPoint = (k?: number): Point => {
634
+ // Return a random point on the line.
635
+ const pt = this.d
636
+ .clone()
637
+ .multiplyByScalar(randomIntSym((k === undefined || k <= 1) ? 3 : k, false))
638
+ .add(this.#OA)
639
+
640
+ return new Point(pt)
641
+ }
642
+
643
+ simplify = (): this => {
644
+ const lcm = Numeric.lcm(this.#a.denominator, this.#b.denominator, this.#c.denominator),
645
+ gcd = Numeric.gcd(this.#a.numerator, this.#b.numerator, this.#c.numerator)
646
+
647
+ this.fromCoefficient(
648
+ this.#a.clone().multiply(lcm).divide(gcd),
649
+ this.#b.clone().multiply(lcm).divide(gcd),
650
+ this.#c.clone().multiply(lcm).divide(gcd),
651
+ )
652
+
653
+ return this
654
+ }
655
+
656
+ get slope(): Fraction {
657
+ return this.#a.clone().opposite().divide(this.#b)
658
+ }
655
659
  }