@js-draw/math 1.0.0 → 1.2.0

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/Color4.d.ts +40 -0
  3. package/dist/cjs/Color4.js +102 -0
  4. package/dist/cjs/Color4.test.d.ts +1 -0
  5. package/dist/cjs/Mat33.test.d.ts +1 -0
  6. package/dist/cjs/Vec2.test.d.ts +1 -0
  7. package/dist/cjs/Vec3.test.d.ts +1 -0
  8. package/dist/cjs/polynomial/solveQuadratic.test.d.ts +1 -0
  9. package/dist/cjs/rounding.test.d.ts +1 -0
  10. package/dist/cjs/shapes/LineSegment2.test.d.ts +1 -0
  11. package/dist/cjs/shapes/Path.fromString.test.d.ts +1 -0
  12. package/dist/cjs/shapes/Path.test.d.ts +1 -0
  13. package/dist/cjs/shapes/Path.toString.test.d.ts +1 -0
  14. package/dist/cjs/shapes/QuadraticBezier.test.d.ts +1 -0
  15. package/dist/cjs/shapes/Rect2.test.d.ts +1 -0
  16. package/dist/cjs/shapes/Triangle.test.d.ts +1 -0
  17. package/dist/mjs/Color4.d.ts +40 -0
  18. package/dist/mjs/Color4.mjs +102 -0
  19. package/dist/mjs/Color4.test.d.ts +1 -0
  20. package/dist/mjs/Mat33.test.d.ts +1 -0
  21. package/dist/mjs/Vec2.test.d.ts +1 -0
  22. package/dist/mjs/Vec3.test.d.ts +1 -0
  23. package/dist/mjs/polynomial/solveQuadratic.test.d.ts +1 -0
  24. package/dist/mjs/rounding.test.d.ts +1 -0
  25. package/dist/mjs/shapes/LineSegment2.test.d.ts +1 -0
  26. package/dist/mjs/shapes/Path.fromString.test.d.ts +1 -0
  27. package/dist/mjs/shapes/Path.test.d.ts +1 -0
  28. package/dist/mjs/shapes/Path.toString.test.d.ts +1 -0
  29. package/dist/mjs/shapes/QuadraticBezier.test.d.ts +1 -0
  30. package/dist/mjs/shapes/Rect2.test.d.ts +1 -0
  31. package/dist/mjs/shapes/Triangle.test.d.ts +1 -0
  32. package/dist-test/test_imports/package-lock.json +13 -0
  33. package/dist-test/test_imports/package.json +12 -0
  34. package/dist-test/test_imports/test-imports.js +15 -0
  35. package/dist-test/test_imports/test-require.cjs +15 -0
  36. package/package.json +4 -3
  37. package/src/Color4.test.ts +94 -0
  38. package/src/Color4.ts +430 -0
  39. package/src/Mat33.test.ts +244 -0
  40. package/src/Mat33.ts +450 -0
  41. package/src/Vec2.test.ts +30 -0
  42. package/src/Vec2.ts +49 -0
  43. package/src/Vec3.test.ts +51 -0
  44. package/src/Vec3.ts +245 -0
  45. package/src/lib.ts +42 -0
  46. package/src/polynomial/solveQuadratic.test.ts +39 -0
  47. package/src/polynomial/solveQuadratic.ts +43 -0
  48. package/src/rounding.test.ts +65 -0
  49. package/src/rounding.ts +167 -0
  50. package/src/shapes/Abstract2DShape.ts +63 -0
  51. package/src/shapes/BezierJSWrapper.ts +93 -0
  52. package/src/shapes/CubicBezier.ts +35 -0
  53. package/src/shapes/LineSegment2.test.ts +99 -0
  54. package/src/shapes/LineSegment2.ts +232 -0
  55. package/src/shapes/Path.fromString.test.ts +223 -0
  56. package/src/shapes/Path.test.ts +309 -0
  57. package/src/shapes/Path.toString.test.ts +77 -0
  58. package/src/shapes/Path.ts +963 -0
  59. package/src/shapes/PointShape2D.ts +33 -0
  60. package/src/shapes/QuadraticBezier.test.ts +31 -0
  61. package/src/shapes/QuadraticBezier.ts +142 -0
  62. package/src/shapes/Rect2.test.ts +209 -0
  63. package/src/shapes/Rect2.ts +346 -0
  64. package/src/shapes/Triangle.test.ts +61 -0
  65. package/src/shapes/Triangle.ts +139 -0
