@khanacademy/kmath 0.0.0-PR443-20230328215601

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.
package/src/ray.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Ray Utils
3
+ * A ray (→) is an array of an endpoint and another point along the ray.
4
+ * For example, [[0, 0], [1, 0]] is the ray starting at the origin and
5
+ * traveling along the positive x-axis.
6
+ */
7
+
8
+ import * as kpoint from "./point";
9
+ import * as kvector from "./vector";
10
+
11
+ import type {Point} from "./point";
12
+
13
+ export type Ray = [Point, Point];
14
+
15
+ export function equal(ray1: Ray, ray2: Ray, tolerance?: number): boolean {
16
+ // Compare the directions of the rays
17
+ const v1 = kvector.subtract(ray1[1], ray1[0]);
18
+ const v2 = kvector.subtract(ray2[1], ray2[0]);
19
+
20
+ const sameOrigin = kpoint.equal(ray1[0], ray2[0]);
21
+ const codirectional = kvector.codirectional(v1, v2, tolerance);
22
+
23
+ return sameOrigin && codirectional;
24
+ }
package/src/vector.ts ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Vector Utils
3
+ * A vector is an array of numbers e.g. [0, 3, 4].
4
+ */
5
+
6
+ import _ from "underscore";
7
+
8
+ import * as knumber from "./number";
9
+
10
+ type Vector = ReadonlyArray<number>;
11
+
12
+ function arraySum(array: ReadonlyArray<number>): number {
13
+ return array.reduce((memo, arg) => memo + arg, 0);
14
+ }
15
+
16
+ function arrayProduct(array: ReadonlyArray<number>): number {
17
+ return array.reduce((memo, arg) => memo * arg, 1);
18
+ }
19
+
20
+ /**
21
+ * Checks if the given vector contains only numbers and, optionally, is of the
22
+ * right dimension (length).
23
+ *
24
+ * is([1, 2, 3]) -> true
25
+ * is([1, "Hello", 3]) -> false
26
+ * is([1, 2, 3], 1) -> false
27
+ */
28
+ export function is<T>(vec: ReadonlyArray<T>, dimension?: number): boolean {
29
+ if (!_.isArray(vec)) {
30
+ return false;
31
+ }
32
+ if (dimension !== undefined && vec.length !== dimension) {
33
+ return false;
34
+ }
35
+ return vec.every(knumber.is);
36
+ }
37
+
38
+ // Normalize to a unit vector
39
+ export function normalize<V extends Vector>(v: V): V {
40
+ return scale(v, 1 / length(v));
41
+ }
42
+
43
+ // Length/magnitude of a vector
44
+ export function length(v: Vector): number {
45
+ return Math.sqrt(dot(v, v));
46
+ }
47
+ // Dot product of two vectors
48
+ export function dot(a: Vector, b: Vector): number {
49
+ const zipped = _.zip(a, b);
50
+ const multiplied = zipped.map(arrayProduct);
51
+ return arraySum(multiplied);
52
+ }
53
+
54
+ /* vector-add multiple [x, y] coords/vectors
55
+ *
56
+ * add([1, 2], [3, 4]) -> [4, 6]
57
+ */
58
+ export function add<V extends Vector>(...vecs: ReadonlyArray<V>): V {
59
+ const zipped = _.zip(...vecs);
60
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
61
+ return zipped.map(arraySum);
62
+ }
63
+
64
+ export function subtract<V extends Vector>(v1: V, v2: V): V {
65
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
66
+ return _.zip(v1, v2).map((dim) => dim[0] - dim[1]);
67
+ }
68
+
69
+ export function negate<V extends Vector>(v: V): V {
70
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
71
+ return v.map((x) => {
72
+ return -x;
73
+ });
74
+ }
75
+
76
+ // Scale a vector
77
+ export function scale<V extends Vector>(v1: V, scalar: number): V {
78
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
79
+ return v1.map((x) => {
80
+ return x * scalar;
81
+ });
82
+ }
83
+
84
+ export function equal(v1: Vector, v2: Vector, tolerance?: number): boolean {
85
+ // _.zip will nicely deal with the lengths, going through
86
+ // the length of the longest vector. knumber.equal then
87
+ // returns false for any number compared to the undefined
88
+ // passed in if one of the vectors is shorter.
89
+ return _.zip(v1, v2).every((pair) =>
90
+ knumber.equal(pair[0], pair[1], tolerance),
91
+ );
92
+ }
93
+
94
+ export function codirectional(
95
+ v1: Vector,
96
+ v2: Vector,
97
+ tolerance?: number,
98
+ ): boolean {
99
+ // The origin is trivially codirectional with all other vectors.
100
+ // This gives nice semantics for codirectionality between points when
101
+ // comparing their difference vectors.
102
+ if (
103
+ knumber.equal(length(v1), 0, tolerance) ||
104
+ knumber.equal(length(v2), 0, tolerance)
105
+ ) {
106
+ return true;
107
+ }
108
+
109
+ v1 = normalize(v1);
110
+ v2 = normalize(v2);
111
+
112
+ return equal(v1, v2, tolerance);
113
+ }
114
+
115
+ export function collinear(v1: Vector, v2: Vector, tolerance?: number): boolean {
116
+ return (
117
+ codirectional(v1, v2, tolerance) ||
118
+ codirectional(v1, negate(v2), tolerance)
119
+ );
120
+ }
121
+
122
+ // TODO(jeremy) These coordinate conversion functions really only handle 2D points (ie. [number, number])
123
+
124
+ // Convert a cartesian coordinate into a radian polar coordinate
125
+ export function polarRadFromCart(
126
+ v: ReadonlyArray<number>,
127
+ ): ReadonlyArray<number> {
128
+ const radius = length(v);
129
+ let theta = Math.atan2(v[1], v[0]);
130
+
131
+ // Convert angle range from [-pi, pi] to [0, 2pi]
132
+ if (theta < 0) {
133
+ theta += 2 * Math.PI;
134
+ }
135
+
136
+ return [radius, theta];
137
+ }
138
+
139
+ // Converts a cartesian coordinate into a degree polar coordinate
140
+ export function polarDegFromCart(
141
+ v: ReadonlyArray<number>,
142
+ ): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
143
+ const polar = polarRadFromCart(v);
144
+ return [polar[0], (polar[1] * 180) / Math.PI];
145
+ }
146
+
147
+ /* Convert a polar coordinate into a cartesian coordinate
148
+ *
149
+ * Examples:
150
+ * cartFromPolarRad(5, Math.PI)
151
+ */
152
+ export function cartFromPolarRad(
153
+ radius: number,
154
+ theta = 0,
155
+ ): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
156
+ return [radius * Math.cos(theta), radius * Math.sin(theta)];
157
+ }
158
+
159
+ /* Convert a polar coordinate into a cartesian coordinate
160
+ *
161
+ * Examples:
162
+ * cartFromPolarDeg(5, 30)
163
+ */
164
+ export function cartFromPolarDeg(
165
+ radius: number,
166
+ theta = 0,
167
+ ): ReadonlyArray<number> {
168
+ return cartFromPolarRad(radius, (theta * Math.PI) / 180);
169
+ }
170
+
171
+ // Rotate vector
172
+ export function rotateRad(
173
+ v: ReadonlyArray<number>,
174
+ theta: number,
175
+ ): ReadonlyArray<number> {
176
+ const polar = polarRadFromCart(v);
177
+ const angle = polar[1] + theta;
178
+ return cartFromPolarRad(polar[0], angle);
179
+ }
180
+
181
+ export function rotateDeg(
182
+ v: ReadonlyArray<number>,
183
+ theta: number,
184
+ ): ReadonlyArray<number> {
185
+ const polar = polarDegFromCart(v);
186
+ const angle = polar[1] + theta;
187
+ return cartFromPolarDeg(polar[0], angle);
188
+ }
189
+
190
+ // Angle between two vectors
191
+ export function angleRad(v1: Vector, v2: Vector): number {
192
+ return Math.acos(dot(v1, v2) / (length(v1) * length(v2)));
193
+ }
194
+
195
+ export function angleDeg(v1: Vector, v2: Vector): number {
196
+ return (angleRad(v1, v2) * 180) / Math.PI;
197
+ }
198
+
199
+ // Vector projection of v1 onto v2
200
+ export function projection<V extends Vector>(v1: V, v2: V): V {
201
+ const scalar = dot(v1, v2) / dot(v2, v2);
202
+ return scale(v2, scalar);
203
+ }
204
+
205
+ // Round each number to a certain number of decimal places
206
+ export function round<V extends Vector>(vec: V, precision: V | number): V {
207
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
208
+ return vec.map((elem, i) =>
209
+ // $FlowFixMe[prop-missing]
210
+ // $FlowFixMe[incompatible-call]
211
+ knumber.round(elem, precision[i] || precision),
212
+ );
213
+ }
214
+
215
+ // Round each number to the nearest increment
216
+ export function roundTo<V extends Vector>(vec: V, increment: V | number): V {
217
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
218
+ return vec.map((elem, i) =>
219
+ // $FlowFixMe[prop-missing]
220
+ // $FlowFixMe[incompatible-call]
221
+ knumber.roundTo(elem, increment[i] || increment),
222
+ );
223
+ }
224
+
225
+ export function floorTo<V extends Vector>(vec: V, increment: V | number): V {
226
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
227
+ return vec.map((elem, i) =>
228
+ // $FlowFixMe[prop-missing]
229
+ // $FlowFixMe[incompatible-call]
230
+ knumber.floorTo(elem, increment[i] || increment),
231
+ );
232
+ }
233
+
234
+ export function ceilTo<V extends Vector>(vec: V, increment: V | number): V {
235
+ // @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
236
+ return vec.map((elem, i) =>
237
+ // $FlowFixMe[prop-missing]
238
+ // $FlowFixMe[incompatible-call]
239
+ knumber.ceilTo(elem, increment[i] || increment),
240
+ );
241
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "exclude": ["dist", "**/*.test.*", "**/*.stories.*", "**/*.cypress.*"],
3
+ "extends": "../tsconfig-shared.json",
4
+ "compilerOptions": {
5
+ "outDir": "./dist",
6
+ "rootDir": "src",
7
+ },
8
+ "references": []
9
+ }