@khanacademy/kmath 0.0.0-PR973-20240207193807 → 0.0.0-PR973-20240207194831
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/.babelrc.js +8 -0
- package/.eslintrc.js +12 -0
- package/CHANGELOG.md +130 -0
- package/logo.svg +1 -0
- package/package.json +2 -5
- package/src/__tests__/line.test.ts +117 -0
- package/src/__tests__/number.test.ts +119 -0
- package/src/__tests__/point.test.ts +48 -0
- package/src/__tests__/vector.test.ts +145 -0
- package/src/index.ts +7 -0
- package/src/line.ts +42 -0
- package/src/logo.ts +39 -0
- package/src/number.ts +104 -0
- package/src/point.ts +102 -0
- package/src/ray.ts +24 -0
- package/src/vector.ts +248 -0
- package/src/version.ts +10 -0
- package/tsconfig-build.json +10 -0
package/.babelrc.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HACK(somewhatabstract): Due to https://github.com/facebook/jest/issues/11741,
|
|
3
|
+
* we need to have this file, or updating inline snapshots can fail rather
|
|
4
|
+
* cryptically.
|
|
5
|
+
*
|
|
6
|
+
* We should remove this when jest is fixed.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = require("../../config/build/babel.config");
|
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
/* eslint-disable import/no-commonjs */
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
rules: {
|
|
7
|
+
"import/no-extraneous-dependencies": [
|
|
8
|
+
"error",
|
|
9
|
+
{packageDir: [__dirname, path.join(__dirname, "../../")]},
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
};
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @khanacademy/kmath
|
|
2
|
+
|
|
3
|
+
## 0.0.0-PR973-20240207194831
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#973](https://github.com/Khan/perseus/pull/973) [`2ffc6971`](https://github.com/Khan/perseus/commit/2ffc6971e7003cbc64a8a3988337c6ca6019c9db) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Include 'types' key in package.json and move types to 'types' package subdirectory
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`2ffc6971`](https://github.com/Khan/perseus/commit/2ffc6971e7003cbc64a8a3988337c6ca6019c9db)]:
|
|
10
|
+
- @khanacademy/perseus-core@0.0.0-PR973-20240207194831
|
|
11
|
+
|
|
12
|
+
## 0.1.12
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#971](https://github.com/Khan/perseus/pull/971) [`90ff7a48`](https://github.com/Khan/perseus/commit/90ff7a483b01552a556c7852427e98153cc20417) Thanks [@benchristel](https://github.com/benchristel)! - Remove source files from the distributed NPM package
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`90ff7a48`](https://github.com/Khan/perseus/commit/90ff7a483b01552a556c7852427e98153cc20417)]:
|
|
19
|
+
- @khanacademy/perseus-core@1.4.2
|
|
20
|
+
|
|
21
|
+
## 0.1.11
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- [#964](https://github.com/Khan/perseus/pull/964) [`d8fbc251`](https://github.com/Khan/perseus/commit/d8fbc25170bd671ad984893553f79f44e3a0d048) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Refactor Movable to be an ES6 class
|
|
26
|
+
|
|
27
|
+
## 0.1.10
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- [#954](https://github.com/Khan/perseus/pull/954) [`43820ff9`](https://github.com/Khan/perseus/commit/43820ff9ce391682b951279a4c6fb9e3db966eb9) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Fix @ts-expect-errors related to Graphie
|
|
32
|
+
|
|
33
|
+
## 0.1.9
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- [#949](https://github.com/Khan/perseus/pull/949) [`59ce1e09`](https://github.com/Khan/perseus/commit/59ce1e0990cecd1123d7b9a671b032fcd03ce1b1) Thanks [@benchristel](https://github.com/benchristel)! - Internal: use built-in JS functions instead of underscore
|
|
38
|
+
|
|
39
|
+
## 0.1.8
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- Updated dependencies [[`1f4e17ba`](https://github.com/Khan/perseus/commit/1f4e17ba77e1491523813655af18a70285a25989), [`8857950b`](https://github.com/Khan/perseus/commit/8857950bdeeb6e13bc3766b1c6545289b21cbe2a)]:
|
|
44
|
+
- @khanacademy/perseus-core@1.4.1
|
|
45
|
+
|
|
46
|
+
## 0.1.7
|
|
47
|
+
|
|
48
|
+
### Patch Changes
|
|
49
|
+
|
|
50
|
+
- [#814](https://github.com/Khan/perseus/pull/814) [`105d2060`](https://github.com/Khan/perseus/commit/105d20603d935d35cff237b17f0bfb57ca751e4c) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Minor build change to how we provide Typescript type definitions (should be no change to build output).
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [[`a91c84fe`](https://github.com/Khan/perseus/commit/a91c84fe53827ff4333220777a9918882b7fe9f0), [`105d2060`](https://github.com/Khan/perseus/commit/105d20603d935d35cff237b17f0bfb57ca751e4c)]:
|
|
53
|
+
- @khanacademy/perseus-core@1.4.0
|
|
54
|
+
|
|
55
|
+
## 0.1.6
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- Updated dependencies [[`79403e06`](https://github.com/Khan/perseus/commit/79403e06eedb597d7818d6c858bbba6f51ff3fe1)]:
|
|
60
|
+
- @khanacademy/perseus-core@1.3.0
|
|
61
|
+
|
|
62
|
+
## 0.1.5
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [[`376eb0e4`](https://github.com/Khan/perseus/commit/376eb0e4aaaa4c7a90fd6107a84bb74d382b077c)]:
|
|
67
|
+
- @khanacademy/perseus-core@1.2.0
|
|
68
|
+
|
|
69
|
+
## 0.1.4
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- Updated dependencies [22a9c408]
|
|
74
|
+
- @khanacademy/perseus-core@1.1.2
|
|
75
|
+
|
|
76
|
+
## 0.1.3
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- 55d4cd00: Print package name and version when loaded in the page
|
|
81
|
+
- Updated dependencies [55d4cd00]
|
|
82
|
+
- @khanacademy/perseus-core@1.1.1
|
|
83
|
+
|
|
84
|
+
## 0.1.2
|
|
85
|
+
|
|
86
|
+
### Patch Changes
|
|
87
|
+
|
|
88
|
+
- ce5e6297: Upgrade wonder-blocks deps to package versions without Flow types
|
|
89
|
+
|
|
90
|
+
## 0.1.1
|
|
91
|
+
|
|
92
|
+
### Patch Changes
|
|
93
|
+
|
|
94
|
+
- 1f062e98: Bump all package versions since the build settings have been updated
|
|
95
|
+
|
|
96
|
+
## 0.1.0
|
|
97
|
+
|
|
98
|
+
### Minor Changes
|
|
99
|
+
|
|
100
|
+
- 53fd3768: Migrate source code to TypeScript
|
|
101
|
+
|
|
102
|
+
## 0.0.8
|
|
103
|
+
|
|
104
|
+
### Patch Changes
|
|
105
|
+
|
|
106
|
+
- f567f660: Update the eslint config to look at both the package.json for the package and the one from the root
|
|
107
|
+
|
|
108
|
+
## 0.0.7
|
|
109
|
+
|
|
110
|
+
### Patch Changes
|
|
111
|
+
|
|
112
|
+
- bf180fe1: Fix our use of import/no-extraneous-dependencies
|
|
113
|
+
|
|
114
|
+
## 0.0.6
|
|
115
|
+
|
|
116
|
+
### Patch Changes
|
|
117
|
+
|
|
118
|
+
- 98d283ff: Fix storybook
|
|
119
|
+
|
|
120
|
+
## 0.0.5
|
|
121
|
+
|
|
122
|
+
### Patch Changes
|
|
123
|
+
|
|
124
|
+
- 1c1f7717: Enhance Flow types for kmath's vector and point
|
|
125
|
+
|
|
126
|
+
## 0.0.4
|
|
127
|
+
|
|
128
|
+
### Patch Changes
|
|
129
|
+
|
|
130
|
+
- a46d62bf: Remove .tgz file that shouldn't be packaged
|
package/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400" height="400" viewBox="0 0 400 400"><defs/><ellipse cx="200" cy="200" rx="200" ry="200" fill="#28ae7b" stroke="none" style="stroke-width: 2px" stroke-width="2"/><path fill="#ffffff" stroke="#ffffff" d="M 293.05,242.4 C 295.7,226.5 324.85,202.65 332.8,200 C 324.85,197.35 295.7,173.5 293.05,157.6" transform="" style="stroke-width: 25px;stroke-linejoin: round;stroke-linecap: round" stroke-width="25" stroke-linejoin="round" stroke-linecap="round"/><path fill="#ffffff" stroke="#ffffff" d="M 80,200 S 80,200 332.05,200" style="stroke-width: 25px" stroke-width="25"/><path fill="#ffffff" stroke="#ffffff" d="M 200,320 L 200,80" style="stroke-width: 25px" stroke-width="25"/></svg>
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Khan Academy's Javascript Numeric Math Utilities",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.0.0-PR973-
|
|
6
|
+
"version": "0.0.0-PR973-20240207194831",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -18,14 +18,11 @@
|
|
|
18
18
|
"main": "dist/index.js",
|
|
19
19
|
"source": "src/index.ts",
|
|
20
20
|
"types": "dist/types/index.d.ts",
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
21
|
"scripts": {
|
|
25
22
|
"test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'"
|
|
26
23
|
},
|
|
27
24
|
"dependencies": {
|
|
28
|
-
"@khanacademy/perseus-core": "0.0.0-PR973-
|
|
25
|
+
"@khanacademy/perseus-core": "0.0.0-PR973-20240207194831"
|
|
29
26
|
},
|
|
30
27
|
"devDependencies": {
|
|
31
28
|
"perseus-build-settings": "^0.3.0",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as line from "../line";
|
|
2
|
+
|
|
3
|
+
describe("kline", function () {
|
|
4
|
+
it("finds distance to point", () => {
|
|
5
|
+
const result = line.distanceToPoint(
|
|
6
|
+
[
|
|
7
|
+
[-5, 0],
|
|
8
|
+
[5, 0],
|
|
9
|
+
],
|
|
10
|
+
[0, 5],
|
|
11
|
+
);
|
|
12
|
+
expect(result).toBe(5);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("reflects a point", () => {
|
|
16
|
+
const result = line.reflectPoint(
|
|
17
|
+
[
|
|
18
|
+
[-5, -5],
|
|
19
|
+
[5, 5],
|
|
20
|
+
],
|
|
21
|
+
[0, 5],
|
|
22
|
+
);
|
|
23
|
+
expect(result).toEqual([5, 0]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("finds the midpoint", () => {
|
|
27
|
+
const result = line.midpoint([
|
|
28
|
+
[-5, -5],
|
|
29
|
+
[5, 5],
|
|
30
|
+
]);
|
|
31
|
+
expect(result).toEqual([0, 0]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("two identical lines should be equal", function () {
|
|
35
|
+
const result = line.equal(
|
|
36
|
+
[
|
|
37
|
+
[1, 1],
|
|
38
|
+
[3, 3],
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
[1, 1],
|
|
42
|
+
[3, 3],
|
|
43
|
+
],
|
|
44
|
+
);
|
|
45
|
+
expect(result).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("two parallel lines should not be equal", function () {
|
|
49
|
+
const result = line.equal(
|
|
50
|
+
[
|
|
51
|
+
[1, 1],
|
|
52
|
+
[3, 3],
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
[1, 2],
|
|
56
|
+
[3, 4],
|
|
57
|
+
],
|
|
58
|
+
);
|
|
59
|
+
expect(result).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("two intersecting lines should not be equal", function () {
|
|
63
|
+
const result = line.equal(
|
|
64
|
+
[
|
|
65
|
+
[1, 1],
|
|
66
|
+
[3, 3],
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
[1, 1],
|
|
70
|
+
[3, 4],
|
|
71
|
+
],
|
|
72
|
+
);
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("two collinear lines should be equal #1", function () {
|
|
77
|
+
const result = line.equal(
|
|
78
|
+
[
|
|
79
|
+
[1, 1],
|
|
80
|
+
[3, 3],
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
[0, 0],
|
|
84
|
+
[5, 5],
|
|
85
|
+
],
|
|
86
|
+
);
|
|
87
|
+
expect(result).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("two collinear lines should be equal #2", function () {
|
|
91
|
+
const result = line.equal(
|
|
92
|
+
[
|
|
93
|
+
[4, 4],
|
|
94
|
+
[5, 5],
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
[0, 0],
|
|
98
|
+
[1, 1],
|
|
99
|
+
],
|
|
100
|
+
);
|
|
101
|
+
expect(result).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("two collinear lines should be equal #3", function () {
|
|
105
|
+
const result = line.equal(
|
|
106
|
+
[
|
|
107
|
+
[0, 0],
|
|
108
|
+
[1, 1],
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
[3, 3],
|
|
112
|
+
[6, 6],
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
expect(result).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {describe, it} from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import * as number from "../number";
|
|
4
|
+
|
|
5
|
+
describe("knumber", function () {
|
|
6
|
+
it.each([3, Math.PI, 6.28, 5e10, 1 / 0])("is a number: %s", (num: any) => {
|
|
7
|
+
expect(number.is(num)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it.each(["10", 0 / 0, NaN])("is not a number:%s", (num: any) => {
|
|
11
|
+
expect(number.is(num)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("two equal numbers should be equal", function () {
|
|
15
|
+
const result = number.equal(1 / 3, (1 / 90) * 30);
|
|
16
|
+
expect(result).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("two different numbers should not be equal", function () {
|
|
20
|
+
const result = number.equal(1 / 3, 1.333333);
|
|
21
|
+
expect(result).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("Infinity should equal Infinity", function () {
|
|
25
|
+
const result = number.equal(
|
|
26
|
+
Number.POSITIVE_INFINITY,
|
|
27
|
+
Number.POSITIVE_INFINITY,
|
|
28
|
+
);
|
|
29
|
+
expect(result).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("+Infinity should not equal -Infinity", function () {
|
|
33
|
+
const result = number.equal(
|
|
34
|
+
Number.POSITIVE_INFINITY,
|
|
35
|
+
Number.NEGATIVE_INFINITY,
|
|
36
|
+
);
|
|
37
|
+
expect(result).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("sign(0) should be 0", function () {
|
|
41
|
+
expect(number.sign(0)).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("sign(-0.0) should be 0", function () {
|
|
45
|
+
expect(number.sign(-0.0)).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("sign(3.2) should be 1", function () {
|
|
49
|
+
expect(number.sign(3.2)).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("sign(-2.8) should be -1", function () {
|
|
53
|
+
expect(number.sign(-2.8)).toBe(-1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("isInteger(-2.8) should be false", function () {
|
|
57
|
+
expect(number.isInteger(-2.8)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("isInteger(-2) should be true", function () {
|
|
61
|
+
expect(number.isInteger(-2)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("isInteger(10.0) should be true", () => {
|
|
65
|
+
expect(number.isInteger(10)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("rounds to correct precision", () => {
|
|
69
|
+
expect(number.round(0.06793, 4)).toBe(0.0679);
|
|
70
|
+
expect(number.round(0.06793, 3)).toBe(0.068);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rounds to correct interval", () => {
|
|
74
|
+
expect(number.roundTo(83, 5)).toBe(85);
|
|
75
|
+
expect(number.roundTo(2.3, 0.5)).toBe(2.5);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("floors to the correct interval", () => {
|
|
79
|
+
expect(number.floorTo(83, 5)).toBe(80);
|
|
80
|
+
expect(number.floorTo(2.3, 0.5)).toBe(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("ceils to the correct interval", () => {
|
|
84
|
+
expect(number.ceilTo(81, 5)).toBe(85);
|
|
85
|
+
expect(number.ceilTo(2.1, 0.5)).toBe(2.5);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("toFraction(-2) should be -2/1", function () {
|
|
89
|
+
expect(number.toFraction(-2)).toStrictEqual([-2, 1]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("toFraction(-2.5) should be -5/2", function () {
|
|
93
|
+
expect(number.toFraction(-2.5)).toStrictEqual([-5, 2]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("toFraction(2/3) should be 2/3", function () {
|
|
97
|
+
expect(number.toFraction(2 / 3)).toStrictEqual([2, 3]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("toFraction(283.33...) should be 850/3", function () {
|
|
101
|
+
expect(number.toFraction(283 + 1 / 3)).toStrictEqual([850, 3]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("toFraction(0) should be 0/1", function () {
|
|
105
|
+
expect(number.toFraction(0)).toStrictEqual([0, 1]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("toFraction(pi) should be pi/1", function () {
|
|
109
|
+
expect(number.toFraction(Math.PI)).toStrictEqual([Math.PI, 1]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("toFraction(0.66) should be 33/50", function () {
|
|
113
|
+
expect(number.toFraction(0.66)).toStrictEqual([33, 50]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("toFraction(0.66, 0.01) should be 2/3", function () {
|
|
117
|
+
expect(number.toFraction(0.66, 0.01)).toStrictEqual([2, 3]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as point from "../point";
|
|
2
|
+
|
|
3
|
+
describe("kpoint", function () {
|
|
4
|
+
it("point.compare should return positive if the first element is larger", function () {
|
|
5
|
+
const result = point.compare([5, 2], [3, 4]);
|
|
6
|
+
expect(result).toBeGreaterThan(0);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("point.compare should return negative if the first element is smaller", function () {
|
|
10
|
+
const result = point.compare([2, 2], [4, 0]);
|
|
11
|
+
expect(result).toBeLessThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("point.compare should return positive if the second element is larger", function () {
|
|
15
|
+
const result = point.compare([5, 2], [5, 1]);
|
|
16
|
+
expect(result).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("point.compare should return negative if the second element is smaller", function () {
|
|
20
|
+
const result = point.compare([2, 2], [2, 4]);
|
|
21
|
+
expect(result).toBeLessThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("point.compare should return positive if the third element is larger", function () {
|
|
25
|
+
const result = point.compare([5, 3, -2], [5, 3, -4]);
|
|
26
|
+
expect(result).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("point.compare should return negative if the third element is smaller", function () {
|
|
30
|
+
const result = point.compare([2, -1, -4], [2, -1, -2]);
|
|
31
|
+
expect(result).toBeLessThan(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("point.compare should return 0 if the vectors are equal", function () {
|
|
35
|
+
const result = point.compare([2, 4, 3], [2, 4, 3]);
|
|
36
|
+
expect(result).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("point.compare should return negative if v1 is shorter than v2", function () {
|
|
40
|
+
const result = point.compare([2, 4], [2, 4, 3]);
|
|
41
|
+
expect(result).toBeLessThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("point.compare should return positive if v1 is longer than v2", function () {
|
|
45
|
+
const result = point.compare([2, 4, -2], [2, 2]);
|
|
46
|
+
expect(result).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as vector from "../vector";
|
|
2
|
+
|
|
3
|
+
describe("kvector", function () {
|
|
4
|
+
it("vector.zip should return empty given empty", function () {
|
|
5
|
+
expect(vector.zip([], [])).toEqual([]);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("vector.zip should create vectors from x and y coords", function () {
|
|
9
|
+
expect(vector.zip([1, 2], [3, 4])).toEqual([
|
|
10
|
+
[1, 3],
|
|
11
|
+
[2, 4],
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("vector.zip should ignore extra xs", function () {
|
|
16
|
+
expect(vector.zip([1, 2, 9], [3, 4])).toEqual([
|
|
17
|
+
[1, 3],
|
|
18
|
+
[2, 4],
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("vector.zip should ignore extra ys", function () {
|
|
23
|
+
expect(vector.zip([1, 2], [3, 4, 9])).toEqual([
|
|
24
|
+
[1, 3],
|
|
25
|
+
[2, 4],
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("vector.map should transform a 2D vector", function () {
|
|
30
|
+
const double = (x) => x * 2;
|
|
31
|
+
expect(vector.map([3, 4], double)).toEqual([6, 8]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("vector.map should pass the index of each element to the callback", function () {
|
|
35
|
+
expect(vector.map([3, 4], (_, i) => i)).toEqual([0, 1]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("vector.add should add two 2D vectors", function () {
|
|
39
|
+
const result = vector.add([1, 2], [3, 4]);
|
|
40
|
+
expect(result).toStrictEqual([4, 6]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("vector.add should add two 3D vectors", function () {
|
|
44
|
+
const result = vector.add([1, 2, 3], [4, 5, 6]);
|
|
45
|
+
expect(result).toStrictEqual([5, 7, 9]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("vector.add should add three 2D vectors", function () {
|
|
49
|
+
const result = vector.add([1, 2], [3, 4], [5, 6]);
|
|
50
|
+
expect(result).toStrictEqual([9, 12]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("vector.subtract should subtract two 2D vectors", function () {
|
|
54
|
+
const result = vector.subtract([1, 2], [3, 4]);
|
|
55
|
+
expect(result).toStrictEqual([-2, -2]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("vector.subtract should subtract two 3D vectors", function () {
|
|
59
|
+
const result = vector.subtract([1, 2, 3], [4, 5, 6]);
|
|
60
|
+
expect(result).toStrictEqual([-3, -3, -3]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("vector.dot should take the dot product of 2 2D vectors", function () {
|
|
64
|
+
const result = vector.dot([1, 2], [3, 4]);
|
|
65
|
+
expect(result).toBe(3 + 8);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("vector.dot should take the dot product of 2 3D vectors", function () {
|
|
69
|
+
const result = vector.dot([1, 2, 3], [4, 5, 6]);
|
|
70
|
+
expect(result).toBe(4 + 10 + 18);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("vector.scale should scale a 2D vector", function () {
|
|
74
|
+
const result = vector.scale([4, 2], 0.5);
|
|
75
|
+
expect(result).toStrictEqual([2, 1]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("vector.scale should scale a 3D vector", function () {
|
|
79
|
+
const result = vector.scale([1, 2, 3], 2);
|
|
80
|
+
expect(result).toStrictEqual([2, 4, 6]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("vector.length should take the length of a 2D vector", function () {
|
|
84
|
+
const result = vector.length([3, 4]);
|
|
85
|
+
expect(result).toBe(5);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("vector.length should take the length of a 3D vector", function () {
|
|
89
|
+
const result = vector.length([4, 0, 3]);
|
|
90
|
+
expect(result).toBe(5);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("vector.equal should return true on two equal 3D vectors", function () {
|
|
94
|
+
const result = vector.equal([6, 3, 4], [6, 3, 4]);
|
|
95
|
+
expect(result).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("vector.equal should return false on two inequal 3D vectors", function () {
|
|
99
|
+
const result = vector.equal([6, 3, 4], [6, 4, 4]);
|
|
100
|
+
expect(result).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("vector.equal should return false on a 2D and 3D vector", function () {
|
|
104
|
+
const result = vector.equal([6, 4], [6, 4, 4]);
|
|
105
|
+
expect(result).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("vector.equal should return false on a 2D and 3D vector", function () {
|
|
109
|
+
const result = vector.equal([6, 3, 4], [6, 3]);
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("vector.equal should return false on a 2D and 3D vector with a trailing 0", function () {
|
|
114
|
+
const result = vector.equal([6, 3, 0], [6, 3]);
|
|
115
|
+
expect(result).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it(
|
|
119
|
+
"vector.collinear should return true on two collinear vectors of " +
|
|
120
|
+
"the same magnitude but different direction",
|
|
121
|
+
function () {
|
|
122
|
+
const result = vector.collinear([3, 3], [-3, -3]);
|
|
123
|
+
expect(result).toBe(true);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it(
|
|
128
|
+
"vector.collinear should return true on two collinear vectors of " +
|
|
129
|
+
"different magnitudes",
|
|
130
|
+
function () {
|
|
131
|
+
const result = vector.collinear([2, 1], [6, 3]);
|
|
132
|
+
expect(result).toBe(true);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it("vector.collinear should return false on non-collinear vectors", function () {
|
|
137
|
+
const result = vector.collinear([1, 2], [-1, 2]);
|
|
138
|
+
expect(result).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("vector.negate of [-2, 2] is [2, -2]", function () {
|
|
142
|
+
const result = vector.negate([-2, 2]);
|
|
143
|
+
expect(result).toStrictEqual([2, -2]);
|
|
144
|
+
});
|
|
145
|
+
});
|
package/src/index.ts
ADDED
package/src/line.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line Utils
|
|
3
|
+
* A line is an array of two points e.g. [[-5, 0], [5, 0]].
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as kpoint from "./point";
|
|
7
|
+
import * as kvector from "./vector";
|
|
8
|
+
|
|
9
|
+
import type {Point} from "./point";
|
|
10
|
+
|
|
11
|
+
export type Line = [Point, Point];
|
|
12
|
+
|
|
13
|
+
export function distanceToPoint(line: Line, point: Point): number {
|
|
14
|
+
return kpoint.distanceToLine(point, line);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function reflectPoint(line: Line, point: Point): Point {
|
|
18
|
+
return kpoint.reflectOverLine(point, line);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function midpoint(line: Line): Point {
|
|
22
|
+
return [(line[0][0] + line[1][0]) / 2, (line[0][1] + line[1][1]) / 2];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function equal(line1: Line, line2: Line, tolerance?: number): boolean {
|
|
26
|
+
// TODO: A nicer implementation might just check collinearity of
|
|
27
|
+
// vectors using underscore magick
|
|
28
|
+
// Compare the directions of the lines
|
|
29
|
+
const v1 = kvector.subtract(line1[1], line1[0]);
|
|
30
|
+
const v2 = kvector.subtract(line2[1], line2[0]);
|
|
31
|
+
if (!kvector.collinear(v1, v2, tolerance)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// If the start point is the same for the two lines, then they are the same
|
|
35
|
+
if (kpoint.equal(line1[0], line2[0])) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Make sure that the direction to get from line1 to
|
|
39
|
+
// line2 is the same as the direction of the lines
|
|
40
|
+
const line1ToLine2Vector = kvector.subtract(line2[0], line1[0]);
|
|
41
|
+
return kvector.collinear(v1, line1ToLine2Vector, tolerance);
|
|
42
|
+
}
|
package/src/logo.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* eslint-disable import/no-default-export */
|
|
2
|
+
// This file describes the graphie source code of the kmath logo
|
|
3
|
+
// currently used on khan.github.io.
|
|
4
|
+
//
|
|
5
|
+
// Also located at http://ka-perseus-graphie.s3.amazonaws.com/42ef3cbadc3e6464124533191728c3c5c55c7355.svg
|
|
6
|
+
|
|
7
|
+
declare let init: any;
|
|
8
|
+
declare let ellipse: any;
|
|
9
|
+
declare let line: any;
|
|
10
|
+
|
|
11
|
+
const GREEN = "#28AE7B";
|
|
12
|
+
|
|
13
|
+
export default () => {
|
|
14
|
+
init({
|
|
15
|
+
range: [
|
|
16
|
+
[0, 10],
|
|
17
|
+
[0, 10],
|
|
18
|
+
],
|
|
19
|
+
scale: 40,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ellipse(5, 5, 5, {
|
|
23
|
+
stroke: null,
|
|
24
|
+
fill: GREEN,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
line([2, 5], [8.5, 5], {
|
|
28
|
+
stroke: "WHITE",
|
|
29
|
+
fill: "WHITE",
|
|
30
|
+
strokeWidth: 25,
|
|
31
|
+
arrows: "->",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
line([5, 2], [5, 8], {
|
|
35
|
+
stroke: "WHITE",
|
|
36
|
+
fill: "WHITE",
|
|
37
|
+
strokeWidth: 25,
|
|
38
|
+
});
|
|
39
|
+
};
|
package/src/number.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Utils
|
|
3
|
+
* A number is a js-number, e.g. 5.12
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_TOLERANCE = 1e-9;
|
|
9
|
+
|
|
10
|
+
// TODO: Should this just be Number.Epsilon
|
|
11
|
+
export const EPSILON: number = Math.pow(2, -42);
|
|
12
|
+
|
|
13
|
+
export function is(x: any): boolean {
|
|
14
|
+
return _.isNumber(x) && !_.isNaN(x);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function equal(x: number, y: number, tolerance?: number): boolean {
|
|
18
|
+
// Checking for undefined makes this function behave nicely
|
|
19
|
+
// with vectors of different lengths that are _.zip'd together
|
|
20
|
+
if (x == null || y == null) {
|
|
21
|
+
return x === y;
|
|
22
|
+
}
|
|
23
|
+
// We check === here so that +/-Infinity comparisons work correctly
|
|
24
|
+
if (x === y) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (tolerance == null) {
|
|
28
|
+
tolerance = DEFAULT_TOLERANCE;
|
|
29
|
+
}
|
|
30
|
+
return Math.abs(x - y) < tolerance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sign(
|
|
34
|
+
x: number,
|
|
35
|
+
tolerance?: number,
|
|
36
|
+
): number /* Should be: 0 | 1 | -1 */ {
|
|
37
|
+
return equal(x, 0, tolerance) ? 0 : Math.abs(x) / x;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isInteger(num: number, tolerance?: number): boolean {
|
|
41
|
+
return equal(Math.round(num), num, tolerance);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Round a number to a certain number of decimal places
|
|
45
|
+
export function round(num: number, precision: number): number {
|
|
46
|
+
const factor = Math.pow(10, precision);
|
|
47
|
+
return Math.round(num * factor) / factor;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Round num to the nearest multiple of increment
|
|
51
|
+
// i.e. roundTo(83, 5) -> 85
|
|
52
|
+
export function roundTo(num: number, increment: number): number {
|
|
53
|
+
return Math.round(num / increment) * increment;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function floorTo(num: number, increment: number): number {
|
|
57
|
+
return Math.floor(num / increment) * increment;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ceilTo(num: number, increment: number): number {
|
|
61
|
+
return Math.ceil(num / increment) * increment;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* toFraction
|
|
66
|
+
*
|
|
67
|
+
* Returns a [numerator, denominator] array rational representation
|
|
68
|
+
* of `decimal`
|
|
69
|
+
*
|
|
70
|
+
* See http://en.wikipedia.org/wiki/Continued_fraction for implementation
|
|
71
|
+
* details
|
|
72
|
+
*
|
|
73
|
+
* toFraction(4/8) => [1, 2]
|
|
74
|
+
* toFraction(0.66) => [33, 50]
|
|
75
|
+
* toFraction(0.66, 0.01) => [2/3]
|
|
76
|
+
* toFraction(283 + 1/3) => [850, 3]
|
|
77
|
+
*/
|
|
78
|
+
export function toFraction(
|
|
79
|
+
decimal: number,
|
|
80
|
+
// can't be 0
|
|
81
|
+
tolerance: number = EPSILON,
|
|
82
|
+
maxDenominator = 1000,
|
|
83
|
+
): [number, number] {
|
|
84
|
+
// Initialize everything to compute successive terms of
|
|
85
|
+
// continued-fraction approximations via recurrence relation
|
|
86
|
+
let n = [1, 0];
|
|
87
|
+
let d = [0, 1];
|
|
88
|
+
let a = Math.floor(decimal);
|
|
89
|
+
let rem = decimal - a;
|
|
90
|
+
|
|
91
|
+
while (d[0] <= maxDenominator) {
|
|
92
|
+
if (equal(n[0] / d[0], decimal, tolerance)) {
|
|
93
|
+
return [n[0], d[0]];
|
|
94
|
+
}
|
|
95
|
+
n = [a * n[0] + n[1], n[0]];
|
|
96
|
+
d = [a * d[0] + d[1], d[0]];
|
|
97
|
+
a = Math.floor(1 / rem);
|
|
98
|
+
rem = 1 / rem - a;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// We failed to find a nice rational representation,
|
|
102
|
+
// so return an irrational "fraction"
|
|
103
|
+
return [decimal, 1];
|
|
104
|
+
}
|
package/src/point.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point Utils
|
|
3
|
+
* A point is an array of two numbers e.g. [0, 0].
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as knumber from "./number";
|
|
7
|
+
import * as kvector from "./vector";
|
|
8
|
+
|
|
9
|
+
// A point, in 2D, 3D, or nD space.
|
|
10
|
+
export type Point = ReadonlyArray<number>;
|
|
11
|
+
|
|
12
|
+
// Rotate point (around origin unless a center is specified)
|
|
13
|
+
export function rotateRad(point: Point, theta: number, center?: Point): Point {
|
|
14
|
+
if (center === undefined) {
|
|
15
|
+
return kvector.rotateRad(point, theta);
|
|
16
|
+
} else {
|
|
17
|
+
return kvector.add(
|
|
18
|
+
center,
|
|
19
|
+
kvector.rotateRad(kvector.subtract(point, center), theta),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function rotateDeg(point: Point, theta: number, center?: Point): Point {
|
|
25
|
+
if (center === undefined) {
|
|
26
|
+
return kvector.rotateDeg(point, theta);
|
|
27
|
+
} else {
|
|
28
|
+
return kvector.add(
|
|
29
|
+
center,
|
|
30
|
+
kvector.rotateDeg(kvector.subtract(point, center), theta),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Distance between two points
|
|
36
|
+
export function distanceToPoint(point1: Point, point2: Point): number {
|
|
37
|
+
return kvector.length(kvector.subtract(point1, point2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Distance between point and line
|
|
41
|
+
export function distanceToLine(point: Point, line: [Point, Point]): number {
|
|
42
|
+
const lv = kvector.subtract(line[1], line[0]);
|
|
43
|
+
const pv = kvector.subtract(point, line[0]);
|
|
44
|
+
const projectedPv = kvector.projection(pv, lv);
|
|
45
|
+
const distancePv = kvector.subtract(projectedPv, pv);
|
|
46
|
+
return kvector.length(distancePv);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Reflect point over line
|
|
50
|
+
export function reflectOverLine<P extends Point>(point: P, line: [P, P]): P {
|
|
51
|
+
const lv = kvector.subtract(line[1], line[0]);
|
|
52
|
+
const pv = kvector.subtract(point, line[0]);
|
|
53
|
+
const projectedPv = kvector.projection(pv, lv);
|
|
54
|
+
const reflectedPv = kvector.subtract(kvector.scale(projectedPv, 2), pv);
|
|
55
|
+
return kvector.add(line[0], reflectedPv);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compares two points, returning -1, 0, or 1, for use with
|
|
60
|
+
* Array.prototype.sort
|
|
61
|
+
*
|
|
62
|
+
* Note: This technically doesn't satisfy the total-ordering
|
|
63
|
+
* requirements of Array.prototype.sort unless equalityTolerance
|
|
64
|
+
* is 0. In some cases very close points that compare within a
|
|
65
|
+
* few equalityTolerances could appear in the wrong order.
|
|
66
|
+
*/
|
|
67
|
+
export function compare(
|
|
68
|
+
point1: Point,
|
|
69
|
+
point2: Point,
|
|
70
|
+
equalityTolerance?: number,
|
|
71
|
+
): number /* TODO: convert to -1 | 0 | 1 type */ {
|
|
72
|
+
if (point1.length !== point2.length) {
|
|
73
|
+
return point1.length - point2.length;
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < point1.length; i++) {
|
|
76
|
+
if (!knumber.equal(point1[i], point2[i], equalityTolerance)) {
|
|
77
|
+
return point1[i] - point2[i];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if a value is a point
|
|
84
|
+
export const is = kvector.is;
|
|
85
|
+
|
|
86
|
+
// Add and subtract vector(s)
|
|
87
|
+
export const addVector = kvector.add;
|
|
88
|
+
export const addVectors = kvector.add;
|
|
89
|
+
export const subtractVector = kvector.subtract;
|
|
90
|
+
export const equal = kvector.equal;
|
|
91
|
+
|
|
92
|
+
// Convert from cartesian to polar and back
|
|
93
|
+
export const polarRadFromCart = kvector.polarRadFromCart;
|
|
94
|
+
export const polarDegFromCart = kvector.polarDegFromCart;
|
|
95
|
+
export const cartFromPolarRad = kvector.cartFromPolarRad;
|
|
96
|
+
export const cartFromPolarDeg = kvector.cartFromPolarDeg;
|
|
97
|
+
|
|
98
|
+
// Rounding
|
|
99
|
+
export const round = kvector.round;
|
|
100
|
+
export const roundTo = kvector.roundTo;
|
|
101
|
+
export const floorTo = kvector.floorTo;
|
|
102
|
+
export const ceilTo = kvector.ceilTo;
|
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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Utils
|
|
3
|
+
* A vector is an array of numbers e.g. [0, 3, 4].
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as knumber from "./number";
|
|
7
|
+
|
|
8
|
+
type Vector = ReadonlyArray<number>;
|
|
9
|
+
|
|
10
|
+
function arraySum(array: ReadonlyArray<number>): number {
|
|
11
|
+
return array.reduce((memo, arg) => memo + arg, 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function arrayProduct(array: ReadonlyArray<number>): number {
|
|
15
|
+
return array.reduce((memo, arg) => memo * arg, 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function zip<T>(xs: ReadonlyArray<T>, ys: ReadonlyArray<T>): [T, T][];
|
|
19
|
+
export function zip<T>(...arrays: ReadonlyArray<T>[]): T[][];
|
|
20
|
+
export function zip<T>(...arrays: ReadonlyArray<T>[]): T[][] {
|
|
21
|
+
const n = Math.min(...arrays.map((a) => a.length));
|
|
22
|
+
const results: T[][] = [];
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
results.push(arrays.map((a) => a[i]));
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function map<T, U>(pair: [T, T], f: (a: T, i: number) => U): [U, U] {
|
|
30
|
+
return [f(pair[0], 0), f(pair[1], 1)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks if the given vector contains only numbers and, optionally, is of the
|
|
35
|
+
* right dimension (length).
|
|
36
|
+
*
|
|
37
|
+
* is([1, 2, 3]) -> true
|
|
38
|
+
* is([1, "Hello", 3]) -> false
|
|
39
|
+
* is([1, 2, 3], 1) -> false
|
|
40
|
+
*/
|
|
41
|
+
export function is(vec: unknown, dimension: 2): vec is [number, number];
|
|
42
|
+
export function is(vec: unknown, dimension?: number): vec is Vector;
|
|
43
|
+
export function is(vec: unknown, dimension?: number) {
|
|
44
|
+
if (!Array.isArray(vec)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (dimension !== undefined && vec.length !== dimension) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return vec.every(knumber.is);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Normalize to a unit vector
|
|
54
|
+
export function normalize<V extends Vector>(v: V): V {
|
|
55
|
+
return scale(v, 1 / length(v));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Length/magnitude of a vector
|
|
59
|
+
export function length(v: Vector): number {
|
|
60
|
+
return Math.sqrt(dot(v, v));
|
|
61
|
+
}
|
|
62
|
+
// Dot product of two vectors
|
|
63
|
+
export function dot(a: Vector, b: Vector): number {
|
|
64
|
+
const zipped = zip(a, b);
|
|
65
|
+
const multiplied = zipped.map(arrayProduct);
|
|
66
|
+
return arraySum(multiplied);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* vector-add multiple [x, y] coords/vectors
|
|
70
|
+
*
|
|
71
|
+
* add([1, 2], [3, 4]) -> [4, 6]
|
|
72
|
+
*/
|
|
73
|
+
export function add<V extends Vector>(...vecs: ReadonlyArray<V>): V {
|
|
74
|
+
const zipped = zip(...vecs);
|
|
75
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
76
|
+
return zipped.map(arraySum);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function subtract<V extends Vector>(v1: V, v2: V): V {
|
|
80
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
81
|
+
return zip(v1, v2).map((dim) => dim[0] - dim[1]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function negate<V extends Vector>(v: V): V {
|
|
85
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
86
|
+
return v.map((x) => {
|
|
87
|
+
return -x;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Scale a vector
|
|
92
|
+
export function scale<V extends Vector>(v1: V, scalar: number): V {
|
|
93
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
94
|
+
return v1.map((x) => {
|
|
95
|
+
return x * scalar;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function equal(v1: Vector, v2: Vector, tolerance?: number): boolean {
|
|
100
|
+
return (
|
|
101
|
+
v1.length === v2.length &&
|
|
102
|
+
zip(v1, v2).every((pair) => knumber.equal(pair[0], pair[1], tolerance))
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function codirectional(
|
|
107
|
+
v1: Vector,
|
|
108
|
+
v2: Vector,
|
|
109
|
+
tolerance?: number,
|
|
110
|
+
): boolean {
|
|
111
|
+
// The origin is trivially codirectional with all other vectors.
|
|
112
|
+
// This gives nice semantics for codirectionality between points when
|
|
113
|
+
// comparing their difference vectors.
|
|
114
|
+
if (
|
|
115
|
+
knumber.equal(length(v1), 0, tolerance) ||
|
|
116
|
+
knumber.equal(length(v2), 0, tolerance)
|
|
117
|
+
) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
v1 = normalize(v1);
|
|
122
|
+
v2 = normalize(v2);
|
|
123
|
+
|
|
124
|
+
return equal(v1, v2, tolerance);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function collinear(v1: Vector, v2: Vector, tolerance?: number): boolean {
|
|
128
|
+
return (
|
|
129
|
+
codirectional(v1, v2, tolerance) ||
|
|
130
|
+
codirectional(v1, negate(v2), tolerance)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// TODO(jeremy) These coordinate conversion functions really only handle 2D points (ie. [number, number])
|
|
135
|
+
|
|
136
|
+
// Convert a cartesian coordinate into a radian polar coordinate
|
|
137
|
+
export function polarRadFromCart(
|
|
138
|
+
v: ReadonlyArray<number>,
|
|
139
|
+
): ReadonlyArray<number> {
|
|
140
|
+
const radius = length(v);
|
|
141
|
+
let theta = Math.atan2(v[1], v[0]);
|
|
142
|
+
|
|
143
|
+
// Convert angle range from [-pi, pi] to [0, 2pi]
|
|
144
|
+
if (theta < 0) {
|
|
145
|
+
theta += 2 * Math.PI;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [radius, theta];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Converts a cartesian coordinate into a degree polar coordinate
|
|
152
|
+
export function polarDegFromCart(
|
|
153
|
+
v: ReadonlyArray<number>,
|
|
154
|
+
): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
155
|
+
const polar = polarRadFromCart(v);
|
|
156
|
+
return [polar[0], (polar[1] * 180) / Math.PI];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
160
|
+
*
|
|
161
|
+
* Examples:
|
|
162
|
+
* cartFromPolarRad(5, Math.PI)
|
|
163
|
+
*/
|
|
164
|
+
export function cartFromPolarRad(
|
|
165
|
+
radius: number,
|
|
166
|
+
theta = 0,
|
|
167
|
+
): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
168
|
+
return [radius * Math.cos(theta), radius * Math.sin(theta)];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
172
|
+
*
|
|
173
|
+
* Examples:
|
|
174
|
+
* cartFromPolarDeg(5, 30)
|
|
175
|
+
*/
|
|
176
|
+
export function cartFromPolarDeg(
|
|
177
|
+
radius: number,
|
|
178
|
+
theta = 0,
|
|
179
|
+
): ReadonlyArray<number> {
|
|
180
|
+
return cartFromPolarRad(radius, (theta * Math.PI) / 180);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Rotate vector
|
|
184
|
+
export function rotateRad(
|
|
185
|
+
v: ReadonlyArray<number>,
|
|
186
|
+
theta: number,
|
|
187
|
+
): ReadonlyArray<number> {
|
|
188
|
+
const polar = polarRadFromCart(v);
|
|
189
|
+
const angle = polar[1] + theta;
|
|
190
|
+
return cartFromPolarRad(polar[0], angle);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function rotateDeg(
|
|
194
|
+
v: ReadonlyArray<number>,
|
|
195
|
+
theta: number,
|
|
196
|
+
): ReadonlyArray<number> {
|
|
197
|
+
const polar = polarDegFromCart(v);
|
|
198
|
+
const angle = polar[1] + theta;
|
|
199
|
+
return cartFromPolarDeg(polar[0], angle);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Angle between two vectors
|
|
203
|
+
export function angleRad(v1: Vector, v2: Vector): number {
|
|
204
|
+
return Math.acos(dot(v1, v2) / (length(v1) * length(v2)));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function angleDeg(v1: Vector, v2: Vector): number {
|
|
208
|
+
return (angleRad(v1, v2) * 180) / Math.PI;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Vector projection of v1 onto v2
|
|
212
|
+
export function projection<V extends Vector>(v1: V, v2: V): V {
|
|
213
|
+
const scalar = dot(v1, v2) / dot(v2, v2);
|
|
214
|
+
return scale(v2, scalar);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Round each number to a certain number of decimal places
|
|
218
|
+
export function round<V extends Vector>(vec: V, precision: V | number): V {
|
|
219
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
220
|
+
return vec.map((elem, i) => knumber.round(elem, precision[i] || precision));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Round each number to the nearest increment
|
|
224
|
+
export function roundTo(
|
|
225
|
+
coord: [number, number],
|
|
226
|
+
increment: [number, number] | number,
|
|
227
|
+
): [number, number];
|
|
228
|
+
export function roundTo<V extends Vector>(vec: V, increment: V | number): V;
|
|
229
|
+
export function roundTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
230
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
231
|
+
return vec.map((elem, i) =>
|
|
232
|
+
knumber.roundTo(elem, increment[i] || increment),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function floorTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
237
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
238
|
+
return vec.map((elem, i) =>
|
|
239
|
+
knumber.floorTo(elem, increment[i] || increment),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function ceilTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
244
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
245
|
+
return vec.map((elem, i) =>
|
|
246
|
+
knumber.ceilTo(elem, increment[i] || increment),
|
|
247
|
+
);
|
|
248
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
2
|
+
// version number during the release build.
|
|
3
|
+
// In dev, you'll never see the version number.
|
|
4
|
+
|
|
5
|
+
import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core";
|
|
6
|
+
|
|
7
|
+
const libName = "@khanacademy/kmath";
|
|
8
|
+
export const libVersion = "__lib_version__";
|
|
9
|
+
|
|
10
|
+
addLibraryVersionToPerseusDebug(libName, libVersion);
|