├── site
├── .babelrc
├── webpack.config.js
├── package.json
├── index.html
├── .gitignore
└── index.js
├── src
├── lib.rs
├── utils.rs
├── axioms.rs
├── geometry.rs
├── interop.rs
└── multivector.rs
├── Cargo.toml
├── .gitignore
├── Cargo.lock
└── README.md
/site/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | presets: ["@babel/preset-env"]
3 | }
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(warnings)]
2 | mod axioms;
3 | mod geometry;
4 | mod interop;
5 | mod multivector;
6 | mod utils;
7 |
8 | use wasm_bindgen::prelude::*;
9 |
10 | #[cfg(feature = "wee_alloc")]
11 | #[global_allocator]
12 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
13 |
--------------------------------------------------------------------------------
/site/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | module.exports = {
3 | entry: "./index.js",
4 | output: {
5 | path: path.resolve(__dirname, "dist"),
6 | filename: "index.js",
7 | },
8 | module: {
9 | rules: [
10 | {
11 | test: /\.(js)$/,
12 | exclude: /node_modules/,
13 | use: "babel-loader",
14 | },
15 | ],
16 | },
17 | mode: "development"
18 | };
--------------------------------------------------------------------------------
/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "serve": "webpack-dev-server"
4 | },
5 | "dependencies": {
6 | "@svgdotjs/svg.draggable.js": "^3.0.2",
7 | "@svgdotjs/svg.js": "^3.0.16"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.10.4",
11 | "@babel/preset-env": "^7.11.0",
12 | "babel-loader": "^8.1.0",
13 | "webpack": "^4.44.1",
14 | "webpack-cli": "^3.3.12",
15 | "webpack-dev-server": "^3.1.10"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/site/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PGA Axioms WASM
7 |
8 |
9 |
10 |
11 |
12 |
13 | Huzita-Hatori Origami Axioms
14 | Press keys 1-7 to explore each axiom
15 | Axiom Description
16 |
17 |
18 |
19 |
20 |
25 |
26 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "pga_axioms"
3 | version = "0.1.0"
4 | authors = ["Michael Walczyk "]
5 | edition = "2018"
6 |
7 | [lib]
8 | crate-type = ["cdylib", "rlib"]
9 |
10 | [features]
11 | default = ["console_error_panic_hook"]
12 |
13 | [dependencies]
14 | console_error_panic_hook = { version = "0.1.6", optional = true }
15 | js-sys = "0.3.47"
16 | serde = { version = "1.0.123", features = ["derive"] }
17 | wasm-bindgen = { version = "0.2.63", features = ["serde-serialize"] }
18 | web-sys = { version = "0.3.47", features = ["console"] }
19 | wee_alloc = { version = "0.4.5", optional = true }
20 |
21 | [dev-dependencies]
22 | wasm-bindgen-test = "0.3.13"
23 |
24 | [profile.release]
25 | # Tell `rustc` to optimize for small code size.
26 | opt-level = "s"
27 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | pub fn set_panic_hook() {
2 | // When the `console_error_panic_hook` feature is enabled, we can call the
3 | // `set_panic_hook` function at least once during initialization, and then
4 | // we will get better error messages if our code ever panics.
5 | //
6 | // For more details see
7 | // https://github.com/rustwasm/console_error_panic_hook#readme
8 | #[cfg(feature = "console_error_panic_hook")]
9 | console_error_panic_hook::set_once();
10 | }
11 |
12 | /// A function that returns the sign of a floating-point number or zero if it is
13 | /// close to zero (within epsilon). Note that the method `std::f32::signum()` exists,
14 | /// but it doesn't work exactly the same way.
15 | pub fn sign_with_tolerance(value: f32) -> f32 {
16 | if value > 0.001 {
17 | 1.0
18 | } else if value < -0.001 {
19 | -1.0
20 | } else {
21 | 0.0
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | Cargo.lock
4 | bin/
5 | pkg/
6 | wasm-pack.log
7 |
8 | *.iml
9 |
10 | .idea
11 |
12 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
13 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
14 |
15 | # User-specific stuff
16 | .idea/**/workspace.xml
17 | .idea/**/tasks.xml
18 | .idea/**/usage.statistics.xml
19 | .idea/**/dictionaries
20 | .idea/**/shelf
21 |
22 | # Generated files
23 | .idea/**/contentModel.xml
24 |
25 | # Sensitive or high-churn files
26 | .idea/**/dataSources/
27 | .idea/**/dataSources.ids
28 | .idea/**/dataSources.local.xml
29 | .idea/**/sqlDataSources.xml
30 | .idea/**/dynamic.xml
31 | .idea/**/uiDesigner.xml
32 | .idea/**/dbnavigator.xml
33 |
34 | # Gradle
35 | .idea/**/gradle.xml
36 | .idea/**/libraries
37 |
38 | # Gradle and Maven with auto-import
39 | # When using Gradle or Maven with auto-import, you should exclude module files,
40 | # since they will be recreated, and may cause churn. Uncomment if using
41 | # auto-import.
42 | # .idea/artifacts
43 | # .idea/compiler.xml
44 | # .idea/jarRepositories.xml
45 | # .idea/modules.xml
46 | # .idea/*.iml
47 | # .idea/modules
48 | # *.iml
49 | # *.ipr
50 |
51 | # CMake
52 | cmake-build-*/
53 |
54 | # Mongo Explorer plugin
55 | .idea/**/mongoSettings.xml
56 |
57 | # File-based project format
58 | *.iws
59 |
60 | # IntelliJ
61 | out/
62 |
63 | # mpeltonen/sbt-idea plugin
64 | .idea_modules/
65 |
66 | # JIRA plugin
67 | atlassian-ide-plugin.xml
68 |
69 | # Cursive Clojure plugin
70 | .idea/replstate.xml
71 |
72 | # Crashlytics plugin (for Android Studio and IntelliJ)
73 | com_crashlytics_export_strings.xml
74 | crashlytics.properties
75 | crashlytics-build.properties
76 | fabric.properties
77 |
78 | # Editor-based Rest Client
79 | .idea/httpRequests
80 |
81 | # Android studio 3.1+ serialized cache file
82 | .idea/caches/build_file_checksums.ser
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Snowpack dependency directory (https://snowpack.dev/)
45 | web_modules/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 | .parcel-cache
78 |
79 | # Next.js build output
80 | .next
81 | out
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and not Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | .vuepress/dist
95 |
96 | # Serverless directories
97 | .serverless/
98 |
99 | # FuseBox cache
100 | .fusebox/
101 |
102 | # DynamoDB Local files
103 | .dynamodb/
104 |
105 | # TernJS port file
106 | .tern-port
107 |
108 | # Stores VSCode versions used for testing VSCode extensions
109 | .vscode-test
110 |
111 | # yarn v2
112 | .yarn/cache
113 | .yarn/unplugged
114 | .yarn/build-state.yml
115 | .yarn/install-state.gz
116 | .pnp.*
--------------------------------------------------------------------------------
/src/axioms.rs:
--------------------------------------------------------------------------------
1 | use crate::geometry;
2 | use crate::multivector::*;
3 | use web_sys::console::dir;
4 |
5 | /// Given two points `p0` and `p1`, there is a unique fold that passes through both of them.
6 | pub fn axiom_1(p0: &Multivector, p1: &Multivector) -> Multivector {
7 | let mut crease = p0.join(p1);
8 | crease.normalized()
9 | }
10 |
11 | /// Given two points `p0` and `p1`, there is a unique fold that places `p0` onto `p1`.
12 | pub fn axiom_2(p0: &Multivector, p1: &Multivector) -> Multivector {
13 | let l = p0.join(p1);
14 | let midpoint = geometry::midpoint(p0, p1);
15 | let crease = geometry::orthogonal(&midpoint, &l);
16 | crease.normalized()
17 | }
18 |
19 | /// Given two lines `l0` and `l1`, there is a fold that places `l0` onto `l1`.
20 | pub fn axiom_3(l0: &Multivector, l1: &Multivector) -> Multivector {
21 | let crease = geometry::bisector(l0, l1);
22 | crease.normalized()
23 |
24 | // There are two possible solutions (two angle bisectors) - the one above or:
25 | // let crease = geometry::orthogonal(l0.meet(l1), crease);
26 | }
27 |
28 | /// Given a point `p` and a line `l`, there is a unique fold perpendicular to `l` that passes
29 | /// through point `p`.
30 | pub fn axiom_4(p: &Multivector, l: &Multivector) -> Multivector {
31 | // Simply take the inner product between l and p to construct the perpendicular that passes
32 | // through p
33 | let crease = geometry::orthogonal(p, l);
34 | crease.normalized()
35 | }
36 |
37 | /// Given two points `p0` and `p1` and a line `l`, there is a fold that places `p0` onto `l` and
38 | /// passes through `p1`.
39 | pub fn axiom_5(p0: &Multivector, p1: &Multivector, l: &Multivector) -> Option {
40 | // Calculate. the radius of the circle centered on `p1` that is tangent to `p0`
41 | let r = geometry::dist_point_to_point(p0, p1);
42 |
43 | // Then, calculate the (shortest) distance from the line to the center of the circle
44 | let dist_from_line_to_center = geometry::dist_point_to_line(p1, &l);
45 |
46 | // Exit early if no intersection is possible
47 | if dist_from_line_to_center > r {
48 | return None;
49 | }
50 |
51 | // Constructs a line perpendicular to `l` that passes through `p1`
52 | let orthogonal = geometry::orthogonal(p1, &l);
53 |
54 | // Then, "meet" this line with the original line to calculate the point of intersection
55 | let mut perpendicular = geometry::intersect_lines(&orthogonal, &l).normalized();
56 |
57 | // "Flip" x/y if e12 is negative
58 | perpendicular = perpendicular * perpendicular.e12();
59 |
60 | // Pythagoras' theorem: find the length of the third side of the triangle
61 | // whose hypotenuse is `r` and other side is `dist_from_line_to_center`
62 | //
63 | // We don't need to take the absolute value of the value inside of the sqrt operation
64 | // (as in enki's ray tracing code) since we check above that `dist_from_line_to_center`
65 | // is less than (or equal to) the radius `r`
66 | let d = (r * r - dist_from_line_to_center * dist_from_line_to_center).sqrt();
67 |
68 | // Multiplying a line by e012 has the effect of "pulling out" its direction vector,
69 | // represented by an ideal point (i.e. a point at infinity) - this is also known as
70 | // metric polarity
71 | let mut direction = (*l) * e012;
72 | direction /= direction.ideal_norm();
73 |
74 | // If l isn't normalized, we have to do the above or just:
75 | // direction = l.normalize() * e012;
76 |
77 | // If there are 2 intersections (i.e., the line "pierces through" the circle), then you
78 | // can choose either point of intersection (both are valid) - simply change the `+` to a
79 | // `-` (or vice-versa)
80 | //
81 | // The point of intersection can be found by translating the point `perp` along the line
82 | // `l` by an amount `d` (in either direction, in the case of 2 intersections)
83 | direction *= d;
84 | let intersection = geometry::translate(&perpendicular, direction.e20(), direction.e01());
85 |
86 | // A new line joining the point of intersection and p0
87 | let m = intersection.join(p0);
88 |
89 | // A line perpendicular to m that passes through p1: note that this line should always
90 | // pass through the midpoint of the line segment `intersection - p0`
91 | let crease = geometry::orthogonal(p1, &m);
92 |
93 | Some(crease.normalized())
94 | }
95 |
96 | /// Given two points `p0` and `p1` and two lines `l0` and `l1`, there is a fold that places `p0` onto
97 | /// `l0` and `p1` onto `l1`.
98 | pub fn axiom_6(
99 | p0: &Multivector,
100 | p1: &Multivector,
101 | l0: &Multivector,
102 | l1: &Multivector,
103 | ) -> Multivector {
104 | unimplemented!();
105 | }
106 |
107 | /// Given one point `p` and two lines `l0` and `l1`, there is a fold that places `p` onto `l0`
108 | /// and is perpendicular to `l1`.
109 | pub fn axiom_7(p: &Multivector, l0: &Multivector, l1: &Multivector) -> Option {
110 | let angle_between = geometry::angle(l0, l1);
111 |
112 | // Lines are parallel - no solution (at least, a solution that does not involve
113 | // infinite elements)
114 | if (angle_between.abs() - std::f32::consts::PI).abs() <= 0.001 {
115 | return None;
116 | }
117 |
118 | // Project line `l1` onto the point `p`
119 | let shifted = geometry::project(&l1, p);
120 |
121 | // Intersect this line with `l0` - normalize and invert e12 if necessary, since
122 | // the input lines will, in general, not be normalized or oriented in the same
123 | // direction
124 | let mut intersect = shifted.meet(&l0);
125 | intersect = intersect.normalized();
126 | intersect *= intersect.e12();
127 |
128 | // Find the midpoint between this new point of intersection and `p` -
129 | // drop a perpendicular from `l1` to this point
130 | let midpoint = geometry::midpoint(p, &intersect);
131 | let crease = geometry::orthogonal(&midpoint, l1);
132 |
133 | Some(crease.normalized())
134 | }
135 |
--------------------------------------------------------------------------------
/src/geometry.rs:
--------------------------------------------------------------------------------
1 | use crate::multivector::Multivector;
2 |
3 | /// Intersect two lines by taking their wedge (outer) product. This is sometimes
4 | /// called the "meet" operator, as it (unconditionally) calculates the point where
5 | /// the two lines meet. Note that this works even if the lines are parallel: the
6 | /// result will be an ideal point (i.e. a point at infinity).
7 | ///
8 | /// Functionally speaking, this is equivalent to both `^` and `meet`.
9 | pub fn intersect_lines(l1: &Multivector, l2: &Multivector) -> Multivector {
10 | (*l1) ^ (*l2)
11 | }
12 |
13 | /// Returns the distance between two points `p1` and `p2`. Algebraically, this is the
14 | /// length (norm) of the line between the two points, which can be found via the
15 | /// regressive ("vee") product `p1 & p2`.
16 | pub fn dist_point_to_point(p1: &Multivector, p2: &Multivector) -> f32 {
17 | let p1 = p1.normalized();
18 | let p2 = p2.normalized();
19 |
20 | (p1 & p2).norm()
21 | }
22 |
23 | /// Returns the distance between point `p` and line `l`. Algebraically, this is
24 | /// the highest grade part of the geometric product `p * l`, or equivalently, `p ^ l`.
25 | ///
26 | /// Note that the order of the arguments does not matter. All that matters is that
27 | /// one argument is a grade-1 element and the other is a grade-2 element.
28 | pub fn dist_point_to_line(p: &Multivector, l: &Multivector) -> f32 {
29 | let p = p.normalized();
30 | let l = l.normalized();
31 |
32 | // Technically, this is a trivector, but we simply return a scalar
33 | (p ^ l).e012()
34 | }
35 |
36 | /// Returns the angle (in radians) between two lines `l1` and `l2`. Algebraically,
37 | /// the cosine of the angle between the two lines is given by their inner product
38 | /// `l1 | l2`.
39 | pub fn angle(l1: &Multivector, l2: &Multivector) -> f32 {
40 | let l1 = l1.normalized();
41 | let l2 = l2.normalized();
42 |
43 | let cos_theta = (l1 | l2).scalar();
44 | cos_theta.acos()
45 | }
46 |
47 | /// Returns the angle bisector of two lines `l1` and `l2`. In general, there are
48 | /// two possible such bisectors. The chosen angle depends on the orientation of the
49 | /// two lines (the angle-pair that "matches" the orientation of both lines).
50 | pub fn bisector(l1: &Multivector, l2: &Multivector) -> Multivector {
51 | let l1 = l1.normalized();
52 | let l2 = l2.normalized();
53 |
54 | l1 + l2
55 | }
56 |
57 | /// Returns the midpoint between two points `p1` and `p2`.
58 | pub fn midpoint(p1: &Multivector, p2: &Multivector) -> Multivector {
59 | let p1 = p1.normalized();
60 | let p2 = p2.normalized();
61 |
62 | p1 + p2
63 | }
64 |
65 | /// Returns the perpendicular bisector between two points `p1` and `p2`.
66 | pub fn perpendicular_bisector(p1: &Multivector, p2: &Multivector) -> Multivector {
67 | let p1 = p1.normalized();
68 | let p2 = p2.normalized();
69 |
70 | let midpoint = midpoint(&p1, &p2);
71 | let line_between = p1.join(&p2);
72 |
73 | orthogonal(&midpoint, &line_between)
74 | }
75 |
76 | /// Projects multivector `target` onto multivector `onto`. The geometric meaning
77 | /// depends on the grade and order of the arguments. For example:
78 | ///
79 | /// `project(p, l)` with a point `p` and line `l`:
80 | ///
81 | /// Computes the product `(p | l) * l`, i.e. the projection of point `p` onto
82 | /// line `l`. The result is a new point that lies on `l`. The perpendicular to
83 | /// `l` that runs through this new point will also pass through `p`.
84 | ///
85 | /// `project(l, p)` with a line `l` and point `p`:
86 | ///
87 | /// Computes the product `(l | p) * p`, i.e. the projection of line `l` onto
88 | /// point `p`. The result is a new line that runs parallel to `l` and passes
89 | /// through `p`.
90 | pub fn project(target: &Multivector, onto: &Multivector) -> Multivector {
91 | ((*target) | (*onto)) * (*onto)
92 | }
93 |
94 | /// Computes the line orthogonal to line `l` that passes through point `p`. Algebraically,
95 | /// this is simply the inner product `p | l`, or alternatively, the lowest grade part of
96 | /// the product `p * l`.
97 | ///
98 | /// The line will be oriented (pointing) in the direction from `p` to `l`. Swapping `p`
99 | /// and `l` will result in the same line but with opposite orientation.
100 | pub fn orthogonal(p: &Multivector, l: &Multivector) -> Multivector {
101 | (*p) | (*l)
102 | }
103 |
104 | /// Reflects the multivector `target` across the multivector `across`. For example, if `target`
105 | /// is a point and `across` is a line, the result will be a new point reflected across the line.
106 | ///
107 | /// The reflected multivector will have the same orientation as the original (for example,
108 | /// if a line is reflected across a point).
109 | pub fn reflect(target: &Multivector, across: &Multivector) -> Multivector {
110 | (*across) * (*target) * (*across)
111 | }
112 |
113 | /// Rotates the multivector by `angle` radians about the point ``. Algebraically,
114 | /// this is equivalent to computing the "sandwich product" `R * m * ~R`.
115 | #[allow(non_snake_case)]
116 | pub fn rotate(m: &Multivector, angle: f32, x: f32, y: f32) -> Multivector {
117 | let R = Multivector::rotor(angle, x, y);
118 | R * (*m) * R.conjugation()
119 | }
120 |
121 | /// Translates the multivector by an amount ``. Algebraically, this is equivalent to
122 | /// computing the "sandwich product" `T * m * ~T`.
123 | #[allow(non_snake_case)]
124 | pub fn translate(m: &Multivector, delta_x: f32, delta_y: f32) -> Multivector {
125 | let T = Multivector::translator(delta_x, delta_y);
126 | T * (*m) * T.conjugation()
127 | }
128 |
129 | #[cfg(test)]
130 | mod tests {
131 | use super::*;
132 |
133 | #[test]
134 | fn test_projections() {
135 | let mut p = Multivector::point(1.0, 2.0);
136 | p[6] = 3.0;
137 | let l = Multivector::line(4.0, 5.0, 6.0);
138 | let result = project(&p, &l);
139 | println!("Projection of p onto l, (p | l) * l = {:?}", result);
140 | // Should be: Multivector { coeff: [0.0, 0.0, 0.0, 0.0, -78.0, -87.0, 123.0, 0.0] }
141 |
142 | let result = project(&l, &p);
143 | println!("Projection of l onto p, (p | l) * p = {:?}", result);
144 | // Should be: Multivector { coeff: [0.0, 42.0, -36.0, -45.0, 0.0, 0.0, 0.0, 0.0] }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/interop.rs:
--------------------------------------------------------------------------------
1 | use crate::axioms;
2 | use crate::geometry;
3 | use crate::multivector::Multivector;
4 | use crate::utils;
5 |
6 | use serde::{Deserialize, Serialize};
7 | use wasm_bindgen::prelude::*;
8 |
9 | /// Convenience line struct for passing data to-from WASM. Represents the line
10 | /// `ax + by + c = 0`.
11 | #[wasm_bindgen]
12 | #[derive(Copy, Clone, Serialize, Deserialize)]
13 | pub struct Line {
14 | pub a: f32,
15 | pub b: f32,
16 | pub c: f32,
17 | }
18 |
19 | #[wasm_bindgen]
20 | impl Line {
21 | #[wasm_bindgen(constructor)]
22 | pub fn new(a: f32, b: f32, c: f32) -> Self {
23 | Self { a, b, c }
24 | }
25 | }
26 |
27 | impl Into for Line {
28 | fn into(self) -> Multivector {
29 | Multivector::line(self.a, self.b, self.c)
30 | }
31 | }
32 |
33 | impl From for Line {
34 | fn from(multivector: Multivector) -> Self {
35 | Self::new(multivector.e1(), multivector.e2(), multivector.e0())
36 | }
37 | }
38 |
39 | /// Convenience point struct for passing data to-from WASM.
40 | #[wasm_bindgen]
41 | #[derive(Copy, Clone, Serialize, Deserialize)]
42 | pub struct Point {
43 | pub x: f32,
44 | pub y: f32,
45 | }
46 |
47 | #[wasm_bindgen]
48 | impl Point {
49 | #[wasm_bindgen(constructor)]
50 | pub fn new(x: f32, y: f32) -> Self {
51 | Self { x, y }
52 | }
53 | }
54 |
55 | impl Into for Point {
56 | fn into(self) -> Multivector {
57 | Multivector::point(self.x, self.y)
58 | }
59 | }
60 |
61 | impl From for Point {
62 | fn from(multivector: Multivector) -> Self {
63 | Self::new(multivector.e20(), multivector.e01())
64 | }
65 | }
66 |
67 | #[derive(Serialize, Deserialize)]
68 | pub struct AxiomResult {
69 | pub line: Line,
70 | positive: Vec,
71 | negative: Vec,
72 | }
73 |
74 | impl AxiomResult {
75 | pub fn new(line: &Line, positive: &Vec, negative: &Vec) -> Self {
76 | Self {
77 | line: *line,
78 | positive: positive.clone(),
79 | negative: negative.clone(),
80 | }
81 | }
82 | }
83 |
84 | #[wasm_bindgen]
85 | #[derive(Copy, Clone)]
86 | pub struct Paper {
87 | pub ul: Point,
88 | pub ur: Point,
89 | pub lr: Point,
90 | pub ll: Point,
91 | }
92 | #[wasm_bindgen]
93 | impl Paper {
94 | /// When the application starts, it will construct a new instance of a `Paper` object
95 | /// on the Javascript side, based on the dimensions of the canvas.
96 | #[wasm_bindgen(constructor)]
97 | pub fn new(ul: Point, ur: Point, lr: Point, ll: Point) -> Self {
98 | Self { ul, ur, lr, ll }
99 | }
100 | }
101 |
102 | impl Paper {
103 | fn points(&self) -> Vec {
104 | vec![self.ul, self.ur, self.lr, self.ll]
105 | }
106 |
107 | pub fn intersect(&self, crease: &Multivector) -> (Vec, Vec) {
108 | // Convert points to full multivectors before continuing
109 | let mut vertices: Vec =
110 | self.points().iter().map(|&vertex| vertex.into()).collect();
111 |
112 | // Which side of the crease is each corner on?
113 | let signs = vertices
114 | .iter()
115 | .map(|p| utils::sign_with_tolerance(geometry::dist_point_to_line(p, crease)))
116 | .collect::>();
117 |
118 | let mut cut_points = Vec::new();
119 |
120 | for vertex_index in 0..vertices.len() {
121 | // The index of the other vertex that forms this edge of the paper
122 | let pair_index = (vertex_index + 1) % vertices.len();
123 |
124 | // Always include the corner points
125 | cut_points.push(vertices[vertex_index]);
126 |
127 | // Check if the two vertices that form this edge are on opposite sides of the crease
128 | // (and not *exactly* incident to it)
129 | if signs[vertex_index] != 0.0
130 | && signs[pair_index] != 0.0
131 | && (signs[vertex_index] != signs[pair_index])
132 | {
133 | // Insert cut point (where this face's edge intersects the crease)
134 | let edge = vertices[vertex_index].join(&vertices[pair_index]);
135 | let intersection = edge.meet(crease);
136 |
137 | cut_points.push(intersection)
138 | }
139 | }
140 |
141 | // Which side of the crease is each cut point on (recalculate?
142 | let signs = cut_points
143 | .iter()
144 | .map(|p| utils::sign_with_tolerance(geometry::dist_point_to_line(p, crease)))
145 | .collect::>();
146 |
147 | let mut positive = Vec::new();
148 | let mut negative = Vec::new();
149 |
150 | for (index, sign) in signs.into_iter().enumerate() {
151 | // Normalize the point
152 | let mut point = cut_points[index].normalized();
153 |
154 | // In both cases below, we normalize the point and divide by its e12
155 | // (homogeneous coordinate) before returning - the only difference is,
156 | // for one set of cut points, we reflect them across the crease first
157 | // (to simulate folding behavior)
158 | if sign <= 0.0 || sign.abs() < 0.001 {
159 | point = geometry::reflect(&point, crease);
160 | point /= point.e12();
161 | negative.push(point.into());
162 | }
163 |
164 | if sign >= 0.0 || sign.abs() < 0.001 {
165 | point /= point.e12();
166 | positive.push(point.into());
167 | }
168 | }
169 |
170 | (positive, negative)
171 | }
172 | }
173 |
174 | pub fn bundle_results(paper: &Paper, crease: &Multivector) -> JsValue {
175 | // Find where the crease intersects the paper and return
176 | let (positive, negative) = paper.intersect(crease);
177 | let line = Line::new(crease.e1(), crease.e2(), crease.e0());
178 | let result = AxiomResult::new(&line, &positive, &negative);
179 |
180 | JsValue::from_serde(&result).unwrap()
181 | }
182 |
183 | #[wasm_bindgen]
184 | pub fn axiom_1(paper: &Paper, p0: Point, p1: Point) -> JsValue {
185 | let crease = axioms::axiom_1(&p0.into(), &p1.into());
186 | bundle_results(paper, &crease)
187 | }
188 |
189 | #[wasm_bindgen]
190 | pub fn axiom_2(paper: &Paper, p0: Point, p1: Point) -> JsValue {
191 | let crease = axioms::axiom_2(&p0.into(), &p1.into());
192 | bundle_results(paper, &crease)
193 | }
194 |
195 | #[wasm_bindgen]
196 | pub fn axiom_3(
197 | paper: &Paper,
198 | l0_src: Point,
199 | l0_dst: Point,
200 | l1_src: Point,
201 | l1_dst: Point,
202 | ) -> JsValue {
203 | let l0 = Into::::into(l0_src) & Into::::into(l0_dst);
204 | let l1 = Into::::into(l1_src) & Into::::into(l1_dst);
205 | let crease = axioms::axiom_3(&l0, &l1);
206 |
207 | // Sometimes, this axiom will return a line at infinity (i.e. a line whose only non-zero
208 | // coefficient is e0) - while mathematically correct, we want to avoid passing this to the
209 | // drawing application
210 | //
211 | // This occurs when, for example, the two lines are parallel but oriented in opposite
212 | // directions
213 | if crease.norm().abs() < 0.001 {
214 | return JsValue::null();
215 | }
216 | bundle_results(paper, &crease)
217 | }
218 |
219 | #[wasm_bindgen]
220 | pub fn axiom_4(paper: &Paper, p0: Point, l0_src: Point, l0_dst: Point) -> JsValue {
221 | // Join the two segment endpoints to form the line between them
222 | let l = Into::::into(l0_src) & Into::::into(l0_dst);
223 | let crease = axioms::axiom_4(&p0.into(), &l);
224 | bundle_results(paper, &crease)
225 | }
226 |
227 | #[wasm_bindgen]
228 | pub fn axiom_5(paper: &Paper, p0: Point, p1: Point, l0_src: Point, l0_dst: Point) -> JsValue {
229 | // Join the two segment endpoints to form the line between them
230 | let l = Into::::into(l0_src) & Into::::into(l0_dst);
231 | let maybe_crease = axioms::axiom_5(&p0.into(), &p1.into(), &l);
232 |
233 | if let Some(crease) = maybe_crease {
234 | return bundle_results(paper, &crease);
235 | }
236 |
237 | JsValue::null()
238 | }
239 |
240 | #[wasm_bindgen]
241 | pub fn axiom_7(
242 | paper: &Paper,
243 | p0: Point,
244 | l0_src: Point,
245 | l0_dst: Point,
246 | l1_src: Point,
247 | l1_dst: Point,
248 | ) -> JsValue {
249 | // Join the two segment endpoints to form the line between them
250 | let l0 = Into::::into(l0_src) & Into::::into(l0_dst);
251 | let l1 = Into::::into(l1_src) & Into::::into(l1_dst);
252 | let maybe_crease = axioms::axiom_7(&p0.into(), &l0, &l1);
253 |
254 | if let Some(crease) = maybe_crease {
255 | return bundle_results(paper, &crease);
256 | }
257 |
258 | JsValue::null()
259 | }
260 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | [[package]]
4 | name = "bumpalo"
5 | version = "3.5.0"
6 | source = "registry+https://github.com/rust-lang/crates.io-index"
7 | checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59"
8 |
9 | [[package]]
10 | name = "cfg-if"
11 | version = "0.1.10"
12 | source = "registry+https://github.com/rust-lang/crates.io-index"
13 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
14 |
15 | [[package]]
16 | name = "cfg-if"
17 | version = "1.0.0"
18 | source = "registry+https://github.com/rust-lang/crates.io-index"
19 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
20 |
21 | [[package]]
22 | name = "console_error_panic_hook"
23 | version = "0.1.6"
24 | source = "registry+https://github.com/rust-lang/crates.io-index"
25 | checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211"
26 | dependencies = [
27 | "cfg-if 0.1.10",
28 | "wasm-bindgen",
29 | ]
30 |
31 | [[package]]
32 | name = "itoa"
33 | version = "0.4.7"
34 | source = "registry+https://github.com/rust-lang/crates.io-index"
35 | checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
36 |
37 | [[package]]
38 | name = "js-sys"
39 | version = "0.3.47"
40 | source = "registry+https://github.com/rust-lang/crates.io-index"
41 | checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65"
42 | dependencies = [
43 | "wasm-bindgen",
44 | ]
45 |
46 | [[package]]
47 | name = "lazy_static"
48 | version = "1.4.0"
49 | source = "registry+https://github.com/rust-lang/crates.io-index"
50 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
51 |
52 | [[package]]
53 | name = "libc"
54 | version = "0.2.82"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
57 |
58 | [[package]]
59 | name = "log"
60 | version = "0.4.13"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2"
63 | dependencies = [
64 | "cfg-if 0.1.10",
65 | ]
66 |
67 | [[package]]
68 | name = "memory_units"
69 | version = "0.4.0"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
72 |
73 | [[package]]
74 | name = "pga_axioms"
75 | version = "0.1.0"
76 | dependencies = [
77 | "console_error_panic_hook",
78 | "js-sys",
79 | "serde",
80 | "wasm-bindgen",
81 | "wasm-bindgen-test",
82 | "web-sys",
83 | "wee_alloc",
84 | ]
85 |
86 | [[package]]
87 | name = "proc-macro2"
88 | version = "1.0.24"
89 | source = "registry+https://github.com/rust-lang/crates.io-index"
90 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
91 | dependencies = [
92 | "unicode-xid",
93 | ]
94 |
95 | [[package]]
96 | name = "quote"
97 | version = "1.0.8"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
100 | dependencies = [
101 | "proc-macro2",
102 | ]
103 |
104 | [[package]]
105 | name = "ryu"
106 | version = "1.0.5"
107 | source = "registry+https://github.com/rust-lang/crates.io-index"
108 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
109 |
110 | [[package]]
111 | name = "scoped-tls"
112 | version = "1.0.0"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
115 |
116 | [[package]]
117 | name = "serde"
118 | version = "1.0.123"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
121 | dependencies = [
122 | "serde_derive",
123 | ]
124 |
125 | [[package]]
126 | name = "serde_derive"
127 | version = "1.0.123"
128 | source = "registry+https://github.com/rust-lang/crates.io-index"
129 | checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
130 | dependencies = [
131 | "proc-macro2",
132 | "quote",
133 | "syn",
134 | ]
135 |
136 | [[package]]
137 | name = "serde_json"
138 | version = "1.0.61"
139 | source = "registry+https://github.com/rust-lang/crates.io-index"
140 | checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
141 | dependencies = [
142 | "itoa",
143 | "ryu",
144 | "serde",
145 | ]
146 |
147 | [[package]]
148 | name = "syn"
149 | version = "1.0.60"
150 | source = "registry+https://github.com/rust-lang/crates.io-index"
151 | checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
152 | dependencies = [
153 | "proc-macro2",
154 | "quote",
155 | "unicode-xid",
156 | ]
157 |
158 | [[package]]
159 | name = "unicode-xid"
160 | version = "0.2.1"
161 | source = "registry+https://github.com/rust-lang/crates.io-index"
162 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
163 |
164 | [[package]]
165 | name = "wasm-bindgen"
166 | version = "0.2.70"
167 | source = "registry+https://github.com/rust-lang/crates.io-index"
168 | checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be"
169 | dependencies = [
170 | "cfg-if 1.0.0",
171 | "serde",
172 | "serde_json",
173 | "wasm-bindgen-macro",
174 | ]
175 |
176 | [[package]]
177 | name = "wasm-bindgen-backend"
178 | version = "0.2.70"
179 | source = "registry+https://github.com/rust-lang/crates.io-index"
180 | checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7"
181 | dependencies = [
182 | "bumpalo",
183 | "lazy_static",
184 | "log",
185 | "proc-macro2",
186 | "quote",
187 | "syn",
188 | "wasm-bindgen-shared",
189 | ]
190 |
191 | [[package]]
192 | name = "wasm-bindgen-futures"
193 | version = "0.4.20"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94"
196 | dependencies = [
197 | "cfg-if 1.0.0",
198 | "js-sys",
199 | "wasm-bindgen",
200 | "web-sys",
201 | ]
202 |
203 | [[package]]
204 | name = "wasm-bindgen-macro"
205 | version = "0.2.70"
206 | source = "registry+https://github.com/rust-lang/crates.io-index"
207 | checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c"
208 | dependencies = [
209 | "quote",
210 | "wasm-bindgen-macro-support",
211 | ]
212 |
213 | [[package]]
214 | name = "wasm-bindgen-macro-support"
215 | version = "0.2.70"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385"
218 | dependencies = [
219 | "proc-macro2",
220 | "quote",
221 | "syn",
222 | "wasm-bindgen-backend",
223 | "wasm-bindgen-shared",
224 | ]
225 |
226 | [[package]]
227 | name = "wasm-bindgen-shared"
228 | version = "0.2.70"
229 | source = "registry+https://github.com/rust-lang/crates.io-index"
230 | checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
231 |
232 | [[package]]
233 | name = "wasm-bindgen-test"
234 | version = "0.3.20"
235 | source = "registry+https://github.com/rust-lang/crates.io-index"
236 | checksum = "f0d4da138503a4cf86801b94d95781ee3619faa8feca830569cc6b54997b8b5c"
237 | dependencies = [
238 | "console_error_panic_hook",
239 | "js-sys",
240 | "scoped-tls",
241 | "wasm-bindgen",
242 | "wasm-bindgen-futures",
243 | "wasm-bindgen-test-macro",
244 | ]
245 |
246 | [[package]]
247 | name = "wasm-bindgen-test-macro"
248 | version = "0.3.20"
249 | source = "registry+https://github.com/rust-lang/crates.io-index"
250 | checksum = "c3199c33f06500c731d5544664c24d0c2b742b98debc6b1c6f0c6d6e8fb7c19b"
251 | dependencies = [
252 | "proc-macro2",
253 | "quote",
254 | ]
255 |
256 | [[package]]
257 | name = "web-sys"
258 | version = "0.3.47"
259 | source = "registry+https://github.com/rust-lang/crates.io-index"
260 | checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3"
261 | dependencies = [
262 | "js-sys",
263 | "wasm-bindgen",
264 | ]
265 |
266 | [[package]]
267 | name = "wee_alloc"
268 | version = "0.4.5"
269 | source = "registry+https://github.com/rust-lang/crates.io-index"
270 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
271 | dependencies = [
272 | "cfg-if 0.1.10",
273 | "libc",
274 | "memory_units",
275 | "winapi",
276 | ]
277 |
278 | [[package]]
279 | name = "winapi"
280 | version = "0.3.9"
281 | source = "registry+https://github.com/rust-lang/crates.io-index"
282 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
283 | dependencies = [
284 | "winapi-i686-pc-windows-gnu",
285 | "winapi-x86_64-pc-windows-gnu",
286 | ]
287 |
288 | [[package]]
289 | name = "winapi-i686-pc-windows-gnu"
290 | version = "0.4.0"
291 | source = "registry+https://github.com/rust-lang/crates.io-index"
292 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
293 |
294 | [[package]]
295 | name = "winapi-x86_64-pc-windows-gnu"
296 | version = "0.4.0"
297 | source = "registry+https://github.com/rust-lang/crates.io-index"
298 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
299 |
--------------------------------------------------------------------------------
/site/index.js:
--------------------------------------------------------------------------------
1 | import { SVG } from '@svgdotjs/svg.js'
2 | import '@svgdotjs/svg.draggable.js'
3 |
4 | // import {
5 | // axiom_1
6 | // } from '../pkg';
7 |
8 |
9 | function radiansToDegrees(radians) {
10 | return radians * (180.0 / Math.PI);
11 | }
12 |
13 | function degreesToRadians(degrees) {
14 | return degrees * (Math.PI / 180.0);
15 | }
16 |
17 | import('../pkg/pga_axioms.js').then((wasm) => {
18 |
19 | // Setup the canvas
20 | const w = 500;
21 | const h = 500;
22 | const scx = w * 0.5;
23 | const scy = h * 0.5;
24 | const draw = SVG().addTo('body').size(w, h);
25 | const paperSize = Math.min(w, h) * 0.75;
26 | const pointRadius = 10.0;
27 | const lineWidth = 3.0;
28 | const lineExtrema = [-w * 2.0, 0, w * 2.0, 0];
29 |
30 | // Create the "paper" and grab a reference to its bounding box
31 | const paper = draw.rect(paperSize, paperSize)
32 | .attr('fill', '#ffeeee')
33 | .attr('stroke', '#000000')
34 | .center(w * 0.5, h * 0.5)
35 | .addClass('paper');
36 | const paperBounds = paper.bbox();
37 |
38 | // The WASM code needs to know about the dimensions of our paper, which is what
39 | // this object represents
40 | const paperStruct = new wasm.Paper(
41 | new wasm.Point(scx - paperSize * 0.5, scy - paperSize * 0.5), // Upper-left
42 | new wasm.Point(scx + paperSize * 0.5, scy - paperSize * 0.5), // Upper-right
43 | new wasm.Point(scx + paperSize * 0.5, scy + paperSize * 0.5), // Lower-right
44 | new wasm.Point(scx - paperSize * 0.5, scy + paperSize * 0.5) // Lower-left
45 | );
46 |
47 | const axiomSpecifications = [
48 | {
49 | 'description': 'Given two points p0 and p1, there is a unique fold that passes through both of them.',
50 | 'inputs': {
51 | 'points': [
52 | [scx, scy - paperSize * 0.25],
53 | [scx, scy + paperSize * 0.25]
54 | ],
55 | 'lines': []
56 |
57 | },
58 | 'function': wasm.axiom_1
59 | },
60 | {
61 | 'description': 'Given two points p0 and p1, there is a unique fold that places p0 onto p1.',
62 | 'inputs': {
63 | 'points': [
64 | [scx, scy - paperSize * 0.25],
65 | [scx, scy + paperSize * 0.25]
66 | ],
67 | 'lines': []
68 |
69 | },
70 | 'function': wasm.axiom_2
71 | },
72 | {
73 | 'description': 'Given two lines l0 and l1, there is a fold that places l0 onto l1.',
74 | 'inputs': {
75 | 'points': [],
76 | 'lines': [
77 | [scx - paperSize * 0.25, scy - paperSize * 0.25, scx + paperSize * 0.25, scy - paperSize * 0.25],
78 | [scx - paperSize * 0.25, scy + paperSize * 0.25, scx + paperSize * 0.25, scy + paperSize * 0.25]
79 | ]
80 |
81 | },
82 | 'function': wasm.axiom_3
83 | },
84 | {
85 | 'description': 'Given a point p and a line l, there is a unique fold perpendicular to l that passes through point p.',
86 | 'inputs': {
87 | 'points': [
88 | [scx, scy - paperSize * 0.25]
89 | ],
90 | 'lines': [
91 | [scx - paperSize * 0.25, scy, scx + paperSize * 0.25, scy]
92 | ]
93 |
94 | },
95 | 'function': wasm.axiom_4
96 | },
97 | {
98 | 'description': 'Given two points p0 and p1 and a line l, there is a fold that places p0 onto l and passes through p1.',
99 | 'inputs': {
100 | 'points': [
101 | [scx - paperSize * 0.25, scy - paperSize * 0.125],
102 | [scx + paperSize * 0.25, scy - paperSize * 0.125]
103 | ],
104 | 'lines': [
105 | [scx - paperSize * 0.125, scy + paperSize * 0.125, scx + paperSize * 0.125, scy + paperSize * 0.125]
106 | ]
107 |
108 | },
109 | 'function': wasm.axiom_5
110 | },
111 | {
112 | 'description': 'Given one point p and two lines l0 and l1, there is a fold that places p onto l0 and is perpendicular to l1.',
113 | 'inputs': {
114 | 'points': [
115 | [scx - paperSize * 0.25, scy - paperSize * 0.125]
116 | ],
117 | 'lines': [
118 | [scx - paperSize * 0.125, scy, scx + paperSize * 0.125, scy],
119 | [scx, scy + paperSize * 0.125, scx, scy - paperSize * 0.125]
120 | ]
121 |
122 | },
123 | 'function': wasm.axiom_7
124 | }
125 |
126 | ];
127 |
128 | let currentAxiom = axiomSpecifications[0];
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | let crease = drawLineFromCoeffs(1.0, 2.0, 0.0)
139 | .attr('stroke-width', lineWidth)
140 | .attr('stroke', '#000000')
141 | .attr('stroke-dasharray', "2");
142 |
143 | let positive = draw.polygon([])
144 | .attr({
145 | 'fill': '#c4903d',
146 | 'fill-opacity': 0.75,
147 | 'stroke': '#000000',
148 | 'stroke-width': lineWidth * 0.5
149 | });
150 |
151 | let negative = draw.polygon([])
152 | .attr(positive.attr());
153 |
154 | positive.insertAfter(paper);
155 | negative.insertAfter(positive);
156 | crease.insertAfter(negative)
157 |
158 |
159 |
160 |
161 |
162 |
163 | /// Given the coefficients of a 1-vector in 2D PGA, draw a line.
164 | function drawLineFromCoeffs(a, b, c) {
165 | const yIntercept = -c;
166 | const rotation = -radiansToDegrees(Math.atan2(a, b));
167 |
168 | // Set x-coords to somewhere far off the page ("infinity")
169 | const line = draw.line(-w * 2.0, yIntercept, w * 2.0, yIntercept)
170 | .attr({
171 | 'stroke-width': lineWidth,
172 | 'stroke': '#ff0000',
173 | 'stroke-dasharray': '4'
174 | })
175 | .rotate(rotation, 0.0, 0.0);
176 |
177 | return line;
178 | }
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | // Keeps the element within the bounds of the paper.
196 | function checkPaperBoundaries(e) {
197 | const { handler, box } = e.detail
198 | e.preventDefault()
199 |
200 | // Keep the point(s) inside the paper bounds
201 | let { x, y } = box;
202 |
203 | if (x < paperBounds.x) {
204 | x = paperBounds.x
205 | }
206 | if (y < paperBounds.y) {
207 | y = paperBounds.y
208 | }
209 | if (box.x2 > paperBounds.x2) {
210 | x = paperBounds.x2 - box.w
211 | }
212 | if (box.y2 > paperBounds.y2) {
213 | y = paperBounds.y2 - box.h
214 | }
215 | handler.move(x, y);
216 | }
217 |
218 | // A function that calls into WASM code to calculate a new crease based
219 | // on the current state of the paper.
220 | function callCurrentAxiom() {
221 | // Retrieve all of the relevant points and convert them into WASM objects
222 | const pointCoords = draw.children()
223 | .filter(elem => elem.hasClass('point'))
224 | .map(elem => new wasm.Point(elem.cx(), elem.cy()));
225 |
226 | const segmentEndpointCoords = draw.children()
227 | .filter(elem => elem.hasClass('segment'))
228 | .map(element => [
229 | new wasm.Point(...element.array().slice(0, 2)),
230 | new wasm.Point(...element.array().slice(2, 4))
231 | ]);
232 |
233 | const coords = pointCoords.concat(segmentEndpointCoords);
234 |
235 | // Run current axiom - points then lines (in that order)
236 | const results = currentAxiom.function(
237 | paperStruct,
238 | ...coords
239 | );
240 |
241 | // WASM code will return `null` if no valid creases are found
242 | if (results != null && results.line.a != null && results.line.b != null && results.line.c != null) {
243 | // Rebuild the crease and update the cut polygons
244 | crease.remove();
245 | crease = drawLineFromCoeffs(results.line.a, results.line.b, results.line.c);
246 | crease.insertAfter(negative);
247 | positive.plot(results.positive.map(pt => [pt.x, pt.y]));
248 | negative.plot(results.negative.map(pt => [pt.x, pt.y]));
249 | } else {
250 | // Otherwise, a valid crease wasn't found, so hide the crease and cut polygons
251 | crease.remove();
252 | positive.plot([]);
253 | negative.plot([]);
254 | }
255 | }
256 |
257 | const pointDragCallback = function(e) {
258 | checkPaperBoundaries(e);
259 | callCurrentAxiom();
260 | };
261 |
262 | // Iterates through all of the SVG elements that represent interactive
263 | // line segments and updates their internal attributes to match their
264 | // source and destination endpoints.
265 | function updateSegments() {
266 | draw.children()
267 | .filter(elem => elem.hasClass('segment'))
268 | .forEach(elem => {
269 | // See: https://stackoverflow.com/questions/22636291/svg-line-in-y-mxc-format
270 | const src = elem.remember('src');
271 | const dst = elem.remember('dst');
272 | const theta = radiansToDegrees(Math.atan2(dst.cy() - src.cy(), dst.cx() - src.cx()));
273 | elem.plot(...lineExtrema);
274 | elem.attr('transform', `translate(${src.cx()}, ${src.cy()}) rotate(${theta})`)
275 |
276 | // TODO: this should work but doesn't:
277 | // elem
278 | // .transform({
279 | // x: src.cx(),
280 | // y: src.cy()
281 | // .transform({
282 | // rotation: angleInDegrees
283 | // });
284 |
285 | // Or:
286 | //elem.plot(src.cx(), src.cy(), dst.cx(), dst.cy());
287 | });
288 | }
289 |
290 | const segmentCallback = function(e) {
291 | updateSegments();
292 | pointDragCallback(e);
293 | }
294 |
295 | function clear() {
296 | // Delete all existing points and lines
297 | draw.children()
298 | .filter(elem => elem.hasClass('segment') || elem.hasClass('point'))
299 | .forEach(elem => elem.remove());
300 |
301 | // Remove the existing crease and cut polygons
302 | crease.remove();
303 | positive.plot([]);
304 | negative.plot([]);
305 | }
306 |
307 | function initCurrentAxiom() {
308 | clear();
309 |
310 | // Initialize interactive points
311 | currentAxiom.inputs.points.forEach(coords => {
312 | let circle = draw.circle(pointRadius)
313 | .center(coords[0], coords[1])
314 | .addClass('point')
315 | .draggable();
316 |
317 | circle.on('dragmove.namespace', pointDragCallback);
318 | });
319 |
320 | // Initialize interactive lines
321 | currentAxiom.inputs.lines.forEach(coords => {
322 | // Create the interactive point that represents the source endpoint of this line segment
323 | let src = draw.circle(pointRadius)
324 | .center(...coords.slice(0, 2))
325 | .addClass('point')
326 | .draggable();
327 |
328 | // Create the interactive point that represents the destination endpoint of this line segment
329 | let dst = draw.circle(pointRadius)
330 | .center(...coords.slice(2, 4))
331 | .addClass('point')
332 | .draggable();
333 |
334 | // Create the line segment itself (which is not interactive) - store references to
335 | // its source and destination endpoints so that they can be retrieved later on
336 | const theta = radiansToDegrees(Math.atan2(dst.cy() - src.cy(), dst.cx() - src.cx()));
337 |
338 | let line = draw.line(...lineExtrema)
339 | .attr('transform', `translate(${src.cx()}, ${src.cy()}) rotate(${theta})`)
340 | .attr({
341 | 'stroke-width': lineWidth,
342 | 'stroke': '#312d33',
343 | })
344 | .addClass('segment')
345 | .remember('src', src)
346 | .remember('dst', dst);
347 |
348 | src.insertAfter(line);
349 | dst.insertAfter(line);
350 | src.on('dragmove.namespace', segmentCallback);
351 | dst.on('dragmove.namespace', segmentCallback);
352 | });
353 | }
354 |
355 | function switchAxiom(index) {
356 | // Initialize interactive objects
357 | currentAxiom = axiomSpecifications[index];
358 | initCurrentAxiom();
359 |
360 | // Set some descriptive info text
361 | const descriptionP = document.getElementById('description');
362 | descriptionP.innerHTML = currentAxiom.description;
363 |
364 | // Update crease and cut polygons
365 | callCurrentAxiom();
366 | }
367 |
368 | // Add a key callback for changing between the different axioms.
369 | document.addEventListener('keydown', (event) => {
370 | // Make sure that the key is 1-7 (inclusive)
371 | const key = event.key;
372 | const isValidAxiom = /^[1-7]$/i.test(key)
373 |
374 | // Axioms are numbered 1-7, but array indexing is 0-based, so subtract 1
375 | if (isValidAxiom) {
376 | switchAxiom(key - 1);
377 | }
378 | });
379 |
380 | // Kick off the application
381 | switchAxiom(0);
382 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PGA Axioms
2 | 🗺️ A program for exploring the Huzita-Hatori axioms for origami, using projective geometric algebra (PGA).
3 |
4 | ## Description
5 |
6 | ### Huzita-Hatori Axioms
7 | The Huzita-Hatori axioms are a set of 7 rules that describes ways in which one can fold a piece of paper. Every fold
8 | can be described by one of the 7 axioms. The axioms themselves are described in detail in the following [Wikipedia
9 | article](https://en.wikipedia.org/wiki/Huzita%E2%80%93Hatori_axioms#Axiom_7). As an example, axiom #1 states: "given
10 | two points `p0` and `p1`, there is a unique fold that passes through both of them." In this case, the desired crease
11 | is simple the line that passes through both points.
12 |
13 | This software attempts to turn each axiom into an "interactive sketch," where points and lines can be freely moved
14 | around the canvas ("paper").
15 |
16 | ### Projective Geometric Algebra
17 |
18 | #### Introduction
19 | The main purpose of this project was to explore an emerging field of mathematics known as projective geometric
20 | algebra or PGA. At a high-level, PGA is a different / fresh way of dealing with geometric problems that doesn't
21 | involve "standard" linear algebra. In this algebra, geometric objects like points, lines, and planes are treated as
22 | elements of the algebra. Loosely speaking, this means we can "operate on" these objects. Finding the intersection
23 | between two lines, for example, simply amounts to taking the wedge (or outer, exterior) product between the two line
24 | elements. The wedge product is one of many products available in geometric algebra, and others will be discussed
25 | later on in this document.
26 |
27 | As alluded to in the previous paragraph, the primary benefit of using PGA is that computing things like
28 | intersections, projections, rotations, reflections, etc. is *drastically* simplified (of course, after the up-front
29 | work of learning a new mathematical framework).
30 |
31 | Mathematically, PGA is a graded algebra with signature `R*(2,0,1)`. Here, the numbers `(2,0,1)` denote the number of
32 | positive, negative, and zero dimensions in the algebra. Most readers are probably familiar with `R(3,0,0)`, which
33 | has 3 basis vectors (which we might call `e1`, `e2`, `e3`), each of which squares to 1. In PGA, we have 2 basis
34 | vectors that square to 1, which we will call `e1` and `e2`, and 1 basis vector that squares to 0, which we will call
35 | `e0`. Another familiar example might be the complex numbers, which have signature `R(0,1,0)`, with one basis element
36 | that squares to -1. We usually see this element written as `i`. From this simple set of rules, we build the entire algebra.
37 | The `*` in the signature means that we are working in the *dual* algebra (more on this later).
38 |
39 | The *projective* part of PGA comes from the fact that we are working in a "one-up" space: 2-dimensional PGA has 3
40 | (total) dimensions, used to represent 2-dimensional objects (points and lines). This is identical to the use of homogeneous
41 | coordinates in computer graphics, where a point in 3-space is actually represented by a 4-element vector `(x, y, z, w)`
42 | (where `w` is often implicitly set to 0 or 1). In projective space, objects that only differ by a scalar multiple
43 | represent the *same* object. For example, any point `λ * (x, y, w)` (for `w != 0`) represents the same Euclidean
44 | point `(x, y)`. We recover the inhomogeneous coordinates of the point by dividing by `w`. When `w` is zero, the
45 | point is called an **ideal point** (or a point at infinity). This makes some mathematical sense, as dividing by zero
46 | produces infinity, suggesting that this point is "infinitely far away." Intuitively, ideal points are analogous to
47 | standard direction vectors in linear algebra. There also exists an **ideal line**, representing the line at infinity,
48 | along which all ideal points lie.
49 |
50 | The purpose of including these ideal elements is to avoid "special cases." For example, including the ideal points
51 | allows us to say (without any extra conditions) that two lines intersect at a point. In a non-projective setting,
52 | care must be taken to handle the case of parallel lines. However, in projective space, parallel lines actually
53 | intersect at an ideal point.
54 |
55 | For a more detailed treatment of "points at infinity", see the following [blog post](https://pointatinfinityblog.wordpress.com/2016/04/11/points-at-infinity-i-projective-geometry/).
56 |
57 | #### Multivectors
58 |
59 | The basis elements of 2D PGA are: `1, e0, e1, e2, e01, e20, e12, e012`. The particular choice of basis isn't
60 | important (for example, we could use `e02` instead of `e20`), but we choose this basis so that its dual doesn't
61 | introduce any sign changes (more on duality later) and because it is the same basis used by the Ganja.js code
62 | generator. The basis for 2D PGA is organized as follows:
63 |
64 | - `1` is the **scalar** element
65 | - `e0`, `e1`, and `e2` are the basis **vectors**
66 | - `e01`, `e20`, and `e12` are the basis **bivectors**
67 | - `e012` is the basis **trivector** or **pseudoscalar**
68 |
69 | A general element of 2D PGA is called a **multivector** and can be written as the sum of each of the basis elements
70 | (multiplied by some scalar coefficient): `A + B*e0 + C*e1 + D*e2 + E*e01 + F*e20 + G*e12 + H*e012`. This is
71 | analogous to how we might write a "traditional" vector with components `(x, y, z)` as the sum `x*e1 + y*e2 + z*e3`
72 | (where again, `e1`, `e2`, and `e3` are our 3 basis vectors - the x, y, and z axes). Multivectors are the building
73 | blocks of computation in this program.
74 |
75 | #### Grade Selection
76 |
77 | We can "select out" just the vector part of a multivector, `B*e0 + C*e1 + D*e2`, or perhaps just the bivector part
78 | `E*e01 + F*e20 + G*e12`. This is an operation known as **grade selection** and is often denoted `ₙ` for some
79 | multivector `A` and grade `n`. The previous examples correspond to `₁` and `₂`, respectively.
80 |
81 | #### Geometric Primitives
82 |
83 | In PGA, geometric primitives are represented by vectors of different **grades**. In particular, lines are 1-vectors
84 | and points are 2-vectors (in 3D PGA, we would also have planes). For example, a line with equation `Ax + By + C = 0`
85 | is represented by the 1-vector `Ae1 + Be2 + Ce0`. A Euclidean point with coordinates `(A, B)` is represented by the
86 | 2-vector `Be01 + Ae20 + e12`.
87 |
88 | The reason *why* lines are represented this way stems from the following observations: let's say we have two lines
89 | with equations `Ax + By + C = 0` and `Dx + Ey + F = 0`. Assume that `A*A + B*B = 1` and `D*D + E*E = 1`. To compute
90 | the angle between the pair of lines, it is easy to show that `A*D + B*E = cosα`. It is clear from this example that
91 | the result *does not depend on the third coordinate of each line* (`C` and `F` in the example above). This makes
92 | sense: if you plot the pair of lines, changing the last coordinate has the effect of translating the line, which
93 | clearly does not change the result of our angle calculation. Thus, we assign `e0` to this third coordinate since it
94 | squares to zero.
95 |
96 | #### Products
97 |
98 | In order to perform computation with multivectors, we first need to define the ways in which we can operate on them.
99 | For this, geometric algebra defines various types of products. Note that the list below is not exhaustive: rather,
100 | it contains the products that were necessary for this project.
101 |
102 | ##### Geometric Product
103 |
104 | The geometric product is derived from the simple set of rules outlined in the introduction. Namely, `e0` squares to
105 | 0 (known as a **degenerate metric**), while `e1` and `e2` square to 1. We can multiply the basis vectors together to
106 | produce the basis bivectors: for example, the product `e0 * e1 = e01` cannot be further simplified. Note that we can
107 | "shuffle" the result at the cost of a sign change per move. Using the previous example, `e10 = -e01`. Using these
108 | rules, we can simplify more complicated products: `e01 * e12 = e0112 = e02 = -e20`, where in the third step, we have
109 | used the fact that `e1` squares to 1 to simplify the expression.
110 |
111 | To compute the geometric product between two multivectors, we simply distribute the geometric product across each of
112 | the basis elements and simplify as necessary using the "rules" of our algebra. The geometric product is closed in the
113 | sense that the geometric product between any two multivectors `A` and `B` will also be an element of 2D PGA.
114 |
115 | The geometric product between two k-vectors will be, in general, some mixed-grade object. For example, the geometric
116 | product between a point (grade-2) and a line (grade-1) is a multivector with a vector (grade-1) part and a trivector
117 | (grade-3) part.
118 |
119 | The geometric product is the "main" product of geometric algebra, in the sense that the other products below are
120 | defined with respect to the geometric product.
121 |
122 | ##### Inner Product
123 |
124 | The (symmetric) inner product of a k-vector and an s-vector is the grade-`|k - s|` part of their geometric product.
125 | For example, the inner product between a point (grade-2) and a line (grade-1) is another line (grade-1). There are
126 | other types of inner products (the left and right contractions, for example), but they are not used in this codebase.
127 | To calculate the inner product between two multivectors, we simply distribute the inner product across each of the
128 | k-vector / s-vector components of the operands. For example, in 2D PGA a multivector has a grade-0 part, a grade-1 part,
129 | a grade-2 part, and a grade-3 part. We compute the inner product between the first multivector's grade-0 part and each
130 | grade of the second multivector. We repeat this process for all parts of the first multivector. In the end, we are left
131 | with a handful of intermediate results, each of which will be, in general, some sort of multivector. We add these
132 | together and simplify to obtain the final inner product.
133 |
134 | ##### Outer Product
135 |
136 | The outer (wedge, exterior) product of a k-vector and an s-vector is the grade-`|k + s|` part of their geometric
137 | product (or zero if it does not exist). For example, the outer product between two lines (grade-1) is a point
138 | (grade-2). The outer product between two points (grade-2) is zero (since grade `|2 + 2| = 4` elements do not exist
139 | in this 3-dimensional algebra). To calculate the outer product between two multivectors, we follow the same
140 | procedure outlined above for the inner product, except we use the outer product instead.
141 |
142 | Aside: in many introductory texts, the geometric product is written as the sum of the inner and outer products. For
143 | example, for two vectors `u` and `v`, you might see: `uv = u•v + u^v`. However, for general k-vectors, the geometric
144 | product may contain other terms, so the formula does not necessarily apply.
145 |
146 | #### Meet and Join
147 |
148 | The "meet" between two elements is defined as their outer product. This operator is primarily used to compute
149 | incidence relations (i.e. the point of intersection between two lines).
150 |
151 | In this codebase, the "join" operator (or regressive product) of two multivectors is given by the dual of the outer
152 | product of their duals. For example, "joining" two points `p1 & p2` involves the following operations:
153 |
154 | - Compute the dual of each point, which results in two lines (or projective planes)
155 | - Intersect these lines via the "meet" operator
156 | - Compute the dual of the point of intersection, which results in a line
157 |
158 | Intuitively, "joining" two points results in the line that connects them. There is a bit of nuance here with regard
159 | to orientation. To join `A` and `B`, we actually compute `!(!B ^ !A)`, i.e. the order of `A` and `B` appears to be
160 | swapped. This is the "fully oriented" approach presented in PGA4CS by Dorst (see links below). In 3D, there is
161 | actually both an "undual" and dual operator, and so the formula becomes `undual(dual(B) ^ dual(A))`, but in 2D, this
162 | isn't necessary: Poincaré duality works just fine (the "undual" and dual operators are equivalent). This is still a
163 | bit confusing to me and probably requires closer investigation, but for now, the math works.
164 |
165 | #### Duality
166 |
167 | This codebase implements Poincaré duality. The dual of a basis element is simply whatever must be multiplied on the
168 | right in order to recover the unit pseudoscalar `e012`. For example, the dual of `e0` is `e12`, since `e0 * e12 =
169 | e012`. The dual of `e01` is `e2`, since `e01 * e2 = e012`. The dual of `e012` is 1, and the dual of 1 is `e012`. In
170 | 2D PGA, lines and points are dual to one another.
171 |
172 | #### Involutions
173 |
174 | Any operation that multiplies each grade of a multivector by +/-1 is called a **conjugation**. Conjugations
175 | are [involutions](https://en.wikipedia.org/wiki/Involution_(mathematics)), since applying the operation twice will
176 | result in the original multivector. There are three classical operations of conjugation that are implemented in this
177 | codebase:
178 |
179 | 1. Reversion
180 | 2. Grade involution (or the main involution)
181 | 3. Clifford conjugation
182 |
183 | These are explained in detail in the [following paper](https://arxiv.org/pdf/2005.04015.pdf), but basically, they
184 | are used for computing things like the inverse of a multivector under the geometric product and the sandwich product,
185 | which is needed when working with rotors and translators.
186 |
187 | #### Inverse
188 | The inverse of a multivector `A` is the multivector `A^-1` such that `A * A^-1 = 1`. General multivector inverses
189 | are difficult to calculate, but for dimensions <=5, there are relatively simple formulas for doing so. The [following
190 | paper](http://repository.essex.ac.uk/17282/1/TechReport_CES-534.pdf) explains this process, but basically, inverses
191 | are computed by repeatedly applying involutions to the multivector.
192 |
193 | ## Tested On
194 | - Windows 10
195 | - Rust compiler version `1.51.0`
196 | - Chrome browser
197 |
198 | ## To Build
199 | 1. Clone this repo.
200 | 2. Make sure 🦀 [Rust](https://www.rust-lang.org/en-US/) is installed and `cargo` is in your `PATH`.
201 | 3. Make sure [wasm-pack](https://rustwasm.github.io/wasm-pack/) is installed.
202 | 4. Inside the repo, run: `wasm-pack build`.
203 | 5. `cd` into the `site` subdirectory and run `npm install`.
204 | 6. Run `npm run serve` and go to `localhost:8080` to view the site.
205 |
206 | ## To Use
207 | Once the site is loaded, press 1-7 to switch between the 7 axioms. Points and lines can be dragged around the canvas,
208 | and the fold should update in real-time.
209 |
210 | ## Future Directions
211 | Currently, the software does **not** check whether the calculated crease *actually* lies within the bounds of the
212 | paper. Similarly, it doesn't check whether any of the "output geometry" lies within the bounds of the paper. For
213 | example, axiom #7 states: "given one point `p` and two lines `l0` and `l1`, there is a fold that places `p` onto
214 | `l0` and is perpendicular to `l1`." The software (in its current form) only checks that the reflected point `p'`
215 | lies *somewhere* along the line `l0` (even if it is "off the page"). In a sense, we assume that the sheet of paper
216 | is infinitely large. This is the first issue I would like to address, as I believe it would be relatively simple to
217 | implement.
218 |
219 | Working with full multivectors is convenient and expressive, but at the user-level, it can be a bit cumbersome and
220 | confusing. Originally, I set out to replicate Klein's API (see the links below), where we instead represent points
221 | and lines at the struct level, rather than full multivectors. This introduces some additional type-safety, at the
222 | cost of flexibility and code unification: for example, the formula for reflecting a point across a line is the same
223 | as the formula for reflecting a line across a point (with the arguments swapped). Thus, by using full multivectors,
224 | we only have to write one function `reflect` that works in both cases. If, however, we use `Point` and `Line`
225 | structs, we would have to write two different functions that basically do the same thing: `reflect_point_line` and
226 | `reflect_line_point`. There is probably some middle ground here, which requires further investigation.
227 |
228 | ## To Do
229 | - [ ] Finish axiom #6
230 | - [ ] Combine and/or refactor functions in the `geometry` module, as necessary
231 | - [ ] Explore a more "type-safe" approach to PGA
232 |
233 | ## Credits
234 | I am very much at the beginning stages of my PGA journey, but I would not have been able to learn this topic without
235 | the generous guidance of various members of the [Bivector](https://bivector.net/) community.
236 |
237 | In particular, I would like to thank @enki, @mewertX0rz, @bellinterlab, and @ninepoints for answering so many of my
238 | questions on Discord. If you are at all interested in geometric algebra, I encourage you to check out the Bivector
239 | Discord channel.
240 |
241 | Other notes and papers I found useful throughout the creation of this process include:
242 |
243 | 1. [Charles Gunn's SIGGRAPH notes on PGA](https://arxiv.org/pdf/2002.04509.pdf): along with PGA4CS, one of the most
244 | extensive PGA resources available right now. This paper also contains the "PGA cheatsheet": a super helpful list
245 | of formulas for operating on Euclidean geometry with PGA. All of Gunn's publications were very helpful to me while
246 | learning about PGA.
247 | 2. [PGA4CS](https://bivector.net/PGA4CS.html): an extension to Dorst's original text, "Geometric Algebra for
248 | Computer Science" that is entirely focused on PGA.
249 | 3. [Ganja.js](https://github.com/enkimute/ganja.js): my initial multivector implementation was based on the Ganja.js
250 | code generator. Additionally, I was able to verify most of my implementation against Ganja.js, which was super
251 | useful. I recommend Ganja.js (and Coffeeshop) to anyone who is interested in getting started with GA.
252 | 4. [Klein](https://github.com/jeremyong/klein): an amazing, type-safe C++ library for 3D PGA.
253 |
254 | ### License
255 | [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/)
--------------------------------------------------------------------------------
/src/multivector.rs:
--------------------------------------------------------------------------------
1 | #![allow(non_upper_case_globals)]
2 | use std::fmt::Display;
3 | use std::ops::{
4 | Add, BitAnd, BitOr, BitXor, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Not, Sub,
5 | };
6 |
7 | /// A string representation of all of the basis elements of 2D PGA.
8 | pub const BASIS_ELEMENTS: &'static [&'static str] =
9 | &["1", "e0", "e1", "e2", "e01", "e20", "e12", "e012"];
10 |
11 | /// The total number of basis elements in 2D PGA (i.e. the size of the algebra).
12 | pub const BASIS_COUNT: usize = BASIS_ELEMENTS.len();
13 |
14 | /// Basis elements are available as global constants.
15 | pub const e0: Multivector = Multivector::basis(1, 1.0);
16 | pub const e1: Multivector = Multivector::basis(2, 1.0);
17 | pub const e2: Multivector = Multivector::basis(3, 1.0);
18 | pub const e01: Multivector = Multivector::basis(4, 1.0);
19 | pub const e20: Multivector = Multivector::basis(5, 1.0);
20 | pub const e12: Multivector = Multivector::basis(6, 1.0);
21 | pub const e012: Multivector = Multivector::basis(7, 1.0);
22 |
23 | /// We also include the various permutations of the basis elements above as global constants.
24 | pub const e10: Multivector = Multivector::basis(4, -1.0);
25 | pub const e02: Multivector = Multivector::basis(5, -1.0);
26 | pub const e21: Multivector = Multivector::basis(6, -1.0);
27 | pub const e021: Multivector = Multivector::basis(7, -1.0);
28 | pub const e102: Multivector = Multivector::basis(7, -1.0);
29 | pub const e210: Multivector = Multivector::basis(7, -1.0);
30 | pub const e120: Multivector = Multivector::basis(7, 1.0);
31 | pub const e201: Multivector = Multivector::basis(7, 1.0);
32 |
33 | /// An enum representing the grade of a part of a multivector in 2D PGA.
34 | #[derive(Copy, Clone, Debug, Eq, PartialEq)]
35 | pub enum Grade {
36 | Scalar = 0,
37 | Vector,
38 | Bivector,
39 | Trivector,
40 | }
41 |
42 | impl Grade {
43 | pub fn relevant_blade_indices(&self) -> Vec {
44 | match *self {
45 | Grade::Scalar => vec![0],
46 | Grade::Vector => vec![1, 2, 3],
47 | Grade::Bivector => vec![4, 5, 6],
48 | Grade::Trivector => vec![7],
49 | }
50 | }
51 |
52 | /// Given the index of a particular basis blade, return the grade of that
53 | /// element. For example, indices 1, 2, and 3 would all return `Vector`,
54 | /// since these correspond to the grade-1 elements e0, e1, and e2,
55 | /// respectively.
56 | pub fn from_blade_index(index: usize) -> Result {
57 | match index {
58 | 0 => Ok(Grade::Scalar),
59 | 1..=3 => Ok(Grade::Vector),
60 | 4..=6 => Ok(Grade::Bivector),
61 | 7 => Ok(Grade::Trivector),
62 | _ => Err("Invalid index: should be between 0-7 (inclusive)"),
63 | }
64 | }
65 | }
66 |
67 | /// All of the possible grades in 2D PGA.
68 | const GRADES: [Grade; 4] = [
69 | Grade::Scalar,
70 | Grade::Vector,
71 | Grade::Bivector,
72 | Grade::Trivector,
73 | ];
74 |
75 | /// A multivector is a general element of the algebra R(2, 0, 1), i.e. 2D projective geometric
76 | /// algebra (PGA). For all intents and purposes, it can be thought of as an 8-element array of
77 | /// coefficients with "special" functionality. The coefficients correspond to the 8 basis
78 | /// elements of 2D PGA. For example, let the coefficients be denoted `[A, B, C, D, E, F, G, H]`.
79 | /// Then, the corresponding multivector can be written as:
80 | ///
81 | /// `A + B*e0 + C*e1 + D*e2 + E*e01 + F*e20 + G*e12 + H*e012`
82 | ///
83 | #[derive(Copy, Clone, Debug, PartialEq)]
84 | pub struct Multivector {
85 | /// The coefficients of this multivector.
86 | coeff: [f32; BASIS_COUNT],
87 | }
88 |
89 | impl Multivector {
90 | /// Constructs a new multivector with the specified coefficients.
91 | pub fn with_coefficients(coeff: &[f32; BASIS_COUNT]) -> Self {
92 | Self {
93 | coeff: coeff.clone(),
94 | }
95 | }
96 |
97 | /// Constructs the zero multivector (i.e. a multivector with all coefficients set to zero).
98 | pub const fn zeros() -> Self {
99 | Self {
100 | coeff: [0.0; BASIS_COUNT],
101 | }
102 | }
103 |
104 | /// Constructs a multivector with every coefficient set to 1.
105 | pub const fn ones() -> Self {
106 | Self {
107 | coeff: [1.0; BASIS_COUNT],
108 | }
109 | }
110 |
111 | /// In PGA, the origin is represented by the e12 bivector.
112 | pub const fn origin() -> Self {
113 | e12
114 | }
115 |
116 | /// Equivalent to `Multivector::zeros()`.
117 | pub const fn new() -> Self {
118 | Self::zeros()
119 | }
120 |
121 | /// Constructs a multivector representing a basis element of 2D PGA.
122 | pub const fn basis(index: usize, coeff: f32) -> Self {
123 | let mut multivector = Self::zeros();
124 | multivector.coeff[index] = coeff;
125 | multivector
126 | }
127 |
128 | /// Constructs a multivector that represents a Euclidean point (grade-2 element) with
129 | /// coordinates ``.
130 | pub fn point(x: f32, y: f32) -> Self {
131 | let mut multivector = Self::zeros();
132 | multivector[4] = y; // e01, which is dual to e2
133 | multivector[5] = x; // e20, which is dual to e1
134 | multivector[6] = 1.0;
135 | multivector
136 | }
137 |
138 | /// Constructs a multivector that represents an ideal point (i.e. a point at infinity,
139 | /// grade-2 element) with ideal coordinates ``. This can (for all intents and purposes)
140 | /// be thought of as a 2D "vector" in traditional linear algebra.
141 | pub fn ideal_point(x: f32, y: f32) -> Self {
142 | let mut multivector = Self::zeros();
143 | multivector[4] = y; // e01, which is dual to e2
144 | multivector[5] = x; // e20, which is dual to e1
145 | // Technically, this is unnecessary, but we show it for illustration purposes
146 | multivector[6] = 0.0;
147 | multivector
148 | }
149 |
150 | /// Construct a multivector that represents a line (grade-1 element) with the equation:
151 | /// `ax + by + c = 0`.
152 | pub fn line(a: f32, b: f32, c: f32) -> Self {
153 | let mut multivector = Self::zeros();
154 | multivector[1] = c; // e0
155 | multivector[2] = a; // e1
156 | multivector[3] = b; // e2
157 | multivector
158 | }
159 |
160 | /// Returns a multivector that represents a rotor that performs a rotation by `angle`
161 | /// radians about the Euclidean point `` (`c` for "center" of rotation).
162 | pub fn rotor(angle: f32, cx: f32, cy: f32) -> Self {
163 | let point = Self::point(cx, cy);
164 | let half_angle = angle * 0.5;
165 | point * (half_angle).sin() + (half_angle).cos()
166 | }
167 |
168 | /// Returns a multivector that represents a translator that performs a translation by
169 | /// `` units.
170 | pub fn translator(delta_x: f32, delta_y: f32) -> Self {
171 | // Use the formula: 1 + (d / 2) * P_inf - note, however, that this constructs
172 | // a translator that translates objects in a direction orthogonal to P_inf, which
173 | // is why we construct T with the negative y-coordinate below
174 | let direction = Self::ideal_point(delta_x, -delta_y);
175 | let _amount = direction.ideal_norm();
176 |
177 | // This simplifies to the final return statement:
178 | // (direction / amount) * (amount / 2.0) + 1.0
179 | direction * 0.5 + 1.0
180 | }
181 |
182 | /// Returns the scalar part of the multivector.
183 | pub fn scalar(&self) -> f32 {
184 | self[0]
185 | }
186 |
187 | /// Returns the e0 part of the multivector.
188 | pub fn e0(&self) -> f32 {
189 | self[1]
190 | }
191 |
192 | /// Returns the e1 part of the multivector.
193 | pub fn e1(&self) -> f32 {
194 | self[2]
195 | }
196 |
197 | /// Returns the e2 part of the multivector.
198 | pub fn e2(&self) -> f32 {
199 | self[3]
200 | }
201 |
202 | /// Returns the e01 part of the multivector.
203 | pub fn e01(&self) -> f32 {
204 | self[4]
205 | }
206 |
207 | /// Returns the e20 part of the multivector.
208 | pub fn e20(&self) -> f32 {
209 | self[5]
210 | }
211 |
212 | /// Returns the e12 part of the multivector.
213 | pub fn e12(&self) -> f32 {
214 | self[6]
215 | }
216 |
217 | /// Returns the e012 part of the multivector.
218 | pub fn e012(&self) -> f32 {
219 | self[7]
220 | }
221 |
222 | /// Returns the grade-`n` part of the multivector. For example, calling this function
223 | /// with `Grade::Vector` will return a new multivector with all of its coefficients
224 | /// set to zero except those corresponding to e0, e1, and e2 (the grade-1 parts of
225 | /// the multivector).
226 | pub fn grade_selection(&self, grade: Grade) -> Self {
227 | let mut multivector = self.clone();
228 |
229 | // Figure out which indices to "keep" (i.e. the coefficients that are part
230 | // of the desired grade)
231 | let indices_to_keep = grade.relevant_blade_indices();
232 |
233 | // Set all other coefficients to zero
234 | for index in 0..BASIS_COUNT {
235 | if !indices_to_keep.contains(&index) {
236 | multivector[index] = 0.0;
237 | }
238 | }
239 |
240 | multivector
241 | }
242 |
243 | /// Applies a function to each element of the multivector.
244 | pub fn apply(&mut self, f: fn(f32) -> f32) {
245 | for element in self.coeff.iter_mut() {
246 | *element = f(*element);
247 | }
248 | }
249 |
250 | /// Applies a function to each of the elements that make up the grade-`n` part of the
251 | /// multivector.
252 | pub fn apply_to_grade(&mut self, grade: Grade, f: fn(f32) -> f32) {
253 | for index in grade.relevant_blade_indices() {
254 | self[index] = f(self[index]);
255 | }
256 | }
257 |
258 | /// Computes the Clifford conjugate of the multivector. This is the superposition
259 | /// of the grade involution and reversion operations. It is defined as
260 | /// `(-1)^(k * (k + 1) / 2) * a_k`.
261 | pub fn conjugation(&self) -> Self {
262 | // Negate all but the scalar and trivector parts of the multivector
263 | let mut multivector = self.clone();
264 | multivector[1] = -self[1];
265 | multivector[2] = -self[2];
266 | multivector[3] = -self[3];
267 | multivector[4] = -self[4];
268 | multivector[5] = -self[5];
269 | multivector[6] = -self[6];
270 | multivector
271 | }
272 |
273 | /// Computes the grade involution (also know as the "main involution") of
274 | /// the multivector, which is defined as `(-1)^k * a_k`. In the case of 2D PGA,
275 | /// this means that the vector and trivector parts of the multivector get negated,
276 | /// while the scalar and bivector parts remain the same. The grade involution
277 | /// operator is often denoted with the symbol `^` (above the multivector).
278 | pub fn grade_involution(&self) -> Self {
279 | // Note how only the odd graded parts of the multivector are negated
280 | let mut multivector = self.clone();
281 | // Grade-1 part
282 | multivector[1] = -self[1];
283 | multivector[2] = -self[2];
284 | multivector[3] = -self[3];
285 | // Grade-3 part
286 | multivector[7] = -self[7];
287 | multivector
288 | }
289 |
290 | /// Reverses each element of the multivector. For example, `e20 = e2 * e0` becomes
291 | /// `e02 = e0 * e2`. Effectively, this negates the bivector and trivector parts
292 | /// of the multivector, leaving all other parts the same. The reversion operator
293 | /// is often denoted with the symbol `~`.
294 | ///
295 | /// The formula for reversion is `(-1)^(k * (k - 1) / 2) * a_k `.
296 | pub fn reversion(&self) -> Self {
297 | // Using the formula above, we see that only the grade-2 and grade-3 parts
298 | // of the multivector are affected (which makes sense - the reverse of a
299 | // scalar or vector is just the scalar or vector itself)
300 | let mut multivector = self.clone();
301 | multivector[4] = -self[4];
302 | multivector[5] = -self[5];
303 | multivector[6] = -self[6];
304 | multivector[7] = -self[7];
305 | multivector
306 | }
307 |
308 | /// Computes the inverse `A^-1` of this multivector under the geometric product, such
309 | /// that `A * A^-1 = 1`. The inverse is calculated by taking repeated involutions
310 | /// until the denominator becomes a scalar. This is identical to the process of
311 | /// finding the inverse of a complex number but with more steps (i.e. involutions).
312 | ///
313 | /// Note that `A * A^-1 = A^-1 * A = 1` (i.e. we can multiply by the inverse on either
314 | /// the left or the right side - it doesn't matter).
315 | ///
316 | /// Reference: http://repository.essex.ac.uk/17282/1/TechReport_CES-534.pdf
317 | pub fn inverse(&self) -> Self {
318 | // Note that in the calculations below, `den` will always be a scalar
319 | let num = self.conjugation() * self.grade_involution() * self.reversion();
320 | let den = (*self) * num;
321 | let inverse = num / den.scalar();
322 | inverse
323 | }
324 |
325 | /// An alternative, more verbose way of calculating the dual of this multivector.
326 | pub fn dual(&self) -> Self {
327 | !(*self)
328 | }
329 |
330 | /// Computes the join of two multivectors, which is the dual of the outer product
331 | /// of the duals of the original two multivectors: `!(!A ^ !B)`. The join operation
332 | /// can be used, for example, to construct the line (grade-1 element) that "joins"
333 | /// (i.e. passes through) two points (grade-2 elements).
334 | ///
335 | /// Notationally, we are working in a *dual* projectivized space, so the "wedge"
336 | /// operator corresponds to "meet" and the "vee" operator corresponds to "join".
337 | ///
338 | /// Also note that this version of the "join" operator is orientation preserving
339 | /// and follows the formulas laid out in Dorst's "PGA4CS" book. In particular,
340 | /// the order of the arguments is swapped. In 2D, we don't have to worry about
341 | /// any extraneous sign-flips (as mentioned in the book), since the "dual" and
342 | /// "undual" operations are the exact same, i.e. `!(!a) = a` for any multivector
343 | /// in 2D PGA.
344 | pub fn join(&self, rhs: &Self) -> Self {
345 | let a = *self;
346 | let b = *rhs;
347 | !(!b ^ !a)
348 | }
349 |
350 | /// Computes the meet of two multivectors. This can be used to compute incidence
351 | /// relations. For example, the meet of two lines is their point of intersection
352 | /// (which will be an ideal "point at infinity" if the lines are parallel).
353 | pub fn meet(&self, rhs: &Self) -> Self {
354 | let a = *self;
355 | let b = *rhs;
356 | a ^ b
357 | }
358 |
359 | /// Returns the norm of the multivector.
360 | ///
361 | /// The norm is `|A| = √⟨A * ~A⟩₀`, where `~` is the reversion (or conjugation)
362 | /// operator. The reversion operator makes similar elements cancel each other with
363 | /// a positive or zero scalar, so the square root always exists. For example, a
364 | /// point times itself may, in general, be a negative number. But a point times
365 | /// its reversal will always be non-negative scalar.
366 | ///
367 | /// For a multivector, we compute the square root of the sum of the squares
368 | /// of the norm of each component blade. This leads to the formula above.
369 | ///
370 | /// A k-vector that is normalized will square to +/- 1.
371 | pub fn norm(&self) -> f32 {
372 | // TODO: is the `abs()` necessary here? Maybe it only matters for algebras with
373 | // one or more negative dimensions (like CGA)
374 | let multivector = (*self) * self.conjugation();
375 | multivector.scalar().abs().sqrt()
376 | }
377 |
378 | /// Returns the ideal norm of the multivector.
379 | pub fn ideal_norm(&self) -> f32 {
380 | self.dual().norm()
381 | }
382 |
383 | /// Returns a normalized version of the multivector.
384 | pub fn normalized(&self) -> Self {
385 | (*self) / self.norm()
386 | }
387 |
388 | /// Normalizes the multivector (in-place).
389 | pub fn normalize(&mut self) {
390 | *self /= self.norm();
391 | }
392 | }
393 |
394 | /// Returns an immutable reference to the multivector's coefficient at `index`.
395 | /// For example, `a[2]` would correspond to the e1 component and `a[7]` would
396 | /// correspond to the e012 component.
397 | impl Index for Multivector {
398 | type Output = f32;
399 |
400 | fn index(&self, index: usize) -> &Self::Output {
401 | &self.coeff[index]
402 | }
403 | }
404 |
405 | /// Returns a mutable reference to the multivector's coefficient at `index`.
406 | /// For example, `a[2]` would correspond to the e1 component and `a[7]` would
407 | /// correspond to the e012 component.
408 | impl IndexMut for Multivector {
409 | fn index_mut(&mut self, index: usize) -> &mut Self::Output {
410 | &mut self.coeff[index]
411 | }
412 | }
413 |
414 | /// Computes the join between two multivectors `A & B`.
415 | impl BitAnd for Multivector {
416 | type Output = Self;
417 |
418 | fn bitand(self, rhs: Self) -> Self::Output {
419 | self.join(&rhs)
420 | }
421 | }
422 |
423 | /// Computes the inner product between two multivectors `A | B`. This can
424 | /// be calculated by distributing the geometric product across the k-vectors
425 | /// of each multivector (rather than each basis blade). For example, we
426 | /// multiply the grade-0 part of `A` with the grade-0, grade-1, grade-2,
427 | /// and grade-3 parts of `B` (and repeat for the other 3 parts of `A`).
428 | /// For each such pairing, the inner product is the `|k - s|` grade part
429 | /// of the result. Summing together all of these intermediate results
430 | /// gives us the full inner product between `A` and `B`.
431 | ///
432 | /// In the literature, this is sometimes referred to as the "symmetric
433 | /// inner product" (to distinguish it from left or right contractions,
434 | /// for example).
435 | impl BitOr for Multivector {
436 | type Output = Self;
437 |
438 | fn bitor(self, rhs: Self) -> Self::Output {
439 | let a = self[0];
440 | let b = self[1];
441 | let c = self[2];
442 | let d = self[3];
443 | let e = self[4];
444 | let f = self[5];
445 | let g = self[6];
446 | let h = self[7];
447 |
448 | let i = rhs[0];
449 | let j = rhs[1];
450 | let k = rhs[2];
451 | let l = rhs[3];
452 | let m = rhs[4];
453 | let n = rhs[5];
454 | let o = rhs[6];
455 | let p = rhs[7];
456 |
457 | let mut multivector = Self::zeros();
458 | multivector[0] = a * i + c * k + d * l - g * o;
459 | multivector[1] = b * i + a * j + e * k - f * l + d * n - c * m - h * o - g * p; // e0
460 | multivector[2] = c * i + a * k + g * l - d * o; // e1
461 | multivector[3] = d * i + a * l - g * k + c * o;
462 | multivector[4] = e * i + h * l + a * m + d * p; // e01
463 | multivector[5] = f * i + h * k + a * n + c * p; // e20
464 | multivector[6] = g * i + a * o; // e12
465 | multivector[7] = h * i + a * p; // e012
466 | multivector
467 | }
468 | }
469 |
470 | /// Computes the outer product between two multivectors `A ^ B`. This can
471 | /// be calculated by distributing the geometric product across the k-vectors
472 | /// of each multivector (rather than each basis blade). For example, we
473 | /// multiply the grade-0 part of `A` with the grade-0, grade-1, grade-2,
474 | /// and grade-3 parts of `B` (and repeat for the other 3 parts of `A`).
475 | /// For each such pairing, the outer product is the `|k + s|` grade part
476 | /// of the result. Summing together all of these intermediate results
477 | /// gives us the full outer product between `A` and `B`.
478 | ///
479 | /// In the literature, this is sometimes referred to as the "exterior" or
480 | /// "wedge product."
481 | impl BitXor for Multivector {
482 | type Output = Self;
483 |
484 | fn bitxor(self, rhs: Self) -> Self::Output {
485 | let a = self[0];
486 | let b = self[1];
487 | let c = self[2];
488 | let d = self[3];
489 | let e = self[4];
490 | let f = self[5];
491 | let g = self[6];
492 | let h = self[7];
493 |
494 | let i = rhs[0];
495 | let j = rhs[1];
496 | let k = rhs[2];
497 | let l = rhs[3];
498 | let m = rhs[4];
499 | let n = rhs[5];
500 | let o = rhs[6];
501 | let p = rhs[7];
502 |
503 | let mut multivector = Self::zeros();
504 | multivector[0] = a * i;
505 | multivector[1] = b * i + a * j;
506 | multivector[2] = c * i + a * k;
507 | multivector[3] = d * i + a * l;
508 | multivector[4] = e * i + b * k - c * j + a * m;
509 | multivector[5] = f * i + d * j - b * l + a * n;
510 | multivector[6] = g * i + c * l - d * k + a * o;
511 | multivector[7] = h * i + e * l + f * k + g * j + b * o + c * n + d * m + a * p;
512 | multivector
513 | }
514 | }
515 |
516 | /// Adds two multivectors component-wise `A + B`.
517 | impl Add for Multivector {
518 | type Output = Self;
519 |
520 | fn add(self, rhs: Self) -> Self::Output {
521 | let mut multivector = Self::zeros();
522 | for i in 0..BASIS_COUNT {
523 | multivector[i] = self[i] + rhs[i];
524 | }
525 | multivector
526 | }
527 | }
528 |
529 | /// Adds a scalar to the multivector.
530 | impl Add for Multivector {
531 | type Output = Self;
532 |
533 | fn add(self, rhs: f32) -> Self::Output {
534 | let mut multivector = self.clone();
535 | multivector[0] += rhs;
536 | multivector
537 | }
538 | }
539 |
540 | /// Multiplies a multivector by another multivector's inverse under the
541 | /// geometric product `A * B^-1`.
542 | impl Div for Multivector {
543 | type Output = Self;
544 |
545 | fn div(self, rhs: Self) -> Self::Output {
546 | self * rhs.inverse()
547 | }
548 | }
549 |
550 | /// Divides the multivector by a scalar.
551 | impl Div for Multivector {
552 | type Output = Self;
553 |
554 | fn div(self, rhs: f32) -> Self::Output {
555 | let mut multivector = self.clone();
556 | multivector.coeff.iter_mut().for_each(|elem| *elem /= rhs);
557 | multivector
558 | }
559 | }
560 |
561 | /// Divides the multivector by a scalar (in-place).
562 | impl DivAssign for Multivector {
563 | fn div_assign(&mut self, rhs: f32) {
564 | self.coeff.iter_mut().for_each(|elem| *elem /= rhs);
565 | }
566 | }
567 |
568 | /// Computes the full geometric product between two multivectors `A * B`.
569 | /// This can be calculated by distributing the geometric product across
570 | /// all of the individual basis blades. For example, we multiply the
571 | /// e0 component of `A` with the scalar, e0, e1, e2, ..., e012 components
572 | /// of `B`, and so on. We combine all of the intermediate results (each
573 | /// of which will be, in general, a multivector) to create the full,
574 | /// complete multivector `A * B`.
575 | impl Mul for Multivector {
576 | type Output = Self;
577 |
578 | fn mul(self, rhs: Self) -> Self::Output {
579 | let a = self[0];
580 | let b = self[1];
581 | let c = self[2];
582 | let d = self[3];
583 | let e = self[4];
584 | let f = self[5];
585 | let g = self[6];
586 | let h = self[7];
587 |
588 | let i = rhs[0];
589 | let j = rhs[1];
590 | let k = rhs[2];
591 | let l = rhs[3];
592 | let m = rhs[4];
593 | let n = rhs[5];
594 | let o = rhs[6];
595 | let p = rhs[7];
596 |
597 | let mut multivector = Self::zeros();
598 | multivector[0] = a * i + c * k + d * l - g * o;
599 | multivector[1] = a * j + b * i - c * m + d * n - g * p - f * l + e * k - h * o;
600 | multivector[2] = a * k + c * i - d * o + g * l;
601 | multivector[3] = a * l + c * o - g * k + d * i;
602 | multivector[6] = a * o + c * l - d * k + g * i;
603 | multivector[5] = a * n - b * l + c * p + d * j + g * m + f * i - e * o + h * k;
604 | multivector[4] = a * m + b * k - c * j + d * p - g * n + f * o + e * i + h * l;
605 | multivector[7] = a * p + b * o + c * n + d * m + g * j + f * k + e * l + h * i;
606 | multivector
607 | }
608 | }
609 |
610 | /// Multiplies the multivector by a scalar.
611 | impl Mul for Multivector {
612 | type Output = Self;
613 |
614 | fn mul(self, rhs: f32) -> Self::Output {
615 | let mut multivector = self.clone();
616 | multivector.coeff.iter_mut().for_each(|elem| *elem *= rhs);
617 | multivector
618 | }
619 | }
620 |
621 | /// Multiplies the multivector by a scalar (in-place).
622 | impl MulAssign for Multivector {
623 | fn mul_assign(&mut self, rhs: f32) {
624 | self.coeff.iter_mut().for_each(|elem| *elem *= rhs);
625 | }
626 | }
627 |
628 | /// Negates all components of the multivector.
629 | impl Neg for Multivector {
630 | type Output = Self;
631 |
632 | fn neg(self) -> Self::Output {
633 | let mut multivector = self.clone();
634 | multivector.coeff.iter_mut().for_each(|elem| *elem *= -1.0);
635 | multivector
636 | }
637 | }
638 |
639 | /// Computes the Poincare dual of this multivector. For example, points
640 | /// and lines are dual to one another in 2D PGA.
641 | impl Not for Multivector {
642 | type Output = Self;
643 |
644 | fn not(self) -> Self::Output {
645 | let mut multivector = Self::zeros();
646 | for i in 0..BASIS_COUNT {
647 | // Set:
648 | // Element 0 to element 7
649 | // Element 1 to element 6
650 | // ..etc.
651 | multivector[i] = self[BASIS_COUNT - i - 1];
652 | }
653 | multivector
654 | }
655 | }
656 |
657 | /// Subtracts two multivectors component-wise `A - B`.
658 | impl Sub for Multivector {
659 | type Output = Self;
660 |
661 | fn sub(self, rhs: Self) -> Self::Output {
662 | let mut multivector = Self::zeros();
663 | for i in 0..BASIS_COUNT {
664 | multivector[i] = self[i] - rhs[i];
665 | }
666 | multivector
667 | }
668 | }
669 |
670 | /// Subtracts a scalar from the multivector.
671 | impl Sub for Multivector {
672 | type Output = Self;
673 |
674 | fn sub(self, rhs: f32) -> Self::Output {
675 | let mut multivector = self.clone();
676 | multivector[0] -= rhs;
677 | multivector
678 | }
679 | }
680 |
681 | /// Credit: Ganja.js codegen engine features this implementation.
682 | impl Display for Multivector {
683 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
684 | let eps = 0.00001;
685 | let mut n = 0;
686 | let ret = self
687 | .coeff
688 | .iter()
689 | .enumerate()
690 | .filter_map(|(i, &coeff)| {
691 | if coeff > eps || coeff < -eps {
692 | n = 1;
693 | Some(format!(
694 | "{}{}",
695 | format!("{:.*}", 7, coeff)
696 | .trim_end_matches('0')
697 | .trim_end_matches('.'),
698 | if i > 0 { BASIS_ELEMENTS[i] } else { "" }
699 | ))
700 | } else {
701 | None
702 | }
703 | })
704 | .collect::>()
705 | .join(" + ");
706 | if n == 0 {
707 | write!(f, "0")
708 | } else {
709 | write!(f, "{}", ret)
710 | }
711 | }
712 | }
713 |
714 | #[cfg(test)]
715 | mod tests {
716 | use super::*;
717 |
718 | #[test]
719 | fn test_constructors() {
720 | let a = Multivector::with_coefficients(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
721 | let b = Multivector::zeros();
722 | let c = Multivector::ones();
723 | let d = e0;
724 | }
725 |
726 | #[test]
727 | fn test_display() {
728 | let a = Multivector::with_coefficients(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
729 | let b = Multivector::zeros();
730 | let c = e0;
731 | println!("{}", a);
732 | println!("{}", b);
733 | println!("{}", c);
734 | }
735 |
736 | #[test]
737 | fn test_basis_elements() {
738 | // Should be 0
739 | let result = e0 * e0;
740 | println!("e0 * e0 = {}", result);
741 |
742 | // Should be 1
743 | let result = e1 * e1;
744 | println!("e1 * e1 = {}", result);
745 |
746 | // Should be 1
747 | let result = e2 * e2;
748 | println!("e2 * e2 = {}", result);
749 |
750 | // Should be -1
751 | let result = e12 * e12;
752 | println!("e12 * e12 = {}", result);
753 |
754 | // Should be 0
755 | let result = e20 * e20;
756 | println!("e20 * e20 = {}", result);
757 |
758 | // Should be 0
759 | let result = e01 * e01;
760 | println!("e01 * e01 = {}", result);
761 | }
762 |
763 | #[test]
764 | fn test_inverse() {
765 | // First, try with a simple point (grade-2 element)
766 | let p = Multivector::point(1.0, 2.0);
767 | let p_inv = p.inverse();
768 | let result = p * p_inv;
769 | println!("p * p_inv = {}", result);
770 |
771 | // Then, try with a full multivector
772 | let a = Multivector::with_coefficients(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
773 | let a_inv = a.inverse();
774 | let result = a * a_inv;
775 | println!("a * a_inv = {}", result);
776 | }
777 |
778 | #[test]
779 | fn test_geometric_product() {
780 | // Should be: 23 + 108e0 + -6e1 + -8e2 + -74e01 + -60e20 + -14e12 + -120e012
781 | let a = Multivector::with_coefficients(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
782 | let b = Multivector::with_coefficients(&[-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0]);
783 | let result = a * b;
784 | println!("a * b = {}", result);
785 | }
786 |
787 | #[test]
788 | fn test_inner_product() {
789 | // Should be: 23 + 108e0 + -6e1 + -8e2 + -74e01 + -60e20 + -14e12 + -16e012
790 | let a = Multivector::with_coefficients(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
791 | let b = Multivector::with_coefficients(&[-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0]);
792 | let result = a | b;
793 | println!("a | b = {}", result);
794 | }
795 |
796 | #[test]
797 | fn test_outer_product() {
798 | // Should be: -1 + -4e0 + -6e1 + -8e2 + -10e01 + -12e20 + -14e12 + -120e012
799 | let a = Multivector::with_coefficients(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
800 | let b = Multivector::with_coefficients(&[-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0]);
801 | let result = a ^ b;
802 | println!("a ^ b = {}", result);
803 | }
804 |
805 | #[test]
806 | fn test_join_and_meet() {
807 | // Should be the Euclidean point: <1, -2>
808 | let l1 = Multivector::line(1.0, 2.0, 3.0);
809 | let l2 = Multivector::line(4.0, 5.0, 6.0);
810 | let mut result = l1 ^ l2;
811 | result /= result.e12();
812 | let x = result.e20();
813 | let y = result.e01();
814 | println!(
815 | "l1 ^ l2 = {} or the point <{}, {}> where l1 and l2 meet",
816 | result, x, y
817 | );
818 |
819 | // Should be the line: x - y + 1 = 0
820 | let p1 = Multivector::point(1.0, 2.0);
821 | let p2 = Multivector::point(3.0, 4.0);
822 | let mut result = p1.join(&p2);
823 | //result /= result.e0();
824 | let a = result.e1();
825 | let b = result.e2();
826 | let c = result.e0();
827 | println!(
828 | "p1 & p2 = {} or the line {}x + {}y + {} = 0 that joins p1 and p2",
829 | result, a, b, c
830 | );
831 | }
832 |
833 | #[test]
834 | fn test_rotors_and_translators() {
835 | // Should be the Euclidean point: <3, 4>
836 | let p = Multivector::point(1.0, 2.0);
837 | let T = Multivector::translator(2.0, 2.0);
838 | let mut result = T * p * T.conjugation();
839 | result /= result.e12();
840 | let x = result.e20();
841 | let y = result.e01();
842 | println!(
843 | "T * p * ~T = {} or the translated point <{}, {}>",
844 | result, x, y
845 | );
846 |
847 | let p = Multivector::point(1.0, 2.0);
848 | let R = Multivector::rotor(45.0f32.to_radians(), 0.0, 0.0);
849 | let result = R * p * R.conjugation();
850 | println!("R * p * ~R = {}", result);
851 | }
852 |
853 | #[test]
854 | fn test_norm() {
855 | // Should be ~5 (arbitrary)
856 | let a = Multivector::with_coefficients(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
857 | let b = Multivector::with_coefficients(&[-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0]);
858 | println!("Norm of A: {}", a.norm());
859 | println!("Norm of B: {}", b.norm());
860 |
861 | // Should always be +/- 1
862 | println!("After normalization: {}", a.normalized().norm());
863 | println!("After normalization: {}", b.normalized().norm());
864 | }
865 |
866 | #[test]
867 | fn test_pga_cheatsheet_formulas() {
868 | // The line: x = 0
869 | let l = Multivector::line(1.0, 0.0, 0.0);
870 | let p = Multivector::point(-3.0, 2.0);
871 |
872 | // Should be the Euclidean point: <3, 2>
873 | let mut result = l * p * l;
874 | result = result / result.e12();
875 | let x = result.e20();
876 | let y = result.e01();
877 | println!(
878 | "l * p * l = {} or the point <{}, {}> reflected across l",
879 | result, x, y
880 | );
881 | }
882 | }
883 |
--------------------------------------------------------------------------------