package/src/Vec3.ts ADDED
@@ -0,0 +1,245 @@
1
+
2
+
3
+ /**
4
+ * A vector with three components, $\begin{pmatrix} x \\ y \\ z \end{pmatrix}$.
5
+ * Can also be used to represent a two-component vector.
6
+ *
7
+ * A `Vec3` is immutable.
8
+ *
9
+ * @example
10
+ *
11
+ * ```ts,runnable,console
12
+ * import { Vec3 } from '@js-draw/math';
13
+ *
14
+ * console.log('Vector addition:', Vec3.of(1, 2, 3).plus(Vec3.of(0, 1, 0)));
15
+ * console.log('Scalar multiplication:', Vec3.of(1, 2, 3).times(2));
16
+ * console.log('Cross products:', Vec3.unitX.cross(Vec3.unitY));
17
+ * console.log('Magnitude:', Vec3.of(1, 2, 3).length(), 'or', Vec3.of(1, 2, 3).magnitude());
18
+ * console.log('Square Magnitude:', Vec3.of(1, 2, 3).magnitudeSquared());
19
+ * console.log('As an array:', Vec3.unitZ.asArray());
20
+ * ```
21
+ */
22
+ export class Vec3 {
23
+ private constructor(
24
+ public readonly x: number,
25
+ public readonly y: number,
26
+ public readonly z: number
27
+ ) {
28
+ }
29
+
30
+ /** Returns the x, y components of this. */
31
+ public get xy(): { x: number; y: number } {
32
+ // Useful for APIs that behave differently if .z is present.
33
+ return {
34
+ x: this.x,
35
+ y: this.y,
36
+ };
37
+ }
38
+
39
+ /** Construct a vector from three components. */
40
+ public static of(x: number, y: number, z: number): Vec3 {
41
+ return new Vec3(x, y, z);
42
+ }
43
+
44
+ /** Returns this' `idx`th component. For example, `Vec3.of(1, 2, 3).at(1) → 2`. */
45
+ public at(idx: number): number {
46
+ if (idx === 0) return this.x;
47
+ if (idx === 1) return this.y;
48
+ if (idx === 2) return this.z;
49
+
50
+ throw new Error(`${idx} out of bounds!`);
51
+ }
52
+
53
+ /** Alias for this.magnitude. */
54
+ public length(): number {
55
+ return this.magnitude();
56
+ }
57
+
58
+ public magnitude(): number {
59
+ return Math.sqrt(this.dot(this));
60
+ }
61
+
62
+ public magnitudeSquared(): number {
63
+ return this.dot(this);
64
+ }
65
+
66
+ /**
67
+ * Return this' angle in the XY plane (treats this as a Vec2).
68
+ *
69
+ * This is equivalent to `Math.atan2(vec.y, vec.x)`.
70
+ */
71
+ public angle(): number {
72
+ return Math.atan2(this.y, this.x);
73
+ }
74
+
75
+ /**
76
+ * Returns a unit vector in the same direction as this.
77
+ *
78
+ * If `this` has zero length, the resultant vector has `NaN` components.
79
+ */
80
+ public normalized(): Vec3 {
81
+ const norm = this.magnitude();
82
+ return Vec3.of(this.x / norm, this.y / norm, this.z / norm);
83
+ }
84
+
85
+ /**
86
+ * Like {@link normalized}, except returns zero if this has zero magnitude.
87
+ */
88
+ public normalizedOrZero(): Vec3 {
89
+ if (this.eq(Vec3.zero)) {
90
+ return Vec3.zero;
91
+ }
92
+
93
+ return this.normalized();
94
+ }
95
+
96
+ /** @returns A copy of `this` multiplied by a scalar. */
97
+ public times(c: number): Vec3 {
98
+ return Vec3.of(this.x * c, this.y * c, this.z * c);
99
+ }
100
+
101
+ public plus(v: Vec3): Vec3 {
102
+ return Vec3.of(this.x + v.x, this.y + v.y, this.z + v.z);
103
+ }
104
+
105
+ public minus(v: Vec3): Vec3 {
106
+ return Vec3.of(this.x - v.x, this.y - v.y, this.z - v.z);
107
+ }
108
+
109
+ public dot(other: Vec3): number {
110
+ return this.x * other.x + this.y * other.y + this.z * other.z;
111
+ }
112
+
113
+ public cross(other: Vec3): Vec3 {
114
+ // | i j k |
115
+ // | x1 y1 z1| = (i)(y1z2 - y2z1) - (j)(x1z2 - x2z1) + (k)(x1y2 - x2y1)
116
+ // | x2 y2 z2|
117
+ return Vec3.of(
118
+ this.y * other.z - other.y * this.z,
119
+ other.x * this.z - this.x * other.z,
120
+ this.x * other.y - other.x * this.y
121
+ );
122
+ }
123
+
124
+ /**
125
+ * If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
126
+ * if `other is a `number`, returns the result of scalar multiplication.
127
+ *
128
+ * @example
129
+ * ```
130
+ * Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
131
+ * ```
132
+ */
133
+ public scale(other: Vec3|number): Vec3 {
134
+ if (typeof other === 'number') {
135
+ return this.times(other);
136
+ }
137
+
138
+ return Vec3.of(
139
+ this.x * other.x,
140
+ this.y * other.y,
141
+ this.z * other.z,
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
147
+ * 90 degrees counter-clockwise.
148
+ */
149
+ public orthog(): Vec3 {
150
+ // If parallel to the z-axis
151
+ if (this.dot(Vec3.unitX) === 0 && this.dot(Vec3.unitY) === 0) {
152
+ return this.dot(Vec3.unitX) === 0 ? Vec3.unitX : this.cross(Vec3.unitX).normalized();
153
+ }
154
+
155
+ return this.cross(Vec3.unitZ.times(-1)).normalized();
156
+ }
157
+
158
+ /** Returns this plus a vector of length `distance` in `direction`. */
159
+ public extend(distance: number, direction: Vec3): Vec3 {
160
+ return this.plus(direction.normalized().times(distance));
161
+ }
162
+
163
+ /** Returns a vector `fractionTo` of the way to target from this. */
164
+ public lerp(target: Vec3, fractionTo: number): Vec3 {
165
+ return this.times(1 - fractionTo).plus(target.times(fractionTo));
166
+ }
167
+
168
+ /**
169
+ * `zip` Maps a component of this and a corresponding component of
170
+ * `other` to a component of the output vector.
171
+ *
172
+ * @example
173
+ * ```
174
+ * const a = Vec3.of(1, 2, 3);
175
+ * const b = Vec3.of(0.5, 2.1, 2.9);
176
+ *
177
+ * const zipped = a.zip(b, (aComponent, bComponent) => {
178
+ * return Math.min(aComponent, bComponent);
179
+ * });
180
+ *
181
+ * console.log(zipped.toString()); // → Vec(0.5, 2, 2.9)
182
+ * ```
183
+ */
184
+ public zip(
185
+ other: Vec3, zip: (componentInThis: number, componentInOther: number)=> number
186
+ ): Vec3 {
187
+ return Vec3.of(
188
+ zip(other.x, this.x),
189
+ zip(other.y, this.y),
190
+ zip(other.z, this.z)
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Returns a vector with each component acted on by `fn`.
196
+ *
197
+ * @example
198
+ * ```
199
+ * console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
200
+ * ```
201
+ */
202
+ public map(fn: (component: number, index: number)=> number): Vec3 {
203
+ return Vec3.of(
204
+ fn(this.x, 0), fn(this.y, 1), fn(this.z, 2)
205
+ );
206
+ }
207
+
208
+ public asArray(): [ number, number, number ] {
209
+ return [this.x, this.y, this.z];
210
+ }
211
+
212
+ /**
213
+ * [fuzz] The maximum difference between two components for this and [other]
214
+ * to be considered equal.
215
+ *
216
+ * @example
217
+ * ```
218
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 100); // → true
219
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 0.1); // → false
220
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3); // → true
221
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3.01); // → true
222
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
223
+ * ```
224
+ */
225
+ public eq(other: Vec3, fuzz: number = 1e-10): boolean {
226
+ for (let i = 0; i < 3; i++) {
227
+ if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ return true;
233
+ }
234
+
235
+ public toString(): string {
236
+ return `Vec(${this.x}, ${this.y}, ${this.z})`;
237
+ }
238
+
239
+
240
+ public static unitX = Vec3.of(1, 0, 0);
241
+ public static unitY = Vec3.of(0, 1, 0);
242
+ public static unitZ = Vec3.of(0, 0, 1);
243
+ public static zero = Vec3.of(0, 0, 0);
244
+ }
245
+ export default Vec3;
package/src/lib.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ```ts,runnable,console
3
+ * import { Vec2, Mat33, Rect2 } from '@js-draw/math';
4
+ *
5
+ * // Example: Rotate a vector 90 degrees about the z-axis
6
+ * const rotate90Degrees = Mat33.zRotation(Math.PI/2); // π/2 radians = 90 deg
7
+ * const moveUp = Mat33.translation(Vec2.of(1, 0));
8
+ * const moveUpThenRotate = rotate90Degrees.rightMul(moveUp);
9
+ * console.log(moveUpThenRotate.transformVec2(Vec2.of(1, 2)));
10
+ *
11
+ * // Example: Bounding box of some points
12
+ * console.log(Rect2.bboxOf([
13
+ * Vec2.of(1, 2), Vec2.of(3, 4), Vec2.of(-100, 1000),
14
+ * ]));
15
+ * ```
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+
20
+ export { LineSegment2 } from './shapes/LineSegment2';
21
+ export {
22
+ Path,
23
+
24
+ PathCommandType,
25
+ PathCommand,
26
+ LinePathCommand,
27
+ MoveToPathCommand,
28
+ QuadraticBezierPathCommand,
29
+ CubicBezierPathCommand,
30
+ } from './shapes/Path';
31
+ export { Rect2 } from './shapes/Rect2';
32
+ export { QuadraticBezier } from './shapes/QuadraticBezier';
33
+
34
+ export { Mat33, Mat33Array } from './Mat33';
35
+ export { Point2, Vec2 } from './Vec2';
36
+ export { Vec3 } from './Vec3';
37
+ export { Color4 } from './Color4';
38
+ export { toRoundedString } from './rounding';
39
+
40
+
41
+ // Note: All above exports cannot use `export { default as ... } from "..."` because this
42
+ // breaks TypeDoc -- TypeDoc otherwise labels any imports of these classes as `default`.
@@ -0,0 +1,39 @@
1
+
2
+ import solveQuadratic from './solveQuadratic';
3
+
4
+ describe('solveQuadratic', () => {
5
+ it('should solve linear equations', () => {
6
+ expect(solveQuadratic(0, 1, 2)).toMatchObject([ -2, -2 ]);
7
+ expect(solveQuadratic(0, 0, 2)[0]).toBeNaN();
8
+ });
9
+
10
+ it('should return both solutions to quadratic equations', () => {
11
+ type TestCase = [[number, number, number], [number, number]];
12
+
13
+ const testCases: TestCase[] = [
14
+ [ [ 1, 0, 0 ], [ 0, 0 ] ],
15
+ [ [ 2, 0, 0 ], [ 0, 0 ] ],
16
+
17
+ [ [ 1, 0, -1 ], [ 1, -1 ] ],
18
+ [ [ 1, 0, -4 ], [ 2, -2 ] ],
19
+ [ [ 1, 0, 4 ], [ NaN, NaN ] ],
20
+
21
+ [ [ 1, 1, 0 ], [ 0, -1 ] ],
22
+ [ [ 1, 2, 0 ], [ 0, -2 ] ],
23
+
24
+ [ [ 1, 2, 1 ], [ -1, -1 ] ],
25
+ [ [ -9, 2, 1/3 ], [ 1/3, -1/9 ] ],
26
+ ];
27
+
28
+ for (const [ testCase, solution ] of testCases) {
29
+ const foundSolutions = solveQuadratic(...testCase);
30
+ for (let i = 0; i < 2; i++) {
31
+ if (isNaN(solution[i]) && isNaN(foundSolutions[i])) {
32
+ expect(foundSolutions[i]).toBeNaN();
33
+ } else {
34
+ expect(foundSolutions[i]).toBeCloseTo(solution[i]);
35
+ }
36
+ }
37
+ }
38
+ });
39
+ });
@@ -0,0 +1,43 @@
1
+
2
+ /**
3
+ * Solves an equation of the form ax² + bx + c = 0.
4
+ * The larger solution is returned first.
5
+ *
6
+ * If there are no solutions, returns `[NaN, NaN]`. If there is one solution,
7
+ * repeats the solution twice in the result.
8
+ */
9
+ const solveQuadratic = (a: number, b: number, c: number): [number, number] => {
10
+ // See also https://en.wikipedia.org/wiki/Quadratic_formula
11
+
12
+ if (a === 0) {
13
+ let solution;
14
+
15
+ if (b === 0) {
16
+ solution = c === 0 ? 0 : NaN;
17
+ } else {
18
+ // Then we have bx + c = 0
19
+ // which implies bx = -c.
20
+ // Thus, x = -c/b
21
+ solution = -c / b;
22
+ }
23
+
24
+ return [ solution, solution ];
25
+ }
26
+
27
+ const discriminant = b * b - 4 * a * c;
28
+
29
+ if (discriminant < 0) {
30
+ return [ NaN, NaN ];
31
+ }
32
+
33
+ const rootDiscriminant = Math.sqrt(discriminant);
34
+ const solution1 = (-b + rootDiscriminant) / (2 * a);
35
+ const solution2 = (-b - rootDiscriminant) / (2 * a);
36
+
37
+ if (solution1 > solution2) {
38
+ return [ solution1, solution2 ];
39
+ } else {
40
+ return [ solution2, solution1 ];
41
+ }
42
+ };
43
+ export default solveQuadratic;
@@ -0,0 +1,65 @@
1
+ import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';
2
+
3
+ describe('toRoundedString', () => {
4
+ it('should round up numbers endings similar to .999999999999999', () => {
5
+ expect(toRoundedString(0.999999999)).toBe('1');
6
+ expect(toRoundedString(0.899999999)).toBe('.9');
7
+ expect(toRoundedString(9.999999999)).toBe('10');
8
+ expect(toRoundedString(-10.999999999)).toBe('-11');
9
+ });
10
+
11
+ it('should round up numbers similar to 10.999999998', () => {
12
+ expect(toRoundedString(10.999999998)).toBe('11');
13
+ });
14
+
15
+ it('should round strings with multiple digits after the ending decimal points', () => {
16
+ expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
17
+ expect(toRoundedString(4.06425600000023)).toBe('4.064256');
18
+ });
19
+
20
+ it('should round down strings ending endings similar to .00000001', () => {
21
+ expect(toRoundedString(10.00000001)).toBe('10');
22
+ expect(toRoundedString(-30.00000001)).toBe('-30');
23
+ expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
24
+ });
25
+
26
+ it('should not round numbers insufficiently close to the next', () => {
27
+ expect(toRoundedString(-10.9999)).toBe('-10.9999');
28
+ expect(toRoundedString(-10.0001)).toBe('-10.0001');
29
+ expect(toRoundedString(-10.123499)).toBe('-10.123499');
30
+ expect(toRoundedString(0.00123499)).toBe('.00123499');
31
+ });
32
+ });
33
+
34
+ it('toStringOfSamePrecision', () => {
35
+ expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
36
+ expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
37
+ expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
38
+ expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
39
+ expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
40
+ expect(toStringOfSamePrecision(-1.99999, '1.1', '5.32')).toBe('-2');
41
+ expect(toStringOfSamePrecision(1.99999, '1.1', '5.32')).toBe('2');
42
+ expect(toStringOfSamePrecision(1.89999, '1.1', '5.32')).toBe('1.9');
43
+ expect(toStringOfSamePrecision(9.99999999, '-1.1234')).toBe('10');
44
+ expect(toStringOfSamePrecision(9.999999998999996, '100')).toBe('10');
45
+ expect(toStringOfSamePrecision(0.000012345, '0.000012')).toBe('.000012');
46
+ expect(toStringOfSamePrecision(0.000012645, '.000012')).toBe('.000013');
47
+ expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
48
+ expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
49
+ expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
50
+ expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
51
+ });
52
+
53
+ it('cleanUpNumber', () => {
54
+ expect(cleanUpNumber('000.0000')).toBe('0');
55
+ expect(cleanUpNumber('-000.0000')).toBe('0');
56
+ expect(cleanUpNumber('0.0000')).toBe('0');
57
+ expect(cleanUpNumber('0.001')).toBe('.001');
58
+ expect(cleanUpNumber('-0.001')).toBe('-.001');
59
+ expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
60
+ expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
61
+ expect(cleanUpNumber('1234')).toBe('1234');
62
+ expect(cleanUpNumber('1234.5')).toBe('1234.5');
63
+ expect(cleanUpNumber('1234.500')).toBe('1234.5');
64
+ expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
65
+ });
@@ -0,0 +1,167 @@
1
+ // @packageDocumentation @internal
2
+
3
+ // Clean up stringified numbers
4
+ export const cleanUpNumber = (text: string) => {
5
+ // Regular expression substitions can be somewhat expensive. Only do them
6
+ // if necessary.
7
+
8
+ if (text.indexOf('e') > 0) {
9
+ // Round to zero.
10
+ if (text.match(/[eE][-]\d{2,}$/)) {
11
+ return '0';
12
+ }
13
+ }
14
+
15
+ const lastChar = text.charAt(text.length - 1);
16
+ if (lastChar === '0' || lastChar === '.') {
17
+ // Remove trailing zeroes
18
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
19
+ text = text.replace(/[.]0+$/, '.');
20
+
21
+ // Remove trailing period
22
+ text = text.replace(/[.]$/, '');
23
+ }
24
+
25
+ const firstChar = text.charAt(0);
26
+ if (firstChar === '0' || firstChar === '-') {
27
+ // Remove unnecessary leading zeroes.
28
+ text = text.replace(/^(0+)[.]/, '.');
29
+ text = text.replace(/^-(0+)[.]/, '-.');
30
+ text = text.replace(/^(-?)0+$/, '$10');
31
+ }
32
+
33
+ if (text === '-0') {
34
+ return '0';
35
+ }
36
+
37
+ return text;
38
+ };
39
+
40
+ /**
41
+ * Converts `num` to a string, removing trailing digits that were likely caused by
42
+ * precision errors.
43
+ *
44
+ * @example
45
+ * ```ts,runnable,console
46
+ * import { toRoundedString } from '@js-draw/math';
47
+ *
48
+ * console.log('Rounded: ', toRoundedString(1.000000011));
49
+ * ```
50
+ */
51
+ export const toRoundedString = (num: number): string => {
52
+ // Try to remove rounding errors. If the number ends in at least three/four zeroes
53
+ // (or nines) just one or two digits, it's probably a rounding error.
54
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
55
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
56
+
57
+ let text = num.toString(10);
58
+ if (text.indexOf('.') === -1) {
59
+ return text;
60
+ }
61
+
62
+ const roundingDownMatch = hasRoundingDownExp.exec(text);
63
+ if (roundingDownMatch) {
64
+ const negativeSign = roundingDownMatch[1];
65
+ const postDecimalString = roundingDownMatch[3];
66
+ const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
67
+ const postDecimal = parseInt(postDecimalString, 10);
68
+ const preDecimal = parseInt(roundingDownMatch[2], 10);
69
+
70
+ const origPostDecimalString = roundingDownMatch[3];
71
+
72
+ let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
73
+ let carry = 0;
74
+ if (newPostDecimal.length > postDecimal.toString().length) {
75
+ // Left-shift
76
+ newPostDecimal = newPostDecimal.substring(1);
77
+ carry = 1;
78
+ }
79
+
80
+ // parseInt(...).toString() removes leading zeroes. Add them back.
81
+ while (newPostDecimal.length < origPostDecimalString.length) {
82
+ newPostDecimal = carry.toString(10) + newPostDecimal;
83
+ carry = 0;
84
+ }
85
+
86
+ text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
87
+ }
88
+
89
+ text = text.replace(fixRoundingUpExp, '$1');
90
+
91
+ return cleanUpNumber(text);
92
+ };
93
+
94
+ const numberExp = /^([-]?)(\d*)[.](\d+)$/;
95
+ export const getLenAfterDecimal = (numberAsString: string) => {
96
+ const numberMatch = numberExp.exec(numberAsString);
97
+ if (!numberMatch) {
98
+ // If not a match, either the number is exponential notation (or is something
99
+ // like NaN or Infinity)
100
+ if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
101
+ return -1;
102
+ // Or it has no decimal point
103
+ } else {
104
+ return 0;
105
+ }
106
+ }
107
+
108
+ const afterDecimalLen = numberMatch[3].length;
109
+ return afterDecimalLen;
110
+ };
111
+
112
+ // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
113
+ export const toStringOfSamePrecision = (num: number, ...references: string[]): string => {
114
+ const text = num.toString(10);
115
+ const textMatch = numberExp.exec(text);
116
+ if (!textMatch) {
117
+ return text;
118
+ }
119
+
120
+ let decimalPlaces = -1;
121
+ for (const reference of references) {
122
+ decimalPlaces = Math.max(getLenAfterDecimal(reference), decimalPlaces);
123
+ }
124
+
125
+ if (decimalPlaces === -1) {
126
+ return toRoundedString(num);
127
+ }
128
+
129
+ // Make text's after decimal length match [afterDecimalLen].
130
+ let postDecimal = textMatch[3].substring(0, decimalPlaces);
131
+ let preDecimal = textMatch[2];
132
+ const nextDigit = textMatch[3].charAt(decimalPlaces);
133
+
134
+ if (nextDigit !== '') {
135
+ const asNumber = parseInt(nextDigit, 10);
136
+ if (asNumber >= 5) {
137
+ // Don't attempt to parseInt() an empty string.
138
+ if (postDecimal.length > 0) {
139
+ const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
140
+
141
+ let leadingZeroes = '';
142
+ let postLeading = postDecimal;
143
+ if (leadingZeroMatch) {
144
+ leadingZeroes = leadingZeroMatch[1];
145
+ postLeading = leadingZeroMatch[2];
146
+ }
147
+
148
+ postDecimal = (parseInt(postDecimal) + 1).toString();
149
+
150
+ // If postDecimal got longer, remove leading zeroes if possible
151
+ if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
152
+ leadingZeroes = leadingZeroes.substring(1);
153
+ }
154
+
155
+ postDecimal = leadingZeroes + postDecimal;
156
+ }
157
+
158
+ if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
159
+ preDecimal = (parseInt(preDecimal) + 1).toString();
160
+ postDecimal = postDecimal.substring(1);
161
+ }
162
+ }
163
+ }
164
+
165
+ const negativeSign = textMatch[1];
166
+ return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
167
+ };
@@ -0,0 +1,63 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import { Point2 } from '../Vec2';
3
+ import Rect2 from './Rect2';
4
+
5
+ abstract class Abstract2DShape {
6
+ protected static readonly smallValue = 1e-12;
7
+
8
+ /**
9
+ * @returns the distance from `point` to this shape. If `point` is within this shape,
10
+ * this returns the distance from `point` to the edge of this shape.
11
+ *
12
+ * @see {@link signedDistance}
13
+ */
14
+ public distance(point: Point2) {
15
+ return Math.abs(this.signedDistance(point));
16
+ }
17
+
18
+ /**
19
+ * Computes the [signed distance function](https://en.wikipedia.org/wiki/Signed_distance_function)
20
+ * for this shape.
21
+ */
22
+ public abstract signedDistance(point: Point2): number;
23
+
24
+ /**
25
+ * @returns points at which this shape intersects the given `lineSegment`.
26
+ *
27
+ * If this is a closed shape, returns points where the given `lineSegment` intersects
28
+ * the **boundary** of this.
29
+ */
30
+ public abstract intersectsLineSegment(lineSegment: LineSegment2): Point2[];
31
+
32
+ /**
33
+ * Returns `true` if and only if the given `point` is contained within this shape.
34
+ *
35
+ * `epsilon` is a small number used to counteract floating point error. Thus, if
36
+ * `point` is within `epsilon` of the inside of this shape, `containsPoint` may also
37
+ * return `true`.
38
+ *
39
+ * The default implementation relies on `signedDistance`.
40
+ * Subclasses may override this method to provide a more efficient implementation.
41
+ */
42
+ public containsPoint(point: Point2, epsilon: number = Abstract2DShape.smallValue): boolean {
43
+ return this.signedDistance(point) < epsilon;
44
+ }
45
+
46
+ /**
47
+ * Returns a bounding box that precisely fits the content of this shape.
48
+ */
49
+ public abstract getTightBoundingBox(): Rect2;
50
+
51
+ /**
52
+ * Returns a bounding box that **loosely** fits the content of this shape.
53
+ *
54
+ * The result of this call can be larger than the result of {@link getTightBoundingBox},
55
+ * **but should not be smaller**. Thus, a call to `getLooseBoundingBox` can be significantly
56
+ * faster than a call to {@link getTightBoundingBox} for some shapes.
57
+ */
58
+ public getLooseBoundingBox(): Rect2 {
59
+ return this.getTightBoundingBox();
60
+ }
61
+ }
62
+
63
+ export default Abstract2DShape;