├── 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 | --------------------------------------------------------------------------